[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:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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() &&!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
Reference in New Issue
Block a user