diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/calendar/static/src | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/calendar/static/src')
| -rw-r--r-- | addons/calendar/static/src/js/base_calendar.js | 164 | ||||
| -rw-r--r-- | addons/calendar/static/src/js/calendar_controller.js | 65 | ||||
| -rw-r--r-- | addons/calendar/static/src/js/calendar_model.js | 21 | ||||
| -rw-r--r-- | addons/calendar/static/src/js/calendar_renderer.js | 118 | ||||
| -rw-r--r-- | addons/calendar/static/src/js/calendar_view.js | 22 | ||||
| -rw-r--r-- | addons/calendar/static/src/js/mail_activity.js | 60 | ||||
| -rw-r--r-- | addons/calendar/static/src/js/systray_activity_menu.js | 56 | ||||
| -rw-r--r-- | addons/calendar/static/src/scss/calendar.scss | 69 | ||||
| -rw-r--r-- | addons/calendar/static/src/xml/base_calendar.xml | 58 | ||||
| -rw-r--r-- | addons/calendar/static/src/xml/notification_calendar.xml | 19 |
10 files changed, 652 insertions, 0 deletions
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> |
