[REF] web: remove legacy calendar view

The calendar view and all its extensions have been converted to
owl, so it's time to get rid of the legacy implementation.

closes odoo/odoo#101816

X-original-commit: 681a6a98c9919021c2f688609e5e4263a9cf4d14
Related: odoo/enterprise#32130
Signed-off-by: Michaël Mattiello <mcm@odoo.com>
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Aaron Bohy
2022-10-01 14:52:16 +02:00
parent eaee797afd
commit 40df434b2d
14 changed files with 2 additions and 8563 deletions
-4
View File
@@ -127,7 +127,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/js/libs/bootstrap.js',
'web/static/src/legacy/js/libs/content-disposition.js',
'web/static/src/legacy/js/libs/download.js',
'web/static/src/legacy/js/libs/fullcalendar.js',
'web/static/src/legacy/js/libs/jquery.js',
'web/static/src/legacy/js/libs/moment.js',
'web/static/src/legacy/js/libs/underscore.js',
@@ -223,7 +222,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/scss/kanban_examples_dialog.scss',
'web/static/src/legacy/scss/kanban_column_progressbar.scss',
'web/static/src/legacy/scss/kanban_view.scss',
'web/static/src/legacy/scss/web_calendar.scss',
'web/static/src/legacy/scss/data_export.scss',
'base/static/src/scss/onboarding.scss',
'web/static/src/legacy/scss/attachment_preview.scss',
@@ -290,7 +288,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/xml/control_panel.xml',
'web/static/src/legacy/xml/fields.xml',
'web/static/src/legacy/xml/kanban.xml',
'web/static/src/legacy/xml/web_calendar.xml',
'web/static/src/legacy/xml/search_panel.xml',
'web/static/src/legacy/xml/week_days.xml',
],
@@ -397,7 +394,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/js/libs/bootstrap.js',
'web/static/src/legacy/js/libs/content-disposition.js',
'web/static/src/legacy/js/libs/download.js',
'web/static/src/legacy/js/libs/fullcalendar.js',
'web/static/src/legacy/js/libs/jquery.js',
'web/static/src/legacy/js/libs/moment.js',
'web/static/src/legacy/js/libs/underscore.js',
@@ -1,258 +0,0 @@
odoo.define('/web/static/src/js/libs/fullcalendar.js', function () {
"use strict";
function createYearCalendarView(FullCalendar) {
const {
Calendar,
createElement,
EventApi,
memoizeRendering,
View,
} = FullCalendar;
class YearView extends View {
constructor() {
super(...arguments);
this.months = null;
this.renderSubCalendarsMem = memoizeRendering(
this.renderSubCalendars, this.unrenderSubCalendars);
this.events = [];
}
//----------------------------------------------------------------------
// Getters
//----------------------------------------------------------------------
get currentDate() {
return this.context.calendar.state.currentDate;
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
/**
* @override
*/
destroy() {
this.renderSubCalendarsMem.unrender();
super.destroy();
}
/**
* Removes the selection on sub calendar.
* Selections on sub calendars are not propagated to this view so
* this view cannot manage them.
*/
unselect() {
for (const { calendar } of this.months) {
calendar.unselect();
}
}
/**
* @override
*/
render() {
this.renderSubCalendarsMem(this.context);
super.render(...arguments);
}
/**
* Renders the main layout (the 4x3 month grid)
*/
renderSubCalendars() {
this.el.classList.add('fc-scroller');
if (!this.context.options.selectable) {
this.el.classList.add('fc-readonly-year-view');
}
this.months = [];
const currentMonthNumber = (new Date()).getMonth();
let currentMonthEl = undefined;
for (let monthNumber = 0; monthNumber < 12; monthNumber++) {
const monthDate = new Date(this.currentDate.getFullYear(), monthNumber);
const monthShortName = moment(monthDate).format('MMM').toLowerCase();
const container = createElement('div', { class: 'fc-month-container' });
this.el.appendChild(container);
const el = createElement('div', {
class: `fc-month fc-month-${monthShortName}`,
});
container.appendChild(el);
const calendar = this._createMonthCalendar(el, monthDate);
this.months.push({ el, calendar });
calendar.render();
if (monthNumber === currentMonthNumber) {
currentMonthEl = el;
}
}
if (currentMonthEl !== undefined) {
currentMonthEl.scrollIntoView();
}
}
/**
* Removes the main layout (the 4x3 month grid).
* Called when view is switched/destroyed.
*/
unrenderSubCalendars() {
for (const { el, calendar } of this.months) {
calendar.destroy();
el.remove();
}
}
/**
* Renders events in sub calendars.
* Called every time event source changed (when changing the date,
* when changing filters, adding/removing filters).
*/
renderEvents() {
// `renderDates` also renders events so if it's called just before
// then do not execute this as it will do a re-render.
if (this.datesRendered) {
this.datesRendered = false;
return;
}
this.events = this._computeEvents();
for (const { calendar } of this.months) {
calendar.refetchEvents();
}
this._setCursorOnEventDates();
}
/**
* Renders dates and events in sub calendars.
* Called when the year of the date changed to render a new
* 4*3 grid of month calendar based on the new year.
*/
renderDates() {
this.events = this._computeEvents();
for (const [monthNumber, { calendar }] of Object.entries(this.months)) {
const monthDate = new Date(this.currentDate.getFullYear(), monthNumber);
calendar.gotoDate(monthDate);
}
this._setCursorOnEventDates();
this.datesRendered = true;
}
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
* @private
*/
_computeEvents() {
const calendar = this.context.calendar;
return calendar.getEvents().map(event => {
const endUTC = calendar.dateEnv.toDate(event._instance.range.end);
const end = new Date(event._instance.range.end);
if (endUTC.getHours() > 0 || endUTC.getMinutes() > 0 ||
endUTC.getSeconds() > 0 || endUTC.getMilliseconds() > 0) {
end.setDate(end.getDate() + 1);
}
// clone event data to not trigger rerendering and issues
const instance = Object.assign({}, event._instance, {
range: { start: new Date(event._instance.range.start), end },
});
const def = Object.assign({}, event._def, {
rendering: 'background',
allDay: true,
});
return new EventApi(this.context.calendar, def, instance);
});
}
/**
* Create a month calendar for the date `monthDate` and mount it on container.
*
* @private
* @param {HTMLElement} container
* @param {Date} monthDate
*/
_createMonthCalendar(container, monthDate) {
return new Calendar(container, Object.assign({}, this.context.options, {
defaultDate: monthDate,
defaultView: 'dayGridMonth',
header: { left: false, center: 'title', right: false },
titleFormat: { month: 'short', year: 'numeric' },
height: 0,
contentHeight: 0,
showNonCurrentDates: false,
views: {
dayGridMonth: {
columnHeaderText: (date) => moment(date).format("ddd")[0],
},
},
selectMinDistance: 5, // needed to not trigger select when click
dateClick: this._onYearDateClick.bind(this),
datesRender: undefined,
events: (info, successCB) => {
successCB(this.events);
},
windowResize: undefined,
}));
}
/**
* Sets fc-has-event class on every dates that have at least one event.
*
* @private
*/
_setCursorOnEventDates() {
for (const el of this.el.querySelectorAll('.fc-has-event')) {
el.classList.remove('fc-has-event');
}
for (const event of Object.values(this.events)) {
let currentDate = moment(event.start);
while (currentDate.isBefore(event.end, 'day')) {
const formattedDate = currentDate.format('YYYY-MM-DD');
const el = this.el.querySelector(`.fc-day-top[data-date="${formattedDate}"]`);
if (el) {
el.classList.add('fc-has-event');
}
currentDate.add(1, 'days');
}
}
}
//----------------------------------------------------------------------
// Handlers
//----------------------------------------------------------------------
/**
* @private
* @param {*} info
*/
_onYearDateClick(info) {
const calendar = this.context.calendar;
const events = this.events
.filter(event => {
const start = moment(event.start);
const end = moment(event.end);
const inclusivity = start.isSame(end, 'day') ? '[]' : '[)';
return moment(info.date).isBetween(start, end, 'day', inclusivity);
})
.map(event => {
return Object.assign({}, event._def, event._instance.range);
});
const yearDateInfo = Object.assign({}, info, {
view: this,
monthView: info.view,
events,
selectable: this.context.options.selectable,
});
calendar.publiclyTrigger('yearDateClick', [yearDateInfo]);
}
}
return FullCalendar.createPlugin({
views: {
dayGridYear: {
class: YearView,
duration: { years: 1 },
defaults: {
fixedWeekCount: true,
},
},
},
});
}
return {
createYearCalendarView,
};
});
@@ -1,573 +0,0 @@
odoo.define('web.CalendarController', function (require) {
"use strict";
/**
* Calendar Controller
*
* This is the controller in the Model-Renderer-Controller architecture of the
* calendar view. Its role is to coordinate the data from the calendar model
* with the renderer, and with the outside world (such as a search view input)
*/
var AbstractController = require('web.AbstractController');
var core = require('web.core');
var Dialog = require('web.Dialog');
var dialogs = require('web.view_dialogs');
var QuickCreate = require('web.CalendarQuickCreate');
var _t = core._t;
var QWeb = core.qweb;
function dateToServer (date, fieldType) {
date = date.clone().locale('en');
if (fieldType === "date") {
return date.local().format('YYYY-MM-DD');
}
return date.utc().format('YYYY-MM-DD HH:mm:ss');
}
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',
updateRecord: '_onUpdateRecord',
viewUpdated: '_onViewUpdated',
AttendeeStatus: '_onAttendeeStatus',
}),
events: _.extend({}, AbstractController.prototype.events, {
'click button.o_calendar_button_new': '_onButtonNew',
'click button.o_calendar_button_prev': '_onButtonNavigation',
'click button.o_calendar_button_today': '_onButtonNavigation',
'click button.o_calendar_button_next': '_onButtonNavigation',
'click button.o_calendar_button_day': '_onButtonScale',
'click button.o_calendar_button_week': '_onButtonScale',
'click button.o_calendar_button_month': '_onButtonScale',
'click button.o_calendar_button_year': '_onButtonScale',
}),
/**
* @override
* @param {Widget} parent
* @param {AbstractModel} model
* @param {AbstractRenderer} renderer
* @param {Object} params
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
this.current_start = null;
this.displayName = params.displayName;
this.quickAddPop = params.quickAddPop;
this.disableQuickCreate = params.disableQuickCreate;
this.eventOpenPopup = params.eventOpenPopup;
this.showUnusualDays = params.showUnusualDays;
this.formViewId = params.formViewId;
this.readonlyFormViewId = params.readonlyFormViewId;
this.mapping = params.mapping;
this.context = params.context;
this.previousOpen = null;
// The quickCreating attribute ensures that we don't do several create
this.quickCreating = false;
this.scales = params.scales;
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Render the buttons according to the CalendarView.buttons template and
* add listeners on it. Set this.$buttons with the produced jQuery element
*
* @param {jQuery} [$node] a jQuery node where the rendered buttons
* should be inserted. $node may be undefined, in which case the Calendar
* inserts them into this.options.$buttons or into a div of its template
*/
renderButtons: function ($node) {
this.$buttons = $(QWeb.render('CalendarView.buttons', this._renderButtonsParameters()));
this.$buttons.find('.o_calendar_button_' + this.mode).addClass('active');
if ($node) {
this.$buttons.appendTo($node);
} else {
this.$('.o_calendar_buttons').replaceWith(this.$buttons);
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Find a className in an array using the start of this class and
* return the last part of a string
* @private
* @param {string} startClassName start of string to find in the "array"
* @param {array|DOMTokenList} classList array of all class
* @return {string|undefined}
*/
_extractLastPartOfClassName(startClassName, classList) {
var result;
classList.forEach(function (value) {
if (value && value.indexOf(startClassName) === 0) {
result = value.substring(startClassName.length);
}
});
return result;
},
/**
* Move to the requested direction and reload the view
*
* @private
* @param {string} to either 'prev', 'next' or 'today'
* @returns {Promise}
*/
_move: function (to) {
this.model[to]();
return this.reload();
},
/**
* Parameter send to QWeb to render the template of Buttons
*
* @private
* @return {{}}
*/
_renderButtonsParameters() {
return {
scales: this.scales,
};
},
/**
* @override
* @private
*/
_update: function () {
var self = this;
if (!this.showUnusualDays) {
return this._super.apply(this, arguments);
}
return this._super.apply(this, arguments).then(function () {
self._rpc({
model: self.modelName,
method: 'get_unusual_days',
args: [dateToServer(self.model.data.start_date, 'date'), dateToServer(self.model.data.end_date, 'date')],
context: self.context,
}).then(function (data) {
_.each(self.$el.find('td.fc-day'), function (td) {
var $td = $(td);
if (data[$td.data('date')]) {
$td.addClass('o_calendar_disabled');
}
});
});
});
},
/**
* @private
* @param {Object} record
* @param {integer} record.id
* @returns {Promise}
*/
_updateRecord: function (record) {
var reload = this.reload.bind(this, {});
return this.model.updateRecord(record).then(reload, reload);
},
/**
* @private
*/
_getEventId(event) {
var id = event.data._id;
id = id && parseInt(id).toString() === id ? parseInt(id) : id;
return id;
},
/**
* @private
*/
_getFormDialogOptions: function(self, event) {
var id = this._getEventId(event);
return {
res_model: self.modelName,
res_id: id || null,
context: event.context || self.context,
title: _.str.sprintf(_t("Open: %s"), event.data.title),
on_saved: function () {
if (event.data.on_save) {
event.data.on_save();
}
self.reload();
},
};
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Handler when a user clicks on button to create event
*
* @private
*/
_onButtonNew() {
this.trigger_up('switch_view', {view_type: 'form'});
},
/**
* Handler when a user click on navigation button like prev, next, ...
*
* @private
* @param {Event|jQueryEvent} jsEvent
*/
_onButtonNavigation(jsEvent) {
const action = this._extractLastPartOfClassName('o_calendar_button_', jsEvent.currentTarget.classList);
if (action) {
this._move(action);
}
},
/**
* Handler when a user click on scale button like day, month, ...
*
* @private
* @param {Event|jQueryEvent} jsEvent
*/
_onButtonScale(jsEvent) {
const scale = this._extractLastPartOfClassName('o_calendar_button_', jsEvent.currentTarget.classList);
if (scale) {
this.model.setScale(scale);
this.reload();
}
},
/**
* @private
* @param {OdooEvent} event
*/
_onChangeDate: function (event) {
var modelData = this.model.get();
if (modelData.target_date.format('YYYY-MM-DD') === event.data.date.format('YYYY-MM-DD')) {
// When clicking on same date, toggle between the two views
switch (modelData.scale) {
case 'month': this.model.setScale('week'); break;
case 'week': this.model.setScale('day'); break;
case 'day': this.model.setScale('month'); break;
}
} else if (modelData.target_date.week() === event.data.date.week()) {
// When clicking on a date in the same week, switch to day view
this.model.setScale('day');
} else {
// When clicking on a random day of a random other week, switch to week view
this.model.setScale('week');
}
if (event.data.scale) {
this.model.setScale(event.data.scale);
}
this.model.setDate(event.data.date);
this.reload();
},
/**
* @private
* @param {OdooEvent} event
*/
_onChangeFilter: async function (event) {
if (event.data.value !== 'all' && event.target.filter_field) {
const domain = [['user_id', '=', this.getSession().uid]];
if (event.data.value !== 'check_all') {
domain.push([event.target.write_field, '=', parseInt(event.data.value)]);
}
const existingFilter = await this._rpc({
model: event.target.write_model,
method: 'search',
args: [domain],
});
let val = {
[event.target.filter_field]: event.data.active,
};
await this._rpc({
model: event.target.write_model,
method: 'write',
args: [existingFilter, val],
});
}
if (this.model.changeFilter(event.data) && !event.data.no_reload) {
this.reload();
}
},
/**
* @private
* @param {OdooEvent} event
*/
_onDeleteRecord: async function (event) {
var self = this;
if (event.data.event.record.recurrency) {
const recurrenceUpdate = await this._askRecurrenceUpdatePolicy();
event.data = Object.assign({}, event.data, {
'recurrenceUpdate': recurrenceUpdate,
});
if (recurrenceUpdate === 'self_only') {
self.model.deleteRecords([event.data.id], self.modelName).then(function () {
self.reload();
});
} else {
return this._rpc({
model: self.modelName,
method: 'action_mass_deletion',
args: [[event.data.id], recurrenceUpdate],
}).then( function () {
self.reload();
});
}
} else {
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,
}));
},
/**
* @private
* @param {OdooEvent} event
*/
_onNext: function (event) {
event.stopPropagation();
this._move('next');
},
/**
* @private
* @param {OdooEvent} event
*/
_onOpenCreate: function (event) {
var self = this;
if (["year", "month"].includes(this.model.get().scale)) {
event.data.allDay = true;
}
var data = this.model.calendarEventToRecord(event.data);
var context = _.extend({}, this.context, event.options && event.options.context);
// context default has more priority in default_get so if data.name is false then it may
// lead to error/warning while saving record in form view as name field can be required
if (data.name) {
context.default_name = data.name;
}
context['default_' + this.mapping.date_start] = data[this.mapping.date_start] || null;
if (this.mapping.date_stop) {
context['default_' + this.mapping.date_stop] = data[this.mapping.date_stop] || null;
}
if (this.mapping.date_delay) {
context['default_' + this.mapping.date_delay] = data[this.mapping.date_delay] || null;
}
if (this.mapping.all_day) {
context['default_' + this.mapping.all_day] = data[this.mapping.all_day] || null;
}
for (var k in context) {
if (context[k] && context[k]._isAMomentObject) {
context[k] = dateToServer(context[k]);
}
}
var options = _.extend({}, this.options, event.options, {
context: context,
title: this._setEventTitle()
});
if (this.quick != null) {
this.quick.destroy();
this.quick = null;
}
if (!options.disableQuickCreate && !event.data.disableQuickCreate && this.quickAddPop) {
this.quick = new QuickCreate(this, true, options, data, event.data);
this.quick.open();
this.quick.opened(function () {
self.quick.focus();
});
return;
}
if (this.eventOpenPopup) {
if (this.previousOpen) { this.previousOpen.close(); }
this.previousOpen = new dialogs.FormViewDialog(self, {
res_model: this.modelName,
context: context,
title: options.title,
view_id: this.formViewId || false,
disable_multiple_selection: true,
on_saved: function () {
if (event.data.on_save) {
event.data.on_save();
}
self.reload();
},
});
this.previousOpen.on('closed', this, () => {
if (event.data.on_close) {
event.data.on_close();
}
})
this.previousOpen.open();
} else {
this.do_action({
type: 'ir.actions.act_window',
res_model: this.modelName,
views: [[this.formViewId || false, 'form']],
target: 'current',
context: context,
});
}
},
/**
* @private
* @param {OdooEvent} event
*/
_onOpenEvent: function (event) {
var self = this;
var id = self._getEventId(event);
if (!this.eventOpenPopup) {
this._rpc({
model: self.modelName,
method: 'get_formview_id',
//The event can be called by a view that can have another context than the default one.
args: [[id]],
context: event.context || self.context,
}).then(function (viewId) {
self.do_action({
type:'ir.actions.act_window',
res_id: id,
res_model: self.modelName,
views: [[viewId || false, 'form']],
target: 'current',
context: event.context || self.context,
});
});
return;
}
var options = self._getFormDialogOptions(self, event);
if (this.formViewId) {
options.view_id = parseInt(this.formViewId);
}
new dialogs.FormViewDialog(this, options).open();
},
/**
* @private
* @param {OdooEvent} ev
*/
_onPrev: function (ev) {
ev.stopPropagation();
this._move('prev');
},
/**
* Handles saving data coming from quick create box
*
* @private
* @param {OdooEvent} event
*/
_onQuickCreate: function (event) {
var self = this;
if (this.quickCreating) {
return;
}
this.quickCreating = true;
this.model.createRecord(event)
.then(function () {
self.quick.destroy();
self.quick = null;
self.reload();
self.quickCreating = false;
})
.guardedCatch(function (result) {
var errorEvent = result.event;
// This will occurs if there are some more fields required
// Preventdefaulting the error event will prevent the traceback window
errorEvent.preventDefault();
event.data.options.disableQuickCreate = true;
event.data.data.on_save = self.quick.destroy.bind(self.quick);
self._onOpenCreate(event.data);
self.quickCreating = false;
});
},
/**
* @private
* @param {OdooEvent} event
*/
_onUpdateRecord: function (event) {
this._updateRecord(event.data);
},
/**
* The internal state of the calendar (mode, period displayed) has changed,
* so update the control panel buttons and breadcrumbs accordingly.
*
* @private
* @param {OdooEvent} event
*/
_onViewUpdated: function (event) {
this.mode = event.data.mode;
if (this.$buttons) {
this.$buttons.find('.active').removeClass('active');
const scaleSelection = this.$buttons.find('.scale_button_selection')[0];
if (scaleSelection) {
scaleSelection.innerText = this.mode;
}
this.$buttons.find('.o_calendar_button_' + this.mode).addClass('active');
}
const title = `${this.displayName} (${event.data.title})`;
return this.updateControlPanel({ title });
},
/**
* Update Attendee status in batch for recurrent events
* @private
* @param {OdooEvent} event
*/
_onAttendeeStatus: async function(event) {
const self = this;
let recurrenceUpdate = false;
if (event.data.record.recurrency) {
recurrenceUpdate = await this._askRecurrenceUpdatePolicy();
event.data = Object.assign({}, event.data, {
'recurrenceUpdate': recurrenceUpdate,
});
}
return this._rpc({
model: self.modelName,
method: 'change_attendee_status',
args: [[event.data.id], event.data.selectedStatus, recurrenceUpdate],
}).then( function () {
self.reload();
});
},
/**
* This function has been created for the only purpose of
* changing the title on the quick create from a calendar.
* This way it can be overriden in other apps
*/
_setEventTitle: function () {
return _t('New Event');
},
});
return CalendarController;
});
@@ -1,824 +0,0 @@
odoo.define('web.CalendarModel', function (require) {
"use strict";
var AbstractModel = require('web.AbstractModel');
var Context = require('web.Context');
var core = require('web.core');
var fieldUtils = require('web.field_utils');
var session = require('web.session');
var _t = core._t;
function dateToServer (date) {
return date.clone().utc().locale('en').format('YYYY-MM-DD HH:mm:ss');
}
return AbstractModel.extend({
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
this.end_date = null;
var week_start = _t.database.parameters.week_start;
// calendar uses index 0 for Sunday but Odoo stores it as 7
this.week_start = week_start !== undefined && week_start !== false ? week_start % 7 : moment().startOf('week').day();
this.week_stop = this.week_start + 6;
this.filter_check_all = {};
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Transform fullcalendar event object to OpenERP Data object
*/
calendarEventToRecord: function (event) {
// Normalize event_end without changing fullcalendars event.
var data = {};
data[this.mapping.create_name_field || 'name'] = event.title;
var start = event.start.clone();
var end = event.end && event.end.clone();
// Set end date if not existing
if (!end || end.diff(start) < 0) { // undefined or invalid end date
if (event.allDay) {
end = start.clone();
} else {
// in week mode or day mode, convert allday event to event
end = start.clone().add(2, 'h');
}
} else if (event.allDay) {
// For an "allDay", FullCalendar gives the end day as the
// next day at midnight (instead of 23h59).
end.add(-1, 'days');
}
var isDateEvent = this.fields[this.mapping.date_start].type === 'date';
// An "allDay" event without the "all_day" option is not considered
// as a 24h day. It's just a part of the day (by default: 7h-19h).
if (event.allDay) {
if (!this.mapping.all_day && !isDateEvent) {
if (event.r_start) {
start.hours(event.r_start.hours())
.minutes(event.r_start.minutes())
.seconds(event.r_start.seconds())
.utc();
end.hours(event.r_end.hours())
.minutes(event.r_end.minutes())
.seconds(event.r_end.seconds())
.utc();
} else {
// default hours in the user's timezone
start.hours(7);
end.hours(19);
}
start.add(-this.getSession().getTZOffset(start), 'minutes');
end.add(-this.getSession().getTZOffset(end), 'minutes');
}
} else {
start.add(-this.getSession().getTZOffset(start), 'minutes');
end.add(-this.getSession().getTZOffset(end), 'minutes');
}
if (this.mapping.all_day) {
if (event.record) {
data[this.mapping.all_day] =
(this.data.scale !== 'month' && event.allDay) ||
event.record[this.mapping.all_day] &&
end.diff(start) < 10 ||
false;
} else {
data[this.mapping.all_day] = event.allDay;
}
}
data[this.mapping.date_start] = start;
if (this.mapping.date_stop) {
data[this.mapping.date_stop] = end;
}
if (this.mapping.date_delay) {
if (this.data.scale !== 'month' || (this.data.scale === 'month' && !event.drop)) {
data[this.mapping.date_delay] = (end.diff(start) <= 0 ? end.endOf('day').diff(start) : end.diff(start)) / 1000 / 3600;
}
}
return data;
},
/**
* @param {Object} filter
* @returns {boolean}
*/
changeFilter: function (filter) {
var Filter = this.data.filters[filter.fieldName];
if (filter.value === 'all') {
Filter.all = filter.active;
}
else if (filter.value === 'check_all') {
// Select all numeric filters and update the checkboxes
this.filter_check_all[filter.fieldName] = filter.active;
// Deactivate the All selection
Filter.all = false;
if (filter.active) {
// Check every displayed filters except the 'all' filter
Filter.filters.filter(el => el.value !== 'all').forEach(element => element.active = true );
return true;
} else {
Filter.filters.forEach(element => element.active = false);
return true;
}
}
var f = _.find(Filter.filters, function (f) {
return f.value === filter.value;
});
if (f) {
if (f.active !== filter.active) {
// Update the value according to the checkbox state
f.active = filter.active;
} else if (filter.active) {
// if the filter is NOT active, we remove it by returning true [triggered by _onFilterRemove]
return false;
}
} else if (filter.active) {
Filter.filters.push({
value: filter.value,
active: true,
});
}
return true;
},
/**
* @param {OdooEvent} event
*/
createRecord: function (event) {
var data = this.calendarEventToRecord(event.data.data);
for (var k in data) {
if (data[k] && data[k]._isAMomentObject) {
data[k] = dateToServer(data[k]);
}
}
return this._rpc({
model: this.modelName,
method: 'create',
args: [data],
context: event.data.options.context,
});
},
/**
* @param {any} ids
* @param {any} model
* @returns
*/
deleteRecords: function (ids, model) {
return this._rpc({
model: model,
method: 'unlink',
args: [ids],
context: session.user_context, // todo: combine with view context
});
},
/**
* @override
* @returns {Object}
*/
__get: function () {
return _.extend({}, this.data, {
fields: this.fields
});
},
/**
* @override
* @param {any} params
* @returns {Promise}
*/
__load: function (params) {
var self = this;
this.modelName = params.modelName;
this.fields = params.fields;
this.fieldNames = params.fieldNames;
this.fieldsInfo = params.fieldsInfo;
this.mapping = params.mapping;
this.mode = params.mode; // one of month, week or day
this.scales = params.scales; // one of month, week or day
this.scalesInfo = params.scalesInfo;
// Check whether the date field is editable (i.e. if the events can be
// dragged and dropped)
this.editable = params.editable;
this.creatable = params.creatable;
// display more button when there are too much event on one day
this.eventLimit = params.eventLimit;
// fields to display color, e.g.: user_id.partner_id
this.fieldColor = params.fieldColor;
if (!this.preloadPromise) {
this.preloadPromise = new Promise(function (resolve, reject) {
Promise.all([
self._rpc({model: self.modelName, method: 'check_access_rights', args: ["write", false]}),
self._rpc({model: self.modelName, method: 'check_access_rights', args: ["create", false]})
]).then(function (result) {
var write = result[0];
var create = result[1];
self.write_right = write;
self.create_right = create;
resolve();
}).guardedCatch(reject);
});
}
this.data = {
domain: params.domain,
context: params.context,
// get in arch the filter to display in the sidebar and the field to read
filters: params.filters,
};
this.setDate(params.initialDate);
// Use mode attribute in xml file to specify zoom timeline (day,week,month)
// by default month.
this.setScale(params.mode);
_.each(this.data.filters, function (filter) {
if (filter.avatar_field && !filter.avatar_model) {
filter.avatar_model = self.modelName;
}
});
return this.preloadPromise.then(this._loadCalendar.bind(this));
},
/**
* Move the current date range to the next period
*/
next: function () {
this.setDate(this.data.target_date.clone().add(1, this.data.scale));
},
/**
* Move the current date range to the previous period
*/
prev: function () {
this.setDate(this.data.target_date.clone().add(-1, this.data.scale));
},
/**
* @override
* @param {Object} [params.context]
* @param {Array} [params.domain]
* @returns {Promise}
*/
__reload: function (handle, params) {
if (params.domain) {
this.data.domain = params.domain;
}
if (params.context) {
this.data.context = params.context;
}
return this._loadCalendar();
},
/**
* @param {Moment} start. in local TZ
*/
setDate: function (start) {
// keep highlight/target_date in localtime
this.data.highlight_date = this.data.target_date = start.clone();
this.data.start_date = this.data.end_date = start;
switch (this.data.scale) {
case 'year': {
const yearStart = this.data.start_date.clone().startOf('year');
let yearStartDay = this.week_start;
if (yearStart.day() < yearStartDay) {
// the 1st of January is before our week start (e.g. week start is Monday, and
// 01/01 is Sunday), so we go one week back
yearStartDay -= 7;
}
this.data.start_date = yearStart.day(yearStartDay).startOf('day');
this.data.end_date = this.data.end_date.clone()
.endOf('year').day(this.week_stop).endOf('day');
break;
}
case 'month':
var monthStart = this.data.start_date.clone().startOf('month');
var monthStartDay;
if (monthStart.day() >= this.week_start) {
// the month's first day is after our week start
// Then we are in the right week
monthStartDay = this.week_start;
} else {
// The month's first day is before our week start
// Then we should go back to the the previous week
monthStartDay = this.week_start - 7;
}
this.data.start_date = monthStart.day(monthStartDay).startOf('day');
this.data.end_date = this.data.start_date.clone().add(5, 'week').day(this.week_stop).endOf('day');
break;
case 'week':
var weekStartDay = this.week_start;
if (this.data.start_date.day() < this.week_start) {
// The week's first day is after our current day
// Then we should go back to the previous week
weekStartDay -= 7;
}
this.data.start_date = this.data.start_date.clone().day(weekStartDay).startOf('day');
this.data.end_date = this.data.end_date.clone().day(weekStartDay + 6).endOf('day');
break;
default:
this.data.start_date = this.data.start_date.clone().startOf('day');
this.data.end_date = this.data.end_date.clone().endOf('day');
}
// We have set start/stop datetime as definite begin/end boundaries of a period (month, week, day)
// in local TZ (what is the begining of the week *I am* in ?)
// The following code:
// - converts those to UTC using our homemade method (testable)
// - sets the moment UTC flag to true, to ensure compatibility with third party libs
var manualUtcDateStart = this.data.start_date.clone().add(-this.getSession().getTZOffset(this.data.start_date), 'minutes');
var formattedUtcDateStart = manualUtcDateStart.format('YYYY-MM-DDTHH:mm:ss') + 'Z';
this.data.start_date = moment.utc(formattedUtcDateStart);
var manualUtcDateEnd = this.data.end_date.clone().add(-this.getSession().getTZOffset(this.data.start_date), 'minutes');
var formattedUtcDateEnd = manualUtcDateEnd.format('YYYY-MM-DDTHH:mm:ss') + 'Z';
this.data.end_date = moment.utc(formattedUtcDateEnd);
},
/**
* @param {string} scale the scale to set
*/
setScale: function (scale) {
if (!_.contains(this.scales, scale)) {
scale = "week";
}
this.data.scale = scale;
this.setDate(this.data.target_date);
},
/**
* Move the current date range to the period containing today
*/
today: function () {
this.setDate(moment(new Date()));
},
/**
* @param {Object} record
* @param {integer} record.id
* @returns {Promise}
*/
updateRecord: function (record) {
// Cannot modify actual name yet
var data = _.omit(this.calendarEventToRecord(record), 'name');
for (var k in data) {
if (data[k] && data[k]._isAMomentObject) {
data[k] = dateToServer(data[k]);
}
}
var context = new Context(this.data.context, {from_ui: true});
return this._rpc({
model: this.modelName,
method: 'write',
args: [[parseInt(record.id, 10)], data],
context: context
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Converts this.data.filters into a domain
*
* @private
* @returns {Array}
*/
_getFilterDomain: function () {
// List authorized values for every field
// fields with an active 'all' filter are skipped
var authorizedValues = {};
var avoidValues = {};
_.each(this.data.filters, function (filter) {
// Skip 'all' filters because they do not affect the domain
if (filter.all) return;
// all visible filters have been activated
if (filter.visible) return;
// Loop over subfilters to complete authorizedValues
_.each(filter.filters, function (f) {
if (filter.write_model) {
if (!authorizedValues[filter.fieldName])
authorizedValues[filter.fieldName] = [];
if (f.active) {
authorizedValues[filter.fieldName].push(f.value);
}
} else {
if (!f.active) {
if (!avoidValues[filter.fieldName])
avoidValues[filter.fieldName] = [];
avoidValues[filter.fieldName].push(f.value);
}
}
});
});
// Compute the domain
var domain = [];
for (var field in authorizedValues) {
domain.push([field, 'in', authorizedValues[field]]);
}
for (var field in avoidValues) {
if (avoidValues[field].length > 0) {
domain.push([field, 'not in', avoidValues[field]]);
}
}
return domain;
},
/**
* @private
* @returns {Object}
*/
_getFullCalendarOptions: function () {
var format12Hour = {
hour: 'numeric',
minute: '2-digit',
omitZeroMinute: true,
meridiem: 'short'
};
var format24Hour = {
hour: 'numeric',
minute: '2-digit',
hour12: false,
};
return {
defaultView: this.scalesInfo[this.mode || 'week'],
header: false,
selectable: this.creatable && this.create_right,
selectMirror: true,
editable: this.editable,
droppable: true,
navLinks: false,
eventLimit: this.eventLimit, // allow "more" link when too many events
snapMinutes: 15,
longPressDelay: 500,
eventResizableFromStart: true,
nowIndicator: true,
weekNumbers: true,
weekNumbersWithinDays: true,
weekNumberCalculation: function (date) {
// Since FullCalendar v4 ISO 8601 week date is preferred so we force the old system
return moment(date).week();
},
weekLabel: _t("Week"),
allDayText: _t("All day"),
monthNames: moment.months(),
monthNamesShort: moment.monthsShort(),
dayNames: moment.weekdays(),
dayNamesShort: moment.weekdaysShort(),
dayNamesMin: moment.weekdaysMin(),
firstDay: this.week_start,
slotLabelFormat: _t.database.parameters.time_format.search("%H") !== -1 ? format24Hour : format12Hour,
allDaySlot: this.mapping.all_day || this.fields[this.mapping.date_start].type === 'date',
};
},
/**
* Return a domain from the date range
*
* @private
* @returns {Array} A domain containing datetimes start and stop in UTC
* those datetimes are formatted according to server's standards
*/
_getRangeDomain: function () {
// Build OpenERP Domain to filter object by this.mapping.date_start field
// between given start, end dates.
var domain = [[this.mapping.date_start, '<=', dateToServer(this.data.end_date)]];
if (this.mapping.date_stop) {
domain.push([this.mapping.date_stop, '>=', dateToServer(this.data.start_date)]);
} else if (!this.mapping.date_delay) {
domain.push([this.mapping.date_start, '>=', dateToServer(this.data.start_date)]);
}
return domain;
},
/**
* @private
* @returns {Promise}
*/
_loadCalendar: function () {
var self = this;
this.data.fc_options = this._getFullCalendarOptions();
var defs = _.map(this.data.filters, this._loadFilter.bind(this));
return Promise.all(defs).then(function () {
return self._rpc({
model: self.modelName,
method: 'search_read',
context: self.data.context,
fields: self.fieldNames,
domain: self.data.domain.concat(self._getRangeDomain()).concat(self._getFilterDomain())
})
.then(async function (events) {
self._parseServerData(events);
self.data.data = await self._getCalendarEventData(events);
return Promise.all([
self._loadColors(self.data, self.data.data),
self._loadRecordsToFilters(self.data, self.data.data)
]);
});
});
},
/**
* @private
* @param {any} element
* @param {any} events
* @returns {Promise}
*/
_loadColors: function (element, events) {
if (this.fieldColor) {
const fieldName = this.fieldColor;
_.each(events, function (event) {
var value = event.record[fieldName];
event.color_index = _.isArray(value) ? value[0] : value;
});
this.model_color = this.fields[fieldName].relation || element.model;
}
return Promise.resolve();
},
/**
* Get the color index used to render the event and the filter.
* Since we have a maximum of 30 colors, we have to adapt the color
* index in case the id exceeds the number of colors available.
*
* - actual_color_list contains the list of color already used in the filter
* - color_index is the color we try to apply for the event
*
* @private
* @param {Array} actual_color_list
* @param {Integer} color_index
* @returns {Integer}
*/
_getColorIndex: function(actual_color_list, color_index) {
var actual_color_list = actual_color_list || [0];
var new_color_index = color_index % 50;
if (new_color_index !== color_index && actual_color_list.includes(new_color_index)) {
new_color_index = this._getColorIndex(actual_color_list, new_color_index + 3);
}
return new_color_index;
},
/**
* @private
* @param {any} filter
* @returns {Promise}
*/
_loadFilter: function (filter) {
if (!filter.write_model) {
return Promise.resolve();
}
var field = this.fields[filter.fieldName];
var fields = [filter.write_field];
if (filter.filter_field) {
fields.push(filter.filter_field);
}
return this._rpc({
model: filter.write_model,
method: 'search_read',
domain: [["user_id", "=", session.uid]],
fields: fields,
})
.then(function (res) {
var records = _.map(res, function (record) {
var _value = record[filter.write_field];
var value = _.isArray(_value) ? _value[0] : _value;
var f = _.find(filter.filters, function (f) {return f.value === value;});
var formater = fieldUtils.format[_.contains(['many2many', 'one2many'], field.type) ? 'many2one' : field.type];
// By default, only current user partner is checked.
return {
'id': record.id,
'value': value,
'label': formater(_value, field),
'active': (f && f.active) || (filter.filter_field && record[filter.filter_field]),
};
});
records.sort(function (f1,f2) {
return _.string.naturalCmp(f2.label, f1.label);
});
// add my profile
if (field.relation === 'res.partner' || field.relation === 'res.users') {
var value = field.relation === 'res.partner' ? session.partner_id : session.uid;
var me = _.find(records, function (record) {
return record.value === value;
});
if (me) {
records.splice(records.indexOf(me), 1);
} else {
var f = _.find(filter.filters, function (f) {return f.value === value;});
me = {
'value': value,
'label': session.name,
'active': !f || f.active,
};
}
records.unshift(me);
}
// add all selection
records.push({
'value': 'all',
'label': field.relation === 'res.partner' || field.relation === 'res.users' ? _t("Everybody's calendars") : _t("Everything"),
'active': filter.all,
});
filter.filters = records;
});
},
/**
* @private
* @param {any} element
* @param {any} events
* @returns {Promise}
*/
_loadRecordsToFilters: function (element, events) {
var self = this;
var new_filters = {};
var to_read = {};
var defs = [];
var color_filter = {};
_.each(this.data.filters, function (filter, fieldName) {
var field = self.fields[fieldName];
new_filters[fieldName] = filter;
if (filter.write_model) {
if (field.relation === self.model_color) {
_.each(filter.filters, function (f) {
f.color_index = self._getColorIndex(_.map(filter.filters, f => f.value), f.value);
});
}
return;
}
_.each(filter.filters, function (filter) {
filter.display = !filter.active;
});
var fs = [];
var undefined_fs = [];
_.each(events, function (event) {
var data = event.record[fieldName];
if (!_.contains(['many2many', 'one2many'], field.type)) {
data = [data];
} else {
to_read[field.relation] = (to_read[field.relation] || []).concat(data);
}
_.each(data, function (_value) {
var value = _.isArray(_value) ? _value[0] : _value;
var f = {
'color_index': self.model_color === (field.relation || element.model) ? value : false,
'value': value,
'label': fieldUtils.format[field.type](_value, field) || _t("Undefined"),
'avatar_model': field.relation || element.model,
};
// if field used as color does not have value then push filter in undefined_fs,
// such filters should come last in filter list with Undefined string, later merge it with fs
value ? fs.push(f) : undefined_fs.push(f);
});
});
_.each(_.union(fs, undefined_fs), function (f) {
var f1 = _.findWhere(filter.filters, _.omit(f, 'color_index'));
if (f1) {
f1.display = true;
} else {
f.display = f.active = true;
filter.filters.push(f);
}
});
if (filter.color_model && filter.field_color) {
var ids = filter.filters.reduce((acc, f) => {
if (!f.color_index && f.value) {
acc.push(f.value);
}
return acc;
}, []);
if (!color_filter[filter.color_model]) {
color_filter[filter.color_model] = {};
}
if (ids.length) {
defs.push(self._rpc({
model: filter.color_model,
method: 'search_read',
args: [[['id', 'in', _.uniq(ids)]], [filter.field_color]],
})
.then(function (res) {
_.each(res, function (c) {
color_filter[filter.color_model][c.id] = c[filter.field_color];
});
}));
}
}
});
_.each(to_read, function (ids, model) {
defs.push(self._rpc({
model: model,
method: 'name_get',
args: [_.uniq(ids)],
})
.then(function (res) {
to_read[model] = _.object(res);
}));
});
return Promise.all(defs).then(function () {
_.each(self.data.filters, function (filter) {
if (filter.write_model) {
return;
}
if (filter.filters.length && (filter.filters[0].avatar_model in to_read)) {
_.each(filter.filters, function (f) {
f.label = to_read[f.avatar_model][f.value];
});
}
if (filter.color_model && filter.field_color) {
_.each(filter.filters, function (f) {
if (!f.color_index) {
f.color_index = color_filter[filter.color_model] && color_filter[filter.color_model][f.value];
}
});
}
});
});
},
/**
* parse the server values to javascript framwork
*
* @private
* @param {Object} data the server data to parse
*/
_parseServerData: function (data) {
var self = this;
_.each(data, function(event) {
_.each(self.fieldNames, function (fieldName) {
event[fieldName] = self._parseServerValue(self.fields[fieldName], event[fieldName]);
});
});
},
/**
* Transform OpenERP event object to fullcalendar event object
*
* @private
* @param {Object} evt
*/
_recordToCalendarEvent: function (evt) {
var date_start;
var date_stop;
var date_delay = evt[this.mapping.date_delay] || 1.0,
all_day = this.fields[this.mapping.date_start].type === 'date' ||
this.mapping.all_day && evt[this.mapping.all_day] || false,
the_title = '',
attendees = [];
if (!all_day) {
date_start = evt[this.mapping.date_start].clone();
date_stop = this.mapping.date_stop ? evt[this.mapping.date_stop].clone() : null;
} else {
date_start = evt[this.mapping.date_start].clone().startOf('day');
date_stop = this.mapping.date_stop ? evt[this.mapping.date_stop].clone().startOf('day') : null;
}
if (!date_stop && date_delay) {
date_stop = date_start.clone().add(date_delay,'hours');
}
if (!all_day) {
date_start.add(this.getSession().getTZOffset(date_start), 'minutes');
date_stop.add(this.getSession().getTZOffset(date_stop), 'minutes');
}
if (this.mapping.all_day && evt[this.mapping.all_day]) {
date_stop.add(1, 'days');
}
var r = {
'record': evt,
'start': date_start.local(true).toDate(),
'end': date_stop.local(true).toDate(),
'r_start': date_start.clone().local(true).toDate(),
'r_end': date_stop.clone().local(true).toDate(),
'title': the_title,
'allDay': all_day,
'id': evt.id,
'attendees':attendees,
};
if (!(this.mapping.all_day && evt[this.mapping.all_day]) && this.data.scale === 'month' && this.fields[this.mapping.date_start].type !== 'date') {
r.showTime = true;
}
return r;
},
_getCalendarEventData: function (events) {
return _.map(events, this._recordToCalendarEvent.bind(this));
}
});
});
@@ -1,268 +0,0 @@
odoo.define('web.CalendarPopover', function (require) {
"use strict";
var fieldRegistry = require('web.field_registry');
const fieldRegistryOwl = require('web.field_registry_owl');
const FieldWrapper = require('web.FieldWrapper');
var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin');
var Widget = require('web.Widget');
const { WidgetAdapterMixin } = require('web.OwlCompatibility');
var CalendarPopover = Widget.extend(WidgetAdapterMixin, 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;
this._canDelete = eventInfo.canDelete;
this.popoverFields = eventInfo.popoverFields;
},
/**
* @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);
},
/**
* @override
*/
destroy: function () {
this._super.apply(this, arguments);
WidgetAdapterMixin.destroy.call(this);
},
/**
* Called each time the widget is attached into the DOM.
*/
on_attach_callback: function () {
WidgetAdapterMixin.on_attach_callback.call(this);
},
/**
* Called each time the widget is detached from the DOM.
*/
on_detach_callback: function () {
WidgetAdapterMixin.on_detach_callback.call(this);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @return {boolean}
*/
isEventDeletable() {
return this._canDelete;;
},
/**
* @return {boolean}
*/
isEventDetailsVisible() {
return true;
},
/**
* @return {boolean}
*/
isEventEditable() {
return true;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Returns the AbstractField specialization that should be used for the
* given field informations. If there is no mentioned specific widget to
* use, determines one according the field type.
*
* @private
* @param {Object} field
* @param {Object} attrs
* @returns {function|null} AbstractField specialization Class
*/
_getFieldWidgetClass(field, attrs) {
let FieldWidget;
if (attrs.widget) {
FieldWidget = fieldRegistry.getAny(['form' + "." + attrs.widget, attrs.widget]);
if (!FieldWidget) {
console.warn("Missing widget: ", attrs.widget, " for field", attrs.name, "of type", field.type);
}
}
return FieldWidget || fieldRegistry.getAny(['form' + "." + field.type, field.type, "abstract"]);
},
/**
* Generate fields to render into popover
*
* @private
* @returns {Promise}
*/
_processFields: function () {
var self = this;
var fieldsToGenerate = [];
const fieldInformation = {};
var fields = _.keys(this.popoverFields);
for (var i=0; i<fields.length; i++) {
var fieldName = fields[i];
var displayFieldInfo = self.displayFields[fieldName] || {attrs: {invisible: 1}};
var fieldInfo = self.fields[fieldName];
fieldInformation[fieldName] = {
Widget: self._getFieldWidgetClass(fieldInfo, displayFieldInfo.attrs),
};
var field = {
name: fieldName,
string: displayFieldInfo.attrs.string || fieldInfo.string,
value: self.event.extendedProps.record[fieldName],
type: fieldInfo.type,
invisible: displayFieldInfo.attrs.invisible,
};
if (field.type === 'selection') {
field.selection = fieldInfo.selection;
}
if (field.type === 'monetary') {
var currencyField = field.currency_field || 'currency_id';
if (!fields.includes(currencyField) && _.has(self.event.extendedProps.record, currencyField)) {
fields.push(currencyField);
}
}
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, fieldInformation).then(async function (recordID) {
var defs = [];
const recordDataPoint = self.model.localData[recordID];
recordDataPoint.res_id = self.event.extendedProps.record.id;
await self.model._fetchSpecialData(recordDataPoint);
var record = self.model.get(recordID);
_.each(fieldsToGenerate, function (field) {
if (field.invisible) return;
let isLegacy = true;
let fieldWidget;
let FieldClass = fieldRegistryOwl.getAny([field.widget, field.type]);
if (FieldClass) {
isLegacy = false;
fieldWidget = new FieldWrapper(this, FieldClass, {
fieldName: field.name,
record,
options: self.displayFields[field.name],
});
} else {
FieldClass = fieldRegistry.getAny([field.widget, field.type]);
fieldWidget = new FieldClass(self, field.name, record, self.displayFields[field.name]);
}
if (fieldWidget.attrs && !_.isObject(fieldWidget.attrs.modifiers)) {
fieldWidget.attrs.modifiers = fieldWidget.attrs.modifiers ? JSON.parse(fieldWidget.attrs.modifiers) : {};
}
self._registerWidget(recordID, field.name, fieldWidget);
// Only display the fields whose attributes does not make them invisible
let fieldClass = "list-group-item flex-shrink-0 d-flex flex-wrap align-items-center";
if (fieldWidget.attrs && fieldWidget.attrs.modifiers) {
const fieldModifier = record.evalModifiers(_.pick(fieldWidget.attrs.modifiers, 'invisible'));
fieldClass += fieldModifier.invisible ? ' o_invisible_modifier' : '';
}
if (fieldWidget.attrs) {
if (fieldWidget.attrs.options && fieldWidget.attrs.options.icon) {
field.icon = fieldWidget.attrs.options.icon;
}
if (fieldWidget.attrs.class) {
fieldClass += ' ' + fieldWidget.attrs.class;
}
}
var $field = $('<li>', {class: fieldClass});
let $fieldLabel;
if (field.icon) {
$fieldLabel = $('<strong>', {class: 'me-2', html: _.str.sprintf("<b><i class='%s'/></b>", field.icon)});
} else {
$fieldLabel = $('<strong>', {class: 'me-2', text: _.str.sprintf('%s : ', field.string)});
}
$fieldLabel.appendTo($field);
var $fieldContainer = $('<div>', {class: 'flex-grow-1'});
$fieldContainer.appendTo($field);
let def;
if (isLegacy) {
def = fieldWidget.appendTo($fieldContainer);
} else {
def = fieldWidget.mount($fieldContainer[0]);
}
self.$fieldsList.push($field);
defs.push(def);
});
return Promise.all(defs);
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {jQueryEvent} ev
*/
_onClickPopoverEdit: function (ev) {
ev.preventDefault();
this.trigger_up('edit_event', {
id: this.event.id,
title: this.event.extendedProps.record.display_name,
});
},
/**
* @private
* @param {jQueryEvent} ev
*/
_onClickPopoverDelete: function (ev) {
ev.preventDefault();
this.trigger_up('delete_event', {id: this.event.id});
},
});
return CalendarPopover;
});
@@ -1,129 +0,0 @@
odoo.define('web.CalendarQuickCreate', function (require) {
"use strict";
var core = require('web.core');
var Dialog = require('web.Dialog');
var _t = core._t;
var QWeb = core.qweb;
/**
* Quick creation view.
*
* Triggers a single event "added" with a single parameter "name", which is the
* name entered by the user
*
* @class
* @type {*}
*/
var QuickCreate = Dialog.extend({
events: _.extend({}, Dialog.events, {
'keyup input': '_onkeyup',
}),
/**
* @constructor
* @param {Widget} parent
* @param {Object} buttons
* @param {Object} options
* @param {Object} dataTemplate
* @param {Object} dataCalendar
*/
init: function (parent, buttons, options, dataTemplate, dataCalendar) {
this._buttons = buttons || false;
this.options = options;
this.parentCalendar = parent;
// Can hold data pre-set from where you clicked on agenda
this.dataTemplate = dataTemplate || {};
this.dataCalendar = dataCalendar;
var self = this;
this._super(parent, {
title: options.title,
// Close the dialog when clicking outside of it
backdrop: true,
size: 'small',
buttons: this._buttons ? [
{text: _t("Create"), classes: 'btn-primary', click: function () {
if (!self._quickAdd(dataCalendar)) {
self.focus();
}
}},
{text: _t("Edit"), click: function () {
dataCalendar.disableQuickCreate = true;
dataCalendar.title = self.$('input').val().trim();
dataCalendar.on_save = self.destroy.bind(self);
self.trigger_up('openCreate', dataCalendar);
}},
{text: _t("Cancel"), close: true},
] : [],
$content: QWeb.render('CalendarView.quick_create', {widget: this})
});
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
focus: function () {
this.$('input').focus();
},
destroy: function () {
// On quick create, remaining dialogs may be closed. We avoid to lose the selection for new quickcreate
// by checking that the dialog is not already destroyed.
if (!this.isDestroyed()) {
// remove selection when the dialog is closed
if (this.parentCalendar.renderer.state.scale === 'year') {
this.parentCalendar.renderer.calendar.view.unselect();
} else {
this.parentCalendar.renderer.calendar.unselect();
}
}
this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Gathers data from the quick create dialog a launch quick_create(data) method
*/
_quickAdd: function (dataCalendar) {
dataCalendar = $.extend({}, this.dataTemplate, dataCalendar);
var val = this.$('input').val().trim();
if (!val) {
this.$('label, input').addClass('o_field_invalid');
this.displayNotification({ title: _t("Invalid fields:"), message: _t("Meeting Subject"), type: 'danger' });
}
dataCalendar.title = val;
return (val)? this.trigger_up('quickCreate', {data: dataCalendar, options: this.options}) : false;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {keyEvent} event
*/
_onkeyup: function (event) {
if (this._flagEnter) {
return;
}
if(event.keyCode === $.ui.keyCode.ENTER) {
this._flagEnter = true;
if (!this._quickAdd(this.dataCalendar)){
this._flagEnter = false;
}
} else if (event.keyCode === $.ui.keyCode.ESCAPE && this._buttons) {
this.close();
}
},
});
return QuickCreate;
});
File diff suppressed because it is too large Load Diff
@@ -1,216 +0,0 @@
odoo.define('web.CalendarView', function (require) {
"use strict";
var AbstractView = require('web.AbstractView');
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;
// gather the fields to get
var fieldsToGather = [
"date_start",
"date_delay",
"date_stop",
"all_day",
"recurrence_update",
"create_name_field",
];
const scalesInfo = {
day: 'timeGridDay',
week: 'timeGridWeek',
month: 'dayGridMonth',
year: 'dayGridYear',
};
var CalendarView = AbstractView.extend({
display_name: _lt('Calendar'),
icon: 'fa fa-calendar',
jsLibs: [
'/web/static/lib/fullcalendar/core/main.js',
'/web/static/lib/fullcalendar/interaction/main.js',
'/web/static/lib/fullcalendar/moment/main.js',
'/web/static/lib/fullcalendar/daygrid/main.js',
'/web/static/lib/fullcalendar/timegrid/main.js',
'/web/static/lib/fullcalendar/list/main.js'
],
cssLibs: [
'/web/static/lib/fullcalendar/core/main.css',
'/web/static/lib/fullcalendar/daygrid/main.css',
'/web/static/lib/fullcalendar/timegrid/main.css',
'/web/static/lib/fullcalendar/list/main.css'
],
config: _.extend({}, AbstractView.prototype.config, {
Model: CalendarModel,
Controller: CalendarController,
Renderer: CalendarRenderer,
}),
viewType: 'calendar',
searchMenuTypes: ['filter', 'favorite'],
/**
* @override
*/
init: function (viewInfo, params) {
this._super.apply(this, arguments);
var arch = this.arch;
var fields = this.fields;
var attrs = arch.attrs;
if (!attrs.date_start) {
throw new Error(_lt("Calendar view has not defined 'date_start' attribute."));
}
var mapping = {};
var fieldNames = fields.display_name ? ['display_name'] : [];
var displayFields = {};
let popoverFields = {};
_.each(fieldsToGather, function (field) {
if (arch.attrs[field]) {
var fieldName = attrs[field];
mapping[field] = fieldName;
fieldNames.push(fieldName);
}
});
var filters = {};
var eventLimit = attrs.event_limit !== null && (isNaN(+attrs.event_limit) ? _.str.toBool(attrs.event_limit) : +attrs.event_limit);
var modelFilters = [];
_.each(arch.children, function (child) {
if (child.tag !== 'field') return;
var fieldName = child.attrs.name;
fieldNames.push(fieldName);
popoverFields[fieldName] = {attrs: child.attrs};
if (!child.attrs.invisible || child.attrs.filters) {
child.attrs.options = child.attrs.options ? pyUtils.py_eval(child.attrs.options) : {};
if (!child.attrs.invisible) {
displayFields[fieldName] = {attrs: child.attrs};
}
if (params.sidebar === false) return; // if we have not sidebar, (eg: Dashboard), we don't use the filter "coworkers"
if (child.attrs.avatar_field) {
filters[fieldName] = filters[fieldName] || {
'title': fields[fieldName].string,
'fieldName': fieldName,
'filters': [],
'check_all': {},
};
filters[fieldName].avatar_field = child.attrs.avatar_field;
filters[fieldName].avatar_model = fields[fieldName].relation;
}
if (child.attrs.write_model) {
filters[fieldName] = filters[fieldName] || {
'title': fields[fieldName].string,
'fieldName': fieldName,
'filters': [],
'check_all': {},
};
filters[fieldName].write_model = child.attrs.write_model;
filters[fieldName].write_field = child.attrs.write_field; // can't use a x2many fields
filters[fieldName].filter_field = child.attrs.filter_field;
modelFilters.push(fields[fieldName].relation);
}
if (child.attrs.filters) {
filters[fieldName] = filters[fieldName] || {
'title': fields[fieldName].string,
'fieldName': fieldName,
'check_all': {},
'filters': [],
};
if (child.attrs.color) {
filters[fieldName].field_color = child.attrs.color;
filters[fieldName].color_model = fields[fieldName].relation;
}
if (!child.attrs.avatar_field && fields[fieldName].relation) {
if (fields[fieldName].relation.includes(['res.users', 'res.partner', 'hr.employee'])) {
filters[fieldName].avatar_field = 'avatar_128';
}
filters[fieldName].avatar_model = fields[fieldName].relation;
}
}
}
});
if (attrs.color) {
var fieldName = attrs.color;
fieldNames.push(fieldName);
}
//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'
this.controllerParams.quickAddPop = (!('quick_add' in attrs) || utils.toBoolElse(attrs.quick_add+'', true));
this.controllerParams.disableQuickCreate = params.disable_quick_create || !this.controllerParams.quickAddPop;
// If form_view_id is set, then the calendar view will open a form view
// with this id, when it needs to edit or create an event.
this.controllerParams.formViewId =
attrs.form_view_id ? parseInt(attrs.form_view_id, 10) : false;
if (!this.controllerParams.formViewId && params.action) {
var formViewDescr = _.find(params.action.views, function (v) {
return v.type === 'form';
});
if (formViewDescr) {
this.controllerParams.formViewId = formViewDescr.viewID;
}
}
let scales;
const allowedScales = Object.keys(scalesInfo);
if (arch.attrs.scales) {
scales = arch.attrs.scales.split(',')
.filter(x => allowedScales.includes(x));
} else {
scales = allowedScales;
}
this.controllerParams.eventOpenPopup = utils.toBoolElse(attrs.event_open_popup || '', false);
this.controllerParams.showUnusualDays = utils.toBoolElse(attrs.show_unusual_days || '', false);
this.controllerParams.mapping = mapping;
this.controllerParams.context = params.context || {};
this.controllerParams.displayName = params.displayName;
this.controllerParams.scales = scales;
this.rendererParams.displayFields = displayFields;
this.rendererParams.popoverFields = popoverFields;
this.rendererParams.model = viewInfo.model;
this.rendererParams.hideDate = utils.toBoolElse(attrs.hide_date || '', false);
this.rendererParams.hideTime = utils.toBoolElse(attrs.hide_time || '', false);
this.rendererParams.canDelete = this.controllerParams.activeActions.delete;
this.rendererParams.canCreate = this.controllerParams.activeActions.create;
this.rendererParams.scalesInfo = scalesInfo;
this.loadParams.fieldNames = _.uniq(fieldNames);
this.loadParams.mapping = mapping;
this.loadParams.fields = fields;
this.loadParams.fieldsInfo = viewInfo.fieldsInfo;
this.loadParams.editable = !fields[mapping.date_start].readonly;
this.loadParams.creatable = this.controllerParams.activeActions.create;
this.loadParams.eventLimit = eventLimit;
this.loadParams.fieldColor = attrs.color;
this.loadParams.filters = filters;
this.loadParams.mode = (params.context && params.context.default_mode) || attrs.mode;
this.loadParams.scales = scales;
this.loadParams.initialDate = moment(
(params.context && params.context.initial_date) ||
params.initialDate ||
new Date()
);
this.loadParams.scalesInfo = scalesInfo;
},
});
return CalendarView;
});
@@ -28,7 +28,6 @@ odoo.define('web._view_registry', function (require) {
var FormView = require('web.FormView');
var KanbanView = require('web.KanbanView');
var ListView = require('web.ListView');
var CalendarView = require('web.CalendarView');
var view_registry = require('web.view_registry');
view_registry
@@ -36,6 +35,5 @@ view_registry
.add('legacy_form', FormView)
.add('list', ListView)
.add('legacy_list', ListView)
.add('kanban', KanbanView)
.add('calendar', CalendarView);
.add('kanban', KanbanView);
});
+1 -2
View File
@@ -5,8 +5,7 @@
.o_debug .o_legacy_form_view,
.o_debug .o_legacy_kanban_view,
.o_debug .o_legacy_list_view,
.o_debug .o_legacy_calendar_view {
.o_debug .o_legacy_list_view {
border-top: 3px solid red;
}
@@ -1,656 +0,0 @@
.o_legacy_calendar_view {
flex: 1 1 auto;
min-width: 0;
height: 100%;
background-color: map-get($grays, '100');
background: linear-gradient(-45deg, map-get($grays, '100'), white);
.fc-event {
margin: 0 1px;
border-style: solid;
border-width: 0 0 0 3px;
box-sizing: border-box;
overflow: hidden;
background: none;
font-size: 11px;
line-height: 1;
&:not([href]):not([tabindex]) {
color: $body-color;
}
&.fc-dragging.fc-day-grid-event.dayGridMonth .fc-content {
@include text-truncate();
margin: 4px 4px 3px;
}
.fc-bg {
background-color: mix(map-get($theme-colors, 'primary'), white); // Used for placeholder events only (on creation)
width: 101%;
height: 101%; // Compensate border
opacity: 0.7;
transition: opacity 0.2s;
}
.fc-content {
white-space: normal;
margin: 0.25rem;
font-size: 1.1em;
font-weight: 500;
}
// Try to show one full lien for short event
&.fc-short .fc-content {
margin-top: 1px;
}
&.o_cw_custom_highlight {
z-index: 10!important;
.fc-bg{
opacity: 0.95;
}
}
}
.o_calendar_widget {
height: 100%;
> .fc-view-container {
height: 100%;
}
// === Adapt calendar table borders ===
// =====================================
td {
border-color: $border-color;
}
.fc-time-grid .fc-slats .fc-minor td {
border-top-color: map-get($grays, '400');
}
.fc-widget-content {
border-left-color: transparent;
}
.fc-widget-header {
border-color: transparent;
border-bottom-color: $border-color;
padding: 3px 0 5px;
}
hr.fc-widget-header {
padding: 1px;
border: 0;
background: map-get($grays, '400');
}
.fc-timeGrid-view .fc-day-grid .fc-row .fc-content-skeleton {
padding: .5em;
}
.fc-event-container {
color: white;
}
.fc-more-popover {
.fc-header {
padding-left: 1rem;
.fc-title {
font-weight: bold;
}
}
.fc-body {
max-height: 500px;
overflow: auto;
}
}
.o_calendar_disabled {
background-color: $gray-200;
}
// ====== Specific agenda types ======
// ====================================
// ====== Both Day and Week agenda
.fc-timeGridDay-view, .fc-timeGridWeek-view {
.fc-axis {
padding-left: $o-horizontal-padding;
}
.fc-widget-header.fc-today{
border-radius: 25px;
background: $o-brand-odoo;
color: white;
}
// Reinfornce default border color
tbody td {
border-top-color: map-get($grays, '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);
border-left-color: rgba($o-brand-odoo, 0.3);
border-right-color: rgba($o-brand-odoo, 0.3);
+ hr.fc-widget-header {
padding: 1px 0 0;
background: map-get($grays, '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 map-get($grays, '400');
}
.fc-day.fc-widget-content.fc-today:not(.o_calendar_disabled) {
border-left-color: rgba($o-brand-odoo, 0.3);
border-right-color: rgba($o-brand-odoo, 0.3);
background: $o-view-background-color;
}
.fc-event {
// Prevent events with similar color to visually overlap each other
box-shadow: 0 0 0 1px white;
&.fc-event:not(.fc-h-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;
}
}
// ====== Day only
.fc-timeGridDay-view .fc-event {
padding: 10px;
font-size: 14px;
// Try to avoid showing no title for short event
&.fc-short {
padding-top: 0;
padding-bottom: 0;
}
.fc-content {
margin-top: 1px!important;
}
}
// ====== Week only
.fc-timeGridWeek-view {
.fc-now-indicator {
left: $o-horizontal-padding;
}
// Expand tiny events on hover/select
.fc-event:not(.fc-h-event).o_cw_custom_highlight, .fc-event:not(.fc-h-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-dayGridMonth-view {
padding-left: $o-horizontal-padding;
.fc-event {
border-radius: 25px;
}
.fc-widget-header {
padding: 3px 0;
}
.fc-week-number {
background: none;
font-size: 1.2rem;
padding: 0.1rem 0.3rem 0.1rem 0 !important;
line-height: 1;
}
.fc-day-top {
text-align: center;
padding-top: 3px;
padding-bottom: 3px;
.fc-day-number {
float: none !important;
}
}
.fc-day-number {
margin: 5px;
padding: 0.1rem 0.3rem 0.1rem 0;
font-size: 1.2rem;
color: map-get($grays, '900');
font-weight: 400;
line-height: 1;
float: none !important;
}
.fc-day-top.fc-other-month {
opacity: 0.8;
.fc-day-number {
color: map-get($grays, '500');
}
}
td:last-child {
border-right-color: transparent;
}
.fc-bg .fc-today {
border-color: map-get($grays, '300');
}
.fc-bg .fc-today:not(.o_calendar_disabled) {
background: $o-view-background-color;
}
.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;
.fc-content {
display: flex;
justify-content: start;
flex-direction: row;
margin: 4px 4px 3px;
}
.fc-time:not(:empty) {
/*rtl:ignore*/
padding-right: 0.5em;
}
.o_event_title {
@include text-truncate();
}
&.o_cw_nobg {
&.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;
}
}
}
}
// ====== Year only
.fc-dayGridYear-view {
border: none;
height: 100%;
padding-left: $o-horizontal-padding;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
overflow-y: auto;
@include media-breakpoint-down(md) {
padding-right: 5px;
padding-left: 5px;
}
@for $i from 1 through length($o-colors-complete) {
$color: nth($o-colors-complete, $i);
.fc-bgevent.o_calendar_striked {
background: linear-gradient(
transparent 0 40%,
$color 40% 60%,
transparent 60% 100%)
}
.o_cw_popover_link.o_calendar_color_#{$i - 1} {
border: solid 2px;
padding-left: 5px;
border-color: $color;
margin-top: 2px;
background: $color;
color: black;
&.o_attendee_status_tentative {
color: color-contrast($color);
background: repeating-linear-gradient(
45deg,
$color,
$color 10px,
rgba($color, 0.7) 10px,
rgba($color, 0.7) 20px) !important;
}
&.o_attendee_status_needsAction {
background-color: rgba(white, 0.9)!important;
}
&.o_attendee_status_declined {
text-decoration: line-through;
background-color: rgba(white, 0.9)!important;
}
}
}
> table {
height: 100%;
}
table, tr, th, td {
border: none;
}
&.fc-readonly-year-view {
.fc-day-top:not(.fc-has-event) {
cursor: default !important;
}
}
&:not(.fc-readonly-year-view) {
.fc-day-top:hover > .fc-day-number {
font-weight: bold;
border-radius: 100%;
text-align: center;
background-color: rgba(#87c0d1, 0.5);
color: map-get($grays, '900');
}
}
> .fc-month-container {
width: 25%;
min-width: 25rem;
box-sizing: border-box;
@include media-breakpoint-down(md) {
width: 50%;
min-width: 12rem;
}
> .fc-month {
width: 21rem;
margin: auto;
@include media-breakpoint-down(md) {
width: 95%;
}
> .fc-toolbar.fc-header-toolbar {
padding-top: 10px;
margin-bottom: 4px;
cursor: default;
h2 {
font-size: 1.2rem;
color: map-get($grays, '600');
}
}
.fc-widget-header {
padding: 2px 0;
cursor: default;
}
.fc-dayGridMonth-view {
padding-left: unset;
.fc-has-event {
background-color: #B4DFF5;
}
}
.fc-week.fc-row {
min-height: 2rem;
}
.fc-disabled-day {
background-color: unset;
}
.fc-day-top {
text-align: center;
padding: 0.4vh;
cursor: pointer;
> .fc-day-number {
display: block;
float: unset;
line-height: unset;
margin: auto;
padding: 0.1rem 0;
font-size: calc(11px + 0.2vh);
}
&.fc-today > .fc-day-number {
font-weight: bold;
}
}
}
}
}
// ====== RTL layout(s)
&.fc-rtl {
.fc-timeGrid-view .fc-event {
border-width: 0 3px 0 0;
}
.fc-dayGridMonth-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;
}
}
}
}
}
}
// =============== Generate color classes ===============
@for $i from 1 through length($o-filter_colors) {
$color: nth($o-filter_colors, $i);
.o_legacy_calendar_view .fc-view {
.o_calendar_hatched_#{$i - 1} {
background: repeating-linear-gradient(
45deg,
$color,
$color 10px,
rgba($color, 0.4) 10px,
rgba($color, 0.4) 20px
) !important;
.fc-bg {
background: repeating-linear-gradient(
45deg,
$color,
$color 10px,
rgba($color, 0.5) 10px,
rgba($color, 0.5) 20px
);
}
}
.o_calendar_striked_#{$i - 1} {
background: linear-gradient(
transparent 0 45%,
$color 45% 55%,
transparent 55% 100%);
.fc-bg {
background: none;
}
}
.fc-bgevent.o_calendar_color_#{$i - 1} {
border-color: $color;
background-color: $color;
opacity: 0.5;
}
.fc-event.o_calendar_striked_#{$i - 1} {
border: none;
}
.fc-event.o_calendar_color_#{$i - 1} {
border-color: $color;
color: darken($color, 45%);
opacity: 1;
&.o_event_highlight {
opacity: 1;
.fc-content {
font-weight: bold;
}
}
&:not(.o_calendar_hatched):not(.o_calendar_striked){
background: scale-color($color, $lightness: 50%);
.fc-bg {
background: scale-color($color, $lightness: 50%);
}
}
&.o_cw_custom_hover, &.o_cw_custom_highlight {
box-shadow: 0 12px 12px -5px rgba($color, 0.3);
color: color-contrast($color);
z-index: 10!important;
opacity: 1;
right: 1px!important;
left: 1px!important;
margin-right: 0!important;
}
&.o_attendee_status_needsAction, &.o_attendee_status_tentative, &.o_attendee_status_declined, &.o_attendee_status_alone {
border-width: 2px 2px 2px !important;
&.o_cw_custom_highlight {
background-color: $color;
}
}
&.o_attendee_status_tentative {
color: color-contrast($color);
.fc-bg {
background: repeating-linear-gradient(
45deg,
$color,
$color 10px,
rgba(white, 0.4) 10px,
rgba(white, 0.4) 20px
) !important;
}
}
&.o_attendee_status_alone, &.o_attendee_status_needsAction {
background-color: rgba(white, 0.9)!important;
color: black;
.fc-bg {
background-color: rgba(white, 0.9)!important;
}
}
&.o_attendee_status_declined {
text-decoration: line-through;
background-color: rgba(white, 0.9)!important;
color: black;
.fc-bg {
background-color: rgba(white, 0.9)!important;
}
}
}
}
}
@@ -1,181 +0,0 @@
<template>
<div t-name="CalendarView" class="o_calendar_container">
<div class="o_legacy_calendar_view">
<div class="o_calendar_buttons" role="toolbar" aria-label="Calendar toolbar"/>
<div class="o_calendar_widget" />
</div>
<div class="o_calendar_sidebar_container d-none d-md-block">
<div class="o_calendar_sidebar">
<div class="o_calendar_mini"/>
</div>
</div>
</div>
<t t-name="calendar-box">
<div t-att-style="typeof color === 'string' ? ('background-color:' + color) + ';' : ''" t-attf-class="o_event #{record.is_highlighted ? 'o_event_highlight' : ''} #{typeof color === 'number' ? _.str.sprintf('o_calendar_color_%s', color) : 'o_calendar_color_1'} #{record.is_hatched ? 'o_calendar_hatched o_calendar_hatched_' + (typeof color === 'number' ? color : 1): ''} #{record.is_striked ? 'o_calendar_striked o_calendar_striked_' + (typeof color === 'number' ? color : 1): ''}">
<span t-if="showTime" class="fc-time"/>
<div class="o_event_title" t-esc="record.display_name"/>
<div t-if="showLocation and record.location" t-esc="record.location"/>
</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="popover-arrow"/>
<div class="card-header d-flex justify-content-between py-2 pe-2">
<h4 class="p-0 pt-1">
<span class="popover-header border-0"/><span t-if="displayLock" class="ms-1"><i class="fa fa-lock"/></span>
</h4>
<span class="o_cw_popover_close ms-4 mt-2 me-2"><i class="fa fa-close"/></span>
</div>
<div class="o_cw_body">
</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 t-if="widget.isEventDetailsVisible()" class="list-group list-group-flush o_cw_popover_fields_secondary"/>
<div t-attf-class="card-footer border-top #{ !widget.isEventEditable() &amp;&amp;!widget.isEventDeletable() ? 'o_footer_shrink' : '' }">
<a t-if="widget.isEventEditable()" href="#" class="btn btn-primary o_cw_popover_edit">Edit</a>
<a t-if="widget.isEventDeletable()" href="#" class="btn btn-secondary o_cw_popover_delete ms-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>
<t t-set="titleWithoutSpace" t-value="widget.title.split(' ').join('')"></t>
<div class="o_calendar_filter">
<div class="d-flex">
<div class="o_calendar_filter_items_checkall me-2" data-value="check_all">
<input t-att-id="widget.title + '_id'" type="checkbox" name="selection" class="position-absolute"
t-att-checked="widget.filter_check_all[widget.fieldName] ? true : undefined"
/>
<label t-att-for="widget.title + '_id'" class="align-items-center m-0">
<span class="o_cw_filter_input_bg o_calendar_filter_all">
<i class="fa fa-check position-relative"/>
</span>
</label>
</div>
<div t-if="needCollapse" type="button" data-bs-toggle="collapse"
t-attf-data-bs-target="#o_cw_filter_collapse_#{titleWithoutSpace}"
class="justify-content-between align-items-center h5">
<span t-esc="widget.title"/>
<i class="o_cw_filter_collapse_icon fa fa-chevron-down"/>
</div>
<h5 t-elif="widget.title" t-esc="widget.title"/>
</div>
<div t-attf-class="o_calendar_filter_items #{needCollapse ? 'collapse show' : 0}" aria-expanded="true" t-attf-id="o_cw_filter_collapse_#{titleWithoutSpace}">
<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-start m-0">
<span t-attf-class="o_cw_filter_input_bg align-items-start d-flex flex-shrink-0 justify-content-center position-relative me-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 me-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 me-1" alt="Avatar"/>
<span class="o_cw_filter_title text-truncate flex-grow" t-esc="filter.label" t-attf-title="#{ ['all', false].includes(filter.value) or !widget.avatar_field ? 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>
</div>
</div>
</t>
<t t-name="CalendarView.buttons">
<div class="o_calendar_buttons">
<span class="o_calendar_navigation_buttons">
<t t-call="CalendarView.navigation_buttons" />
</span>
<t t-if="!scaleDrop">
<span class="o_calendar_scale_buttons">
<t t-call="CalendarView.scale_buttons" />
</span>
</t>
</div>
</t>
<t t-name="CalendarView.navigation_buttons">
<button class="o_calendar_button_prev btn btn-primary" aria-label="Previous" title="Previous"><span class="fa fa-arrow-left"/></button>
<button class="o_calendar_button_today btn btn-primary" data-hotkey="t">Today</button>
<button class="o_calendar_button_next btn btn-primary" aria-label="Next" title="Next"><span class="fa fa-arrow-right"/></button>
</t>
<t t-name="CalendarView.scale_buttons">
<div class="btn-group dropdown">
<button class="btn btn-secondary dropdown-toggle scale_button_selection text-uppercase" data-hotkey="v" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<button type="button" t-if="scales.includes('day')" class="dropdown-item o_calendar_button_day btn btn-secondary text-uppercase" data-hotkey="d">Day</button>
<button type="button" t-if="scales.includes('week')" class="dropdown-item o_calendar_button_week btn btn-secondary text-uppercase" data-hotkey="w">Week</button>
<button type="button" t-if="scales.includes('month')" class="dropdown-item o_calendar_button_month btn btn-secondary text-uppercase" data-hotkey="m">Month</button>
<button type="button" t-if="scales.includes('year')" class="dropdown-item o_calendar_button_year btn btn-secondary text-uppercase" data-hotkey="y">Year</button>
</div>
</div>
</t>
<main t-name="CalendarView.quick_create" class="o_calendar_quick_create">
<div class="mb-3">
<label for='name' class='col-form-label o_form_label'>Meeting Subject:</label>
<input type='text' name='name' class="o_input o_required_modifier" t-att-value="widget.dataTemplate.name or None"/>
</div>
</main>
<t t-name="CalendarView.yearEvent.popover">
<div class="o_cw_body">
<t t-foreach="groupKeys" t-as="groupKey">
<div class="fw-bold mt-2" t-esc="groupKey"/>
<t t-foreach="groupedEvents[groupKey]" t-as="event">
<t t-set="record" t-value="event.extendedProps.record"/>
<t t-set="color" t-value="event.extendedProps.color_index"/>
<a href="#"
t-attf-class="o_cw_popover_link o_calendar_color_#{typeof color === 'number' ? _.str.sprintf('%s', color) : '1'} o_attendee_status_#{record.is_alone ? 'alone' : record.attendee_status} btn-link d-block"
t-att-data-id="record.id"
t-att-data-title="record.display_name">
<t t-esc="record.start_hour"/> <t t-esc="record.display_name"/>
</a>
</t>
</t>
<div class="mt-2">
<a t-if="canCreate" href="#" class="btn-link o_cw_popover_create">
<i class="fa fa-plus"/> Create
</a>
</div>
</div>
</t>
<t t-name="CalendarView.yearEvent.popover.placeholder">
<div class="popover o_cw_popover" style="position: relative">
<div class="popover-arrow"></div>
<div class="popover-header"></div>
<div style="position: absolute; top: 0; right: 0.5rem;">
<span class="o_cw_popover_close"><i class="fa fa-close small"></i></span>
</div>
<div class="popover-body"></div>
</div>
</t>
</template>
@@ -2,13 +2,9 @@ odoo.define('web.owl_dialog_tests', function (require) {
"use strict";
const LegacyDialog = require('web.Dialog');
const FormView = require('web.FormView');
const CalendarView = require('web.CalendarView');
const makeTestEnvironment = require('web.test_env');
const Dialog = require('web.OwlDialog');
const testUtils = require('web.test_utils');
const { registry } = require("@web/core/registry");
const legacyViewRegistry = require('web.view_registry');
const { makeLegacyDialogMappingTestEnv } = require('@web/../tests/helpers/legacy_env_utils');
const { Dialog: WowlDialog } = require("@web/core/dialog/dialog");
@@ -19,7 +15,6 @@ odoo.define('web.owl_dialog_tests', function (require) {
mount,
destroy,
} = require("@web/../tests/helpers/utils");
const { createWebClient, doAction } = require("@web/../tests/webclient/helpers");
const { LegacyComponent } = require("@web/legacy/legacy_component");
const { Component, useState, xml } = owl;
@@ -417,115 +412,5 @@ odoo.define('web.owl_dialog_tests', function (require) {
destroy(parent1);
destroy(parent2);
});
QUnit.test("remove tabindex on inactive dialog", async (assert) => {
registry.category("views").remove("calendar"); // remove new calendar from registry
registry.category("views").remove("form"); // remove new form from registry
legacyViewRegistry.add("form", FormView); // add legacy form -> will be wrapped and added to new registry
legacyViewRegistry.add("calendar", CalendarView); // add legacy calendar -> will be wrapped and added to new registry
const serverData = {
actions: {
1: {
id: 1,
flags: { initialDate: new Date(2020, 6, 13) },
name: "Test",
res_model: "event",
type: "ir.actions.act_window",
views: [[false, "calendar"]],
},
2: {
id: 2,
name: "Test",
res_model: "event",
target: "new",
type: "ir.actions.act_window",
views: [[2, "form"]],
},
},
models: {
event: {
fields: {
id: { type: "integer" },
display_name: { type: "char" },
start: { type: "date" },
},
records: [{ id: 1, display_name: "Event 1", start: "2020-07-13" }],
methods: {
async check_access_rights() {
return true;
},
async get_formview_id() {
return false;
},
},
},
},
views: {
"event,false,calendar": `<calendar date_start="start" event_open_popup="true" />`,
"event,false,search": `<search />`,
"event,false,form": `
<form><sheet>
<field name="display_name" />
<button type="action" name="2">Click me</button>
</sheet></form>
`,
"event,2,form": `<form><sheet><field name="display_name" /></sheet></form>`,
},
activateMockServer: true,
};
const target = getFixture();
const webClient = await createWebClient({ serverData });
await doAction(webClient, 1);
await testUtils.dom.click(target.querySelector(`.fc-event[data-event-id="1"]`));
await testUtils.dom.click(target.querySelector(`.o_cw_popover_edit`));
assert.containsNone(target, ".o_dialog");
assert.containsOnce(target, ".modal");
assert.containsOnce(target, ".modal[tabindex='-1']");
assert.strictEqual(
target.querySelector(`.o_field_widget[name="display_name"]`).value,
"Event 1"
);
await testUtils.fields.editInput(
target.querySelector(`.o_field_widget[name="display_name"]`),
"legacy"
);
assert.strictEqual(
target.querySelector(`.o_field_widget[name="display_name"]`).value,
"legacy"
);
await testUtils.dom.click(target.querySelector(`button[name="2"]`));
assert.containsOnce(target, ".o_dialog");
assert.containsN(target, ".modal", 2);
assert.containsOnce(target, ".modal:not([tabindex='-1'])");
assert.containsOnce(target, ".o_dialog .modal[tabindex='-1']");
assert.strictEqual(
target.querySelector(`.o_dialog .o_field_widget[name="display_name"]`).value,
""
);
await testUtils.fields.editInput(
target.querySelector(`.o_dialog .o_field_widget[name="display_name"]`),
"wowl"
);
assert.strictEqual(
target.querySelector(`.o_dialog .o_field_widget[name="display_name"]`).value,
"wowl"
);
await testUtils.dom.click(target.querySelector(`.o_dialog .modal-header .btn-close`));
assert.containsNone(target, ".o_dialog");
assert.containsOnce(target, ".modal");
assert.containsOnce(target, ".modal[tabindex='-1']");
await testUtils.dom.click(target.querySelector(`.modal-header .btn-close`));
assert.containsNone(target, ".o_dialog");
assert.containsNone(target, ".modal");
});
});
});
File diff suppressed because it is too large Load Diff