summaryrefslogtreecommitdiff
path: root/addons/calendar/static
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/calendar/static
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/calendar/static')
-rw-r--r--addons/calendar/static/description/icon.pngbin0 -> 9180 bytes
-rw-r--r--addons/calendar/static/description/icon.svg23
-rw-r--r--addons/calendar/static/src/js/base_calendar.js164
-rw-r--r--addons/calendar/static/src/js/calendar_controller.js65
-rw-r--r--addons/calendar/static/src/js/calendar_model.js21
-rw-r--r--addons/calendar/static/src/js/calendar_renderer.js118
-rw-r--r--addons/calendar/static/src/js/calendar_view.js22
-rw-r--r--addons/calendar/static/src/js/mail_activity.js60
-rw-r--r--addons/calendar/static/src/js/systray_activity_menu.js56
-rw-r--r--addons/calendar/static/src/scss/calendar.scss69
-rw-r--r--addons/calendar/static/src/xml/base_calendar.xml58
-rw-r--r--addons/calendar/static/src/xml/notification_calendar.xml19
-rw-r--r--addons/calendar/static/tests/calendar_tests.js80
-rw-r--r--addons/calendar/static/tests/systray_activity_menu_tests.js82
14 files changed, 837 insertions, 0 deletions
diff --git a/addons/calendar/static/description/icon.png b/addons/calendar/static/description/icon.png
new file mode 100644
index 00000000..61b0defe
--- /dev/null
+++ b/addons/calendar/static/description/icon.png
Binary files differ
diff --git a/addons/calendar/static/description/icon.svg b/addons/calendar/static/description/icon.svg
new file mode 100644
index 00000000..a68d5706
--- /dev/null
+++ b/addons/calendar/static/description/icon.svg
@@ -0,0 +1,23 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70">
+ <defs>
+ <path id="icon-a" d="M4,5.35309892e-14 C36.4160122,9.87060235e-15 58.0836068,-3.97961823e-14 65,5.07020818e-14 C69,6.733808e-14 70,1 70,5 C70,43.0488877 70,62.4235458 70,65 C70,69 69,70 65,70 C61,70 9,70 4,70 C1,70 7.10542736e-15,69 7.10542736e-15,65 C7.25721566e-15,62.4676575 3.83358709e-14,41.8005206 3.60818146e-14,5 C-1.13686838e-13,1 1,5.75716207e-14 4,5.35309892e-14 Z"/>
+ <linearGradient id="icon-c" x1="100%" x2="0%" y1="0%" y2="100%">
+ <stop offset="0%" stop-color="#CDC484"/>
+ <stop offset="100%" stop-color="#B5AA59"/>
+ </linearGradient>
+ <path id="icon-d" d="M28.3839286,37.7083333 L24.9017857,37.7083333 C24.3272321,37.7083333 23.8571429,37.2513021 23.8571429,36.6927083 L23.8571429,33.3072917 C23.8571429,32.7486979 24.3272321,32.2916667 24.9017857,32.2916667 L28.3839286,32.2916667 C28.9584821,32.2916667 29.4285714,32.7486979 29.4285714,33.3072917 L29.4285714,36.6927083 C29.4285714,37.2513021 28.9584821,37.7083333 28.3839286,37.7083333 Z M37.7857143,36.6927083 L37.7857143,33.3072917 C37.7857143,32.7486979 37.315625,32.2916667 36.7410714,32.2916667 L33.2589286,32.2916667 C32.684375,32.2916667 32.2142857,32.7486979 32.2142857,33.3072917 L32.2142857,36.6927083 C32.2142857,37.2513021 32.684375,37.7083333 33.2589286,37.7083333 L36.7410714,37.7083333 C37.315625,37.7083333 37.7857143,37.2513021 37.7857143,36.6927083 Z M46.1428571,36.6927083 L46.1428571,33.3072917 C46.1428571,32.7486979 45.6727679,32.2916667 45.0982143,32.2916667 L41.6160714,32.2916667 C41.0415179,32.2916667 40.5714286,32.7486979 40.5714286,33.3072917 L40.5714286,36.6927083 C40.5714286,37.2513021 41.0415179,37.7083333 41.6160714,37.7083333 L45.0982143,37.7083333 C45.6727679,37.7083333 46.1428571,37.2513021 46.1428571,36.6927083 Z M37.7857143,44.8177083 L37.7857143,41.4322917 C37.7857143,40.8736979 37.315625,40.4166667 36.7410714,40.4166667 L33.2589286,40.4166667 C32.684375,40.4166667 32.2142857,40.8736979 32.2142857,41.4322917 L32.2142857,44.8177083 C32.2142857,45.3763021 32.684375,45.8333333 33.2589286,45.8333333 L36.7410714,45.8333333 C37.315625,45.8333333 37.7857143,45.3763021 37.7857143,44.8177083 Z M29.4285714,44.8177083 L29.4285714,41.4322917 C29.4285714,40.8736979 28.9584821,40.4166667 28.3839286,40.4166667 L24.9017857,40.4166667 C24.3272321,40.4166667 23.8571429,40.8736979 23.8571429,41.4322917 L23.8571429,44.8177083 C23.8571429,45.3763021 24.3272321,45.8333333 24.9017857,45.8333333 L28.3839286,45.8333333 C28.9584821,45.8333333 29.4285714,45.3763021 29.4285714,44.8177083 Z M46.1428571,44.8177083 L46.1428571,41.4322917 C46.1428571,40.8736979 45.6727679,40.4166667 45.0982143,40.4166667 L41.6160714,40.4166667 C41.0415179,40.4166667 40.5714286,40.8736979 40.5714286,41.4322917 L40.5714286,44.8177083 C40.5714286,45.3763021 41.0415179,45.8333333 41.6160714,45.8333333 L45.0982143,45.8333333 C45.6727679,45.8333333 46.1428571,45.3763021 46.1428571,44.8177083 Z M54.5,22.8125 L54.5,52.6041667 C54.5,54.8470052 52.6283482,56.6666667 50.3214286,56.6666667 L19.6785714,56.6666667 C17.3716518,56.6666667 15.5,54.8470052 15.5,52.6041667 L15.5,22.8125 C15.5,20.5696615 17.3716518,18.75 19.6785714,18.75 L23.8571429,18.75 L23.8571429,14.3489583 C23.8571429,13.7903646 24.3272321,13.3333333 24.9017857,13.3333333 L28.3839286,13.3333333 C28.9584821,13.3333333 29.4285714,13.7903646 29.4285714,14.3489583 L29.4285714,18.75 L40.5714286,18.75 L40.5714286,14.3489583 C40.5714286,13.7903646 41.0415179,13.3333333 41.6160714,13.3333333 L45.0982143,13.3333333 C45.6727679,13.3333333 46.1428571,13.7903646 46.1428571,14.3489583 L46.1428571,18.75 L50.3214286,18.75 C52.6283482,18.75 54.5,20.5696615 54.5,22.8125 Z M50.3214286,52.0963542 L50.3214286,26.875 L19.6785714,26.875 L19.6785714,52.0963542 C19.6785714,52.375651 19.9136161,52.6041667 20.2008929,52.6041667 L49.7991071,52.6041667 C50.0863839,52.6041667 50.3214286,52.375651 50.3214286,52.0963542 Z"/>
+ </defs>
+ <g fill="none" fill-rule="evenodd">
+ <mask id="icon-b" fill="#fff">
+ <use xlink:href="#icon-a"/>
+ </mask>
+ <g mask="url(#icon-b)">
+ <rect width="70" height="70" fill="url(#icon-c)"/>
+ <path fill="#FFF" fill-opacity=".383" d="M4,1.8 L65,1.8 C67.6666667,1.8 69.3333333,1.13333333 70,-0.2 C70,2.46666667 70,3.46666667 70,2.8 L1.10547097e-14,2.8 C-1.65952376e-14,3.46666667 -2.9161925e-14,2.46666667 -2.66453526e-14,-0.2 C0.666666667,1.13333333 2,1.8 4,1.8 Z" transform="matrix(1 0 0 -1 0 2.8)"/>
+ <path fill="#393939" d="M43.4796282,58 L3.97692274,58 C1.98846137,58 -7.06443391e-15,57.8520408 0,53.8571429 L2.26109583e-16,28.7421381 L18.1939643,6.70980496 L20,6 L24,0 L29.235775,0.958543565 L30.1559259,9.58543565 L42.1178875,8.51357709e-15 L45.7984911,0 L45.7984911,8 L54,8 L53.7495748,40.7451155 L43.4796282,58 Z" opacity=".324" transform="translate(0 12)"/>
+ <path fill="#000" fill-opacity=".383" d="M4,4 L65,4 C67.6666667,4 69.3333333,3 70,1 C70,3.66666667 70,5 70,5 L1.77635684e-15,5 C1.77635684e-15,5 1.77635684e-15,3.66666667 1.77635684e-15,1 C0.666666667,3 2,4 4,4 Z" transform="translate(0 65)"/>
+ <use fill="#000" fill-rule="nonzero" opacity=".35" xlink:href="#icon-d"/>
+ <path fill="#FFF" fill-rule="nonzero" d="M28.3839286,35.7083333 L24.9017857,35.7083333 C24.3272321,35.7083333 23.8571429,35.2513021 23.8571429,34.6927083 L23.8571429,31.3072917 C23.8571429,30.7486979 24.3272321,30.2916667 24.9017857,30.2916667 L28.3839286,30.2916667 C28.9584821,30.2916667 29.4285714,30.7486979 29.4285714,31.3072917 L29.4285714,34.6927083 C29.4285714,35.2513021 28.9584821,35.7083333 28.3839286,35.7083333 Z M37.7857143,34.6927083 L37.7857143,31.3072917 C37.7857143,30.7486979 37.315625,30.2916667 36.7410714,30.2916667 L33.2589286,30.2916667 C32.684375,30.2916667 32.2142857,30.7486979 32.2142857,31.3072917 L32.2142857,34.6927083 C32.2142857,35.2513021 32.684375,35.7083333 33.2589286,35.7083333 L36.7410714,35.7083333 C37.315625,35.7083333 37.7857143,35.2513021 37.7857143,34.6927083 Z M46.1428571,34.6927083 L46.1428571,31.3072917 C46.1428571,30.7486979 45.6727679,30.2916667 45.0982143,30.2916667 L41.6160714,30.2916667 C41.0415179,30.2916667 40.5714286,30.7486979 40.5714286,31.3072917 L40.5714286,34.6927083 C40.5714286,35.2513021 41.0415179,35.7083333 41.6160714,35.7083333 L45.0982143,35.7083333 C45.6727679,35.7083333 46.1428571,35.2513021 46.1428571,34.6927083 Z M37.7857143,42.8177083 L37.7857143,39.4322917 C37.7857143,38.8736979 37.315625,38.4166667 36.7410714,38.4166667 L33.2589286,38.4166667 C32.684375,38.4166667 32.2142857,38.8736979 32.2142857,39.4322917 L32.2142857,42.8177083 C32.2142857,43.3763021 32.684375,43.8333333 33.2589286,43.8333333 L36.7410714,43.8333333 C37.315625,43.8333333 37.7857143,43.3763021 37.7857143,42.8177083 Z M29.4285714,42.8177083 L29.4285714,39.4322917 C29.4285714,38.8736979 28.9584821,38.4166667 28.3839286,38.4166667 L24.9017857,38.4166667 C24.3272321,38.4166667 23.8571429,38.8736979 23.8571429,39.4322917 L23.8571429,42.8177083 C23.8571429,43.3763021 24.3272321,43.8333333 24.9017857,43.8333333 L28.3839286,43.8333333 C28.9584821,43.8333333 29.4285714,43.3763021 29.4285714,42.8177083 Z M46.1428571,42.8177083 L46.1428571,39.4322917 C46.1428571,38.8736979 45.6727679,38.4166667 45.0982143,38.4166667 L41.6160714,38.4166667 C41.0415179,38.4166667 40.5714286,38.8736979 40.5714286,39.4322917 L40.5714286,42.8177083 C40.5714286,43.3763021 41.0415179,43.8333333 41.6160714,43.8333333 L45.0982143,43.8333333 C45.6727679,43.8333333 46.1428571,43.3763021 46.1428571,42.8177083 Z M54.5,20.8125 L54.5,50.6041667 C54.5,52.8470052 52.6283482,54.6666667 50.3214286,54.6666667 L19.6785714,54.6666667 C17.3716518,54.6666667 15.5,52.8470052 15.5,50.6041667 L15.5,20.8125 C15.5,18.5696615 17.3716518,16.75 19.6785714,16.75 L23.8571429,16.75 L23.8571429,12.3489583 C23.8571429,11.7903646 24.3272321,11.3333333 24.9017857,11.3333333 L28.3839286,11.3333333 C28.9584821,11.3333333 29.4285714,11.7903646 29.4285714,12.3489583 L29.4285714,16.75 L40.5714286,16.75 L40.5714286,12.3489583 C40.5714286,11.7903646 41.0415179,11.3333333 41.6160714,11.3333333 L45.0982143,11.3333333 C45.6727679,11.3333333 46.1428571,11.7903646 46.1428571,12.3489583 L46.1428571,16.75 L50.3214286,16.75 C52.6283482,16.75 54.5,18.5696615 54.5,20.8125 Z M50.3214286,50.0963542 L50.3214286,24.875 L19.6785714,24.875 L19.6785714,50.0963542 C19.6785714,50.375651 19.9136161,50.6041667 20.2008929,50.6041667 L49.7991071,50.6041667 C50.0863839,50.6041667 50.3214286,50.375651 50.3214286,50.0963542 Z"/>
+ </g>
+ </g>
+</svg>
diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js
new file mode 100644
index 00000000..f2c1e56b
--- /dev/null
+++ b/addons/calendar/static/src/js/base_calendar.js
@@ -0,0 +1,164 @@
+odoo.define('base_calendar.base_calendar', function (require) {
+"use strict";
+
+var BasicModel = require('web.BasicModel');
+var fieldRegistry = require('web.field_registry');
+var Notification = require('web.Notification');
+var relationalFields = require('web.relational_fields');
+var session = require('web.session');
+var WebClient = require('web.WebClient');
+
+var FieldMany2ManyTags = relationalFields.FieldMany2ManyTags;
+
+var CalendarNotification = Notification.extend({
+ template: "CalendarNotification",
+ xmlDependencies: (Notification.prototype.xmlDependencies || [])
+ .concat(['/calendar/static/src/xml/notification_calendar.xml']),
+
+ init: function(parent, params) {
+ this._super(parent, params);
+ this.eid = params.eventID;
+ this.sticky = true;
+
+ this.events = _.extend(this.events || {}, {
+ 'click .link2event': function() {
+ var self = this;
+
+ this._rpc({
+ route: '/web/action/load',
+ params: {
+ action_id: 'calendar.action_calendar_event_notify',
+ },
+ })
+ .then(function(r) {
+ r.res_id = self.eid;
+ return self.do_action(r);
+ });
+ },
+
+ 'click .link2recall': function() {
+ this.close();
+ },
+
+ 'click .link2showed': function() {
+ this._rpc({route: '/calendar/notify_ack'})
+ .then(this.close.bind(this, false), this.close.bind(this, false));
+ },
+ });
+ },
+});
+
+WebClient.include({
+ display_calendar_notif: function(notifications) {
+ var self = this;
+ var last_notif_timer = 0;
+
+ // Clear previously set timeouts and destroy currently displayed calendar notifications
+ clearTimeout(this.get_next_calendar_notif_timeout);
+ _.each(this.calendar_notif_timeouts, clearTimeout);
+ this.calendar_notif_timeouts = {};
+
+ // For each notification, set a timeout to display it
+ _.each(notifications, function(notif) {
+ var key = notif.event_id + ',' + notif.alarm_id;
+ if (key in self.calendar_notif) {
+ return;
+ }
+ self.calendar_notif_timeouts[key] = setTimeout(function () {
+ var notificationID = self.call('notification', 'notify', {
+ Notification: CalendarNotification,
+ title: notif.title,
+ message: notif.message,
+ eventID: notif.event_id,
+ onClose: function () {
+ delete self.calendar_notif[key];
+ },
+ });
+ self.calendar_notif[key] = notificationID;
+ }, notif.timer * 1000);
+ last_notif_timer = Math.max(last_notif_timer, notif.timer);
+ });
+
+ // Set a timeout to get the next notifications when the last one has been displayed
+ if (last_notif_timer > 0) {
+ this.get_next_calendar_notif_timeout = setTimeout(this.get_next_calendar_notif.bind(this), last_notif_timer * 1000);
+ }
+ },
+ get_next_calendar_notif: function() {
+ session.rpc("/calendar/notify", {}, {shadow: true})
+ .then(this.display_calendar_notif.bind(this))
+ .guardedCatch(function(reason) { //
+ var err = reason.message;
+ var ev = reason.event;
+ if(err.code === -32098) {
+ // Prevent the CrashManager to display an error
+ // in case of an xhr error not due to a server error
+ ev.preventDefault();
+ }
+ });
+ },
+ show_application: function() {
+ // An event is triggered on the bus each time a calendar event with alarm
+ // in which the current user is involved is created, edited or deleted
+ this.calendar_notif_timeouts = {};
+ this.calendar_notif = {};
+ this.call('bus_service', 'onNotification', this, function (notifications) {
+ _.each(notifications, (function (notification) {
+ if (notification[0][1] === 'calendar.alarm') {
+ this.display_calendar_notif(notification[1]);
+ }
+ }).bind(this));
+ });
+ return this._super.apply(this, arguments).then(this.get_next_calendar_notif.bind(this));
+ },
+});
+
+BasicModel.include({
+ /**
+ * @private
+ * @param {Object} record
+ * @param {string} fieldName
+ * @returns {Promise}
+ */
+ _fetchSpecialAttendeeStatus: function (record, fieldName) {
+ var context = record.getContext({fieldName: fieldName});
+ var attendeeIDs = record.data[fieldName] ? this.localData[record.data[fieldName]].res_ids : [];
+ var meetingID = _.isNumber(record.res_id) ? record.res_id : false;
+ return this._rpc({
+ model: 'res.partner',
+ method: 'get_attendee_detail',
+ args: [attendeeIDs, meetingID],
+ context: context,
+ }).then(function (result) {
+ return _.map(result, function (d) {
+ return _.object(['id', 'display_name', 'status', 'color'], d);
+ });
+ });
+ },
+});
+
+var Many2ManyAttendee = FieldMany2ManyTags.extend({
+ // as this widget is model dependant (rpc on res.partner), use it in another
+ // context probably won't work
+ // supportedFieldTypes: ['many2many'],
+ tag_template: "Many2ManyAttendeeTag",
+ specialData: "_fetchSpecialAttendeeStatus",
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @private
+ */
+ _getRenderTagsContext: function () {
+ var result = this._super.apply(this, arguments);
+ result.attendeesData = this.record.specialData.partner_ids;
+ return result;
+ },
+});
+
+fieldRegistry.add('many2manyattendee', Many2ManyAttendee);
+
+});
diff --git a/addons/calendar/static/src/js/calendar_controller.js b/addons/calendar/static/src/js/calendar_controller.js
new file mode 100644
index 00000000..190125ac
--- /dev/null
+++ b/addons/calendar/static/src/js/calendar_controller.js
@@ -0,0 +1,65 @@
+odoo.define('calendar.CalendarController', function (require) {
+ "use strict";
+
+ const Controller = require('web.CalendarController');
+ const Dialog = require('web.Dialog');
+ const { qweb, _t } = require('web.core');
+
+ const CalendarController = Controller.extend({
+
+ _askRecurrenceUpdatePolicy() {
+ return new Promise((resolve, reject) => {
+ new Dialog(this, {
+ title: _t('Edit Recurrent event'),
+ size: 'small',
+ $content: $(qweb.render('calendar.RecurrentEventUpdate')),
+ buttons: [{
+ text: _t('Confirm'),
+ classes: 'btn-primary',
+ close: true,
+ click: function () {
+ resolve(this.$('input:checked').val());
+ },
+ }],
+ }).open();
+ });
+ },
+
+ // TODO factorize duplicated code
+ /**
+ * @override
+ * @private
+ * @param {OdooEvent} event
+ */
+ async _onDropRecord(event) {
+ const _super = this._super; // reference to this._super is lost after async call
+ if (event.data.record.recurrency) {
+ const recurrenceUpdate = await this._askRecurrenceUpdatePolicy();
+ event.data = _.extend({}, event.data, {
+ 'recurrenceUpdate': recurrenceUpdate,
+ });
+ }
+ _super.apply(this, arguments);
+ },
+
+ /**
+ * @override
+ * @private
+ * @param {OdooEvent} event
+ */
+ async _onUpdateRecord(event) {
+ const _super = this._super; // reference to this._super is lost after async call
+ if (event.data.record.recurrency) {
+ const recurrenceUpdate = await this._askRecurrenceUpdatePolicy();
+ event.data = _.extend({}, event.data, {
+ 'recurrenceUpdate': recurrenceUpdate,
+ });
+ }
+ _super.apply(this, arguments);
+ },
+
+ });
+
+ return CalendarController;
+
+});
diff --git a/addons/calendar/static/src/js/calendar_model.js b/addons/calendar/static/src/js/calendar_model.js
new file mode 100644
index 00000000..8d90b543
--- /dev/null
+++ b/addons/calendar/static/src/js/calendar_model.js
@@ -0,0 +1,21 @@
+odoo.define('calendar.CalendarModel', function (require) {
+ "use strict";
+
+ const Model = require('web.CalendarModel');
+
+ const CalendarModel = Model.extend({
+
+ /**
+ * @override
+ * Transform fullcalendar event object to odoo Data object
+ */
+ calendarEventToRecord(event) {
+ const data = this._super(event);
+ return _.extend({}, data, {
+ 'recurrence_update': event.recurrenceUpdate,
+ });
+ }
+ });
+
+ return CalendarModel;
+});
diff --git a/addons/calendar/static/src/js/calendar_renderer.js b/addons/calendar/static/src/js/calendar_renderer.js
new file mode 100644
index 00000000..6aa97a40
--- /dev/null
+++ b/addons/calendar/static/src/js/calendar_renderer.js
@@ -0,0 +1,118 @@
+odoo.define('calendar.CalendarRenderer', function (require) {
+"use strict";
+
+const CalendarRenderer = require('web.CalendarRenderer');
+const CalendarPopover = require('web.CalendarPopover');
+const session = require('web.session');
+
+
+const AttendeeCalendarPopover = CalendarPopover.extend({
+ template: 'Calendar.attendee.status.popover',
+ events: _.extend({}, CalendarPopover.prototype.events, {
+ 'click .o-calendar-attendee-status .dropdown-item': '_onClickAttendeeStatus'
+ }),
+ /**
+ * @constructor
+ */
+ init: function () {
+ var self = this;
+ this._super.apply(this, arguments);
+ // Show status dropdown if user is in attendees list
+ if (this.isCurrentPartnerAttendee()) {
+ this.statusColors = {accepted: 'text-success', declined: 'text-danger', tentative: 'text-muted', needsAction: 'text-dark'};
+ this.statusInfo = {};
+ _.each(this.fields.attendee_status.selection, function (selection) {
+ self.statusInfo[selection[0]] = {text: selection[1], color: self.statusColors[selection[0]]};
+ });
+ this.selectedStatusInfo = this.statusInfo[this.event.extendedProps.record.attendee_status];
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @return {boolean}
+ */
+ isCurrentPartnerAttendee() {
+ return this.event.extendedProps.record.partner_ids.includes(session.partner_id);
+ },
+ /**
+ * @override
+ * @return {boolean}
+ */
+ isEventDeletable() {
+ return this._super() && (this._isEventPrivate() ? this.isCurrentPartnerAttendee() : true);
+ },
+ /**
+ * @override
+ * @return {boolean}
+ */
+ isEventDetailsVisible() {
+ return this._isEventPrivate() ? this.isCurrentPartnerAttendee() : this._super();
+ },
+ /**
+ * @override
+ * @return {boolean}
+ */
+ isEventEditable() {
+ return this._isEventPrivate() ? this.isCurrentPartnerAttendee() : this._super();
+ },
+ /**
+ * @return {boolean}
+ */
+ displayAttendeeAnswerChoice() {
+ // check if we are a partner and if we are the only attendee.
+ // This avoid to display attendee anwser dropdown for single user attendees
+ const isCurrentpartner = (currentValue) => currentValue === session.partner_id;
+ const onlyAttendee = this.event.extendedProps.record.partner_ids.every(isCurrentpartner);
+ return this.isCurrentPartnerAttendee() && ! onlyAttendee;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @return {boolean}
+ */
+ _isEventPrivate() {
+ return this.event.extendedProps.record.privacy === 'private';
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClickAttendeeStatus: function (ev) {
+ ev.preventDefault();
+ var self = this;
+ var selectedStatus = $(ev.currentTarget).attr('data-action');
+ this._rpc({
+ model: 'calendar.event',
+ method: 'change_attendee_status',
+ args: [parseInt(this.event.id), selectedStatus],
+ }).then(function () {
+ self.event.extendedProps.record.attendee_status = selectedStatus; // FIXEME: Maybe we have to reload view
+ self.$('.o-calendar-attendee-status-text').text(self.statusInfo[selectedStatus].text);
+ self.$('.o-calendar-attendee-status-icon').removeClass(_.values(self.statusColors).join(' ')).addClass(self.statusInfo[selectedStatus].color);
+ });
+ },
+});
+
+
+const AttendeeCalendarRenderer = CalendarRenderer.extend({
+ config: _.extend({}, CalendarRenderer.prototype.config, {
+ CalendarPopover: AttendeeCalendarPopover,
+ }),
+});
+
+return AttendeeCalendarRenderer
+
+});
diff --git a/addons/calendar/static/src/js/calendar_view.js b/addons/calendar/static/src/js/calendar_view.js
new file mode 100644
index 00000000..4454dc0e
--- /dev/null
+++ b/addons/calendar/static/src/js/calendar_view.js
@@ -0,0 +1,22 @@
+odoo.define('calendar.CalendarView', function (require) {
+"use strict";
+
+var CalendarController = require('calendar.CalendarController');
+var CalendarModel = require('calendar.CalendarModel');
+const CalendarRenderer = require('calendar.CalendarRenderer');
+var CalendarView = require('web.CalendarView');
+var viewRegistry = require('web.view_registry');
+
+var AttendeeCalendarView = CalendarView.extend({
+ config: _.extend({}, CalendarView.prototype.config, {
+ Renderer: CalendarRenderer,
+ Controller: CalendarController,
+ Model: CalendarModel,
+ }),
+});
+
+viewRegistry.add('attendee_calendar', AttendeeCalendarView);
+
+return AttendeeCalendarView
+
+});
diff --git a/addons/calendar/static/src/js/mail_activity.js b/addons/calendar/static/src/js/mail_activity.js
new file mode 100644
index 00000000..3c478921
--- /dev/null
+++ b/addons/calendar/static/src/js/mail_activity.js
@@ -0,0 +1,60 @@
+odoo.define('calendar.Activity', function (require) {
+"use strict";
+
+var Activity = require('mail.Activity');
+var Dialog = require('web.Dialog');
+var core = require('web.core');
+var _t = core._t;
+
+Activity.include({
+
+ /**
+ * Override behavior to redirect to calendar event instead of activity
+ *
+ * @override
+ */
+ _onEditActivity: function (event, options) {
+ var self = this;
+ var activity_id = $(event.currentTarget).data('activity-id');
+ var activity = _.find(this.activities, function (act) { return act.id === activity_id; });
+ if (activity && activity.activity_category === 'meeting' && activity.calendar_event_id) {
+ return self._super(event, _.extend({
+ res_model: 'calendar.event',
+ res_id: activity.calendar_event_id[0],
+ }));
+ }
+ return self._super(event, options);
+ },
+
+ /**
+ * Override behavior to warn that the calendar event is about to be removed as well
+ *
+ * @override
+ */
+ _onUnlinkActivity: function (event, options) {
+ event.preventDefault();
+ var self = this;
+ var activity_id = $(event.currentTarget).data('activity-id');
+ var activity = _.find(this.activities, function (act) { return act.id === activity_id; });
+ if (activity && activity.activity_category === 'meeting' && activity.calendar_event_id) {
+ Dialog.confirm(
+ self,
+ _t("The activity is linked to a meeting. Deleting it will remove the meeting as well. Do you want to proceed ?"), {
+ confirm_callback: function () {
+ return self._rpc({
+ model: 'mail.activity',
+ method: 'unlink_w_meeting',
+ args: [[activity_id]],
+ })
+ .then(self._reload.bind(self, {activity: true}));
+ },
+ }
+ );
+ }
+ else {
+ return self._super(event, options);
+ }
+ },
+});
+
+});
diff --git a/addons/calendar/static/src/js/systray_activity_menu.js b/addons/calendar/static/src/js/systray_activity_menu.js
new file mode 100644
index 00000000..d6f98b69
--- /dev/null
+++ b/addons/calendar/static/src/js/systray_activity_menu.js
@@ -0,0 +1,56 @@
+odoo.define('calendar.systray.ActivityMenu', function (require) {
+"use strict";
+
+var ActivityMenu = require('mail.systray.ActivityMenu');
+var fieldUtils = require('web.field_utils');
+
+ActivityMenu.include({
+
+ //-----------------------------------------
+ // Private
+ //-----------------------------------------
+
+ /**
+ * parse date to server value
+ *
+ * @private
+ * @override
+ */
+ _getActivityData: function () {
+ var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ var meeting = _.find(self._activities, {type: 'meeting'});
+ if (meeting && meeting.meetings) {
+ _.each(meeting.meetings, function (res) {
+ res.start = fieldUtils.parse.datetime(res.start, false, {isUTC: true});
+ });
+ }
+ });
+ },
+
+ //-----------------------------------------
+ // Handlers
+ //-----------------------------------------
+
+ /**
+ * @private
+ * @override
+ */
+ _onActivityFilterClick: function (ev) {
+ var $el = $(ev.currentTarget);
+ var data = _.extend({}, $el.data());
+ if (data.res_model === "calendar.event" && data.filter === "my") {
+ this.do_action('calendar.action_calendar_event', {
+ additional_context: {
+ default_mode: 'day',
+ search_default_mymeetings: 1,
+ },
+ clear_breadcrumbs: true,
+ });
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+});
+
+});
diff --git a/addons/calendar/static/src/scss/calendar.scss b/addons/calendar/static/src/scss/calendar.scss
new file mode 100644
index 00000000..7e98c95d
--- /dev/null
+++ b/addons/calendar/static/src/scss/calendar.scss
@@ -0,0 +1,69 @@
+.o_calendar_invitation {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 5px;
+
+ &.accepted {
+ background-color: theme-color('success');
+ }
+
+ &.needsAction {
+ background-color: $o-brand-secondary;
+ }
+
+ &.declined {
+ background-color: theme-color('danger');
+ }
+}
+
+.o_add_favorite_calendar {
+ margin-top: 10px;
+ position: relative;
+}
+
+.o_calendar_invitation_page {
+ flex: 0 0 auto;
+ width: 50%;
+ margin: 30px auto 0;
+ @include o-webclient-padding($top: 10px, $bottom: 10px);
+ background-color: $o-view-background-color;
+
+ .o_logo {
+ width: 15%;
+ }
+ .o_event_title {
+ margin-left: 20%;
+
+ h2 {
+ margin-top: 0;
+ }
+ }
+ .o_event_table {
+ clear: both;
+ margin: 15px 0 0;
+
+ th {
+ padding-right: 15px;
+ }
+ ul {
+ padding-left: 0;
+ }
+ }
+
+ .o_accepted {
+ color: theme-color('success');
+ }
+ .o_declined {
+ color: theme-color('danger');
+ }
+}
+.o_meeting_filter {
+ @include o-text-overflow();
+ width: 64%;
+ color: grey;
+ vertical-align: top;
+ &.o_meeting_bold {
+ font-weight: bold;
+ }
+}
diff --git a/addons/calendar/static/src/xml/base_calendar.xml b/addons/calendar/static/src/xml/base_calendar.xml
new file mode 100644
index 00000000..b136ee28
--- /dev/null
+++ b/addons/calendar/static/src/xml/base_calendar.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<template>
+ <t t-name="Many2ManyAttendeeTag" t-extend="FieldMany2ManyTag">
+ <t t-jquery="span:first" t-operation="prepend">
+ <span t-if="attendeesData[el_index]" t-attf-class="o_calendar_invitation #{attendeesData[el_index].status}"/>
+ </t>
+ </t>
+
+ <div t-name="calendar.RecurrentEventUpdate">
+ <div class="form-check o_radio_item">
+ <input name="recurrence-update" type="radio" class="form-check-input o_radio_input" checked="true" value="self_only" id="self_only"/>
+ <label class="form-check-label o_form_label" for="self_only">This event</label>
+ </div>
+
+ <div class="form-check o_radio_item">
+ <input name="recurrence-update" type="radio" class="form-check-input o_radio_input" value="future_events" id="future_events"/>
+ <label class="form-check-label o_form_label" for="future_events">This and following events</label>
+ </div>
+ </div>
+
+ <t t-extend="mail.systray.ActivityMenu.Previews">
+ <t t-jquery="div.o_preview_title" t-operation="after">
+ <div t-if="activity and activity.type == 'meeting'">
+ <t t-set="is_next_meeting" t-value="true"/>
+ <t t-foreach="activity.meetings" t-as="meeting">
+ <div>
+ <span t-att-class="!meeting.allday and is_next_meeting ? 'o_meeting_filter o_meeting_bold' : 'o_meeting_filter'" t-att-data-res_model="activity.model" t-att-data-res_id="meeting.id" t-att-data-model_name="activity.name" t-att-title="meeting.name">
+ <span><t t-esc="meeting.name"/></span>
+ </span>
+ <span t-if="meeting.start" class="float-right">
+ <t t-if="meeting.allday">All Day</t>
+ <t t-else=''>
+ <t t-set="is_next_meeting" t-value="false"/>
+ <t t-esc="moment(meeting.start).local().format(Time.strftime_to_moment_format(_t.database.parameters.time_format))"/>
+ </t>
+ </span>
+ </div>
+ </t>
+ </div>
+ </t>
+ </t>
+
+ <t t-name="Calendar.attendee.status.popover" t-extend="CalendarView.event.popover">
+ <t t-jquery=".o_cw_popover_delete" t-operation="after">
+ <div t-if="widget.displayAttendeeAnswerChoice()" class="btn-group o-calendar-attendee-status ml-2">
+ <a href="#" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ <i t-attf-class="fa fa-circle o-calendar-attendee-status-icon #{widget.selectedStatusInfo.color}"/> <span class="o-calendar-attendee-status-text" t-esc="widget.selectedStatusInfo.text"></span>
+ </a>
+ <div class="dropdown-menu overflow-hidden">
+ <a class="dropdown-item" href="#" data-action="accepted"><i class="fa fa-circle text-success"/> Accept</a>
+ <a class="dropdown-item" href="#" data-action="declined"><i class="fa fa-circle text-danger"/> Decline</a>
+ <a class="dropdown-item" href="#" data-action="tentative"><i class="fa fa-circle text-muted"/> Uncertain</a>
+ </div>
+ </div>
+ </t>
+ </t>
+
+</template>
diff --git a/addons/calendar/static/src/xml/notification_calendar.xml b/addons/calendar/static/src/xml/notification_calendar.xml
new file mode 100644
index 00000000..3c51293f
--- /dev/null
+++ b/addons/calendar/static/src/xml/notification_calendar.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<template>
+
+<t t-name="CalendarNotification" t-extend="Notification">
+ <t t-jquery=".o_notification_title > t" t-operation="replace">
+ <span t-attf-class="link2event eid_{{widget.eid}}">
+ <t t-esc="widget.title"/>
+ </span>
+ </t>
+ <t t-jquery=".o_notification_content" t-operation="after">
+ <div class="mt-2">
+ <button type="button" class="btn btn-primary link2showed oe_highlight oe_form oe_button"><span>OK</span></button>
+ <button type="button" class="btn btn-link link2event">Details</button>
+ <button type="button" class="btn btn-link link2recall">Snooze</button>
+ </div>
+ </t>
+</t>
+
+</template>
diff --git a/addons/calendar/static/tests/calendar_tests.js b/addons/calendar/static/tests/calendar_tests.js
new file mode 100644
index 00000000..3eea3dff
--- /dev/null
+++ b/addons/calendar/static/tests/calendar_tests.js
@@ -0,0 +1,80 @@
+odoo.define('calendar.tests', function (require) {
+"use strict";
+
+var FormView = require('web.FormView');
+var testUtils = require("web.test_utils");
+
+var createView = testUtils.createView;
+
+QUnit.module('calendar', {
+ beforeEach: function () {
+ this.data = {
+ event: {
+ fields: {
+ partner_ids: {string: "Partners", type: "many2many", relation: "partner"},
+ },
+ records: [{
+ id: 14,
+ partner_ids: [1, 2],
+ }],
+ },
+ partner: {
+ fields: {
+ name: {string: "Name", type: "char"},
+ },
+ records: [{
+ id: 1,
+ name: "Jesus",
+ }, {
+ id: 2,
+ name: "Mahomet",
+ }],
+ },
+ };
+ },
+}, function () {
+ QUnit.test("many2manyattendee widget: basic rendering", async function (assert) {
+ assert.expect(9);
+
+ var form = await createView({
+ View: FormView,
+ model: 'event',
+ data: this.data,
+ res_id: 14,
+ arch:
+ '<form>' +
+ '<field name="partner_ids" widget="many2manyattendee"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'get_attendee_detail') {
+ assert.strictEqual(args.model, 'res.partner',
+ "the method should only be called on res.partner");
+ assert.deepEqual(args.args[0], [1, 2],
+ "the partner ids should be passed as argument");
+ assert.strictEqual(args.args[1], 14,
+ "the event id should be passed as argument");
+ return Promise.resolve([
+ [1, "Jesus", "accepted", 0],
+ [2, "Mahomet", "needsAction", 0],
+ ]);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.hasClass(form.$('.o_field_widget[name="partner_ids"]'), 'o_field_many2manytags');
+ assert.containsN(form, '.o_field_widget[name="partner_ids"] .badge', 2,
+ "there should be 2 tags");
+ assert.strictEqual(form.$('.o_field_widget[name="partner_ids"] .badge:first').text().trim(), "Jesus",
+ "the tag should be correctly named");
+ assert.hasClass(form.$('.o_field_widget[name="partner_ids"] .badge:first .o_calendar_invitation'),'accepted',
+ "Jesus should attend the meeting");
+ assert.strictEqual(form.$('.o_field_widget[name="partner_ids"] .badge[data-id="2"]').text().trim(), "Mahomet",
+ "the tag should be correctly named");
+ assert.hasClass(form.$('.o_field_widget[name="partner_ids"] .badge[data-id="2"] .o_calendar_invitation'),'needsAction',
+ "Mohamet should still confirm his attendance to the meeting");
+
+ form.destroy();
+ });
+});
+});
diff --git a/addons/calendar/static/tests/systray_activity_menu_tests.js b/addons/calendar/static/tests/systray_activity_menu_tests.js
new file mode 100644
index 00000000..aaf2bd74
--- /dev/null
+++ b/addons/calendar/static/tests/systray_activity_menu_tests.js
@@ -0,0 +1,82 @@
+odoo.define('calendar.systray.ActivityMenuTests', function (require) {
+"use strict";
+
+const { afterEach, beforeEach, start } = require('mail/static/src/utils/test_utils.js');
+var ActivityMenu = require('mail.systray.ActivityMenu');
+
+var testUtils = require('web.test_utils');
+
+QUnit.module('calendar', {}, function () {
+QUnit.module('ActivityMenu', {
+ beforeEach() {
+ beforeEach(this);
+
+ Object.assign(this.data, {
+ 'calendar.event': {
+ fields: { // those are all fake, this is the mock of a formatter
+ meetings: { type: 'binary' },
+ model: { type: 'char' },
+ name: { type: 'char', required: true },
+ type: { type: 'char' },
+ },
+ records: [{
+ name: "Today's meeting (3)",
+ model: "calendar.event",
+ type: 'meeting',
+ meetings: [{
+ id: 1,
+ res_model: "calendar.event",
+ name: "meeting1",
+ start: "2018-04-20 06:30:00",
+ allday: false,
+ }, {
+ id: 2,
+ res_model: "calendar.event",
+ name: "meeting2",
+ start: "2018-04-20 09:30:00",
+ allday: false,
+ }]
+ }],
+ },
+ });
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('activity menu widget:today meetings', async function (assert) {
+ assert.expect(6);
+ var self = this;
+
+ const { widget } = await start({
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'systray_get_activities') {
+ return Promise.resolve(self.data['calendar.event']['records']);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ const activityMenu = new ActivityMenu(widget);
+ await activityMenu.appendTo($('#qunit-fixture'));
+
+ assert.hasClass(activityMenu.$el, 'o_mail_systray_item', 'should be the instance of widget');
+
+ await testUtils.dom.click(activityMenu.$('.dropdown-toggle'));
+
+ testUtils.mock.intercept(activityMenu, 'do_action', function (event) {
+ assert.strictEqual(event.data.action, "calendar.action_calendar_event", 'should open meeting calendar view in day mode');
+ });
+ await testUtils.dom.click(activityMenu.$('.o_mail_preview'));
+
+ assert.ok(activityMenu.$('.o_meeting_filter'), "should be a meeting");
+ assert.containsN(activityMenu, '.o_meeting_filter', 2, 'there should be 2 meetings');
+ assert.hasClass(activityMenu.$('.o_meeting_filter').eq(0), 'o_meeting_bold', 'this meeting is yet to start');
+ assert.doesNotHaveClass(activityMenu.$('.o_meeting_filter').eq(1), 'o_meeting_bold', 'this meeting has been started');
+ widget.destroy();
+});
+});
+
+});