[IMP] web,hr_homeworking,calendar: overwrite default fc header to allow customization

In some modules at Odoo we want to have custom headers for calendar. Since
fullcalendar accepts a function callback that should render the header, it was
decided to have this method exist in the calendar_common_renderer. So any
other apps that want to add something to the header, can just extend this
method. This commit also changes how homeworking changes the header so that it
complies with this change.

task-3455051
task-3389317

Part-of: odoo/odoo#138670
This commit is contained in:
Leonardo Pavan Rocha
2023-10-23 20:07:23 +00:00
parent 83cd06fae6
commit 2f4a83e5ab
7 changed files with 129 additions and 115 deletions
@@ -1,156 +1,161 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_service/tour_utils";
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_service/tour_utils";
const todayDate = function() {
let now = new Date();
let year = now.getFullYear();
let month = String(now.getMonth() + 1).padStart(2, '0');
let day = String(now.getDate()).padStart(2, '0');
const todayDate = function () {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
return `${month}/${day}/${year} 10:00:00`;
};
return `${month}/${day}/${year} 10:00:00`;
};
registry.category("web_tour.tours").add('calendar_appointments_hour_tour', {
url: '/web',
test: true,
steps: () => [stepUtils.showAppsMenuItem(),
registry.category("web_tour.tours").add("calendar_appointments_hour_tour", {
url: "/web",
test: true,
steps: () => [
stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="calendar.mail_menu_calendar"]',
content: 'Open Calendar',
run: 'click',
content: "Open Calendar",
run: "click",
},
{
trigger: '.o-calendar-button-new',
content: 'Create a new event',
run: 'click',
trigger: ".o-calendar-button-new",
content: "Create a new event",
run: "click",
},
{
trigger: '#name_0',
content: 'Give a name to the new event',
run: 'text TEST EVENT',
trigger: "#name_0",
content: "Give a name to the new event",
run: "text TEST EVENT",
},
{
trigger: '#start_0',
content: 'Give a date to the new event',
trigger: "#start_0",
content: "Give a date to the new event",
run: `text ${todayDate()}`,
},
{
trigger: '.fa-cloud-upload',
content: 'Save the new event',
run: 'click',
trigger: ".fa-cloud-upload",
content: "Save the new event",
run: "click",
},
{
trigger: '.o_back_button',
content: 'Go back to Calendar view',
run: 'click',
trigger: ".o_back_button",
content: "Go back to Calendar view",
run: "click",
},
{
trigger: '.dropdown-toggle:contains("Week")',
content: 'Click to change calendar view',
run: 'click',
content: "Click to change calendar view",
run: "click",
},
{
trigger: '.dropdown-item:contains("Month")',
content: 'Change the calendar view to Month',
run: 'click',
content: "Change the calendar view to Month",
run: "click",
},
{
trigger: '.fc-day-header:contains("Monday")',
content: 'Change the calendar view to week',
trigger: '.fc-day-header:contains("Mon")',
content: "Check the day is properly displayed",
},
{
trigger: '.fc-time:contains("10:00")',
content: 'Check the time is properly displayed',
content: "Check the time is properly displayed",
},
{
trigger: '.o_event_title:contains("TEST EVENT")',
content: 'Check the event title',
content: "Check the event title",
isCheck: true,
},
]});
],
});
registry.category("web_tour.tours").add('test_calendar_delete_tour', {
test: true,
steps: () => [
registry.category("web_tour.tours").add("test_calendar_delete_tour", {
test: true,
steps: () => [
{
content: 'Select filter (everybody)',
content: "Select filter (everybody)",
trigger: 'div[data-value="all"] input',
},
{
content: 'Click on the event (focus + waiting)',
content: "Click on the event (focus + waiting)",
trigger: 'a .fc-content:contains("Test Event")',
async run() {
$('a .fc-content:contains("Test Event")').click();
await new Promise((r) => setTimeout(r, 1000));
$('a .fc-content:contains("Test Event")').click();
}
},
},
{
content: 'Delete the event',
trigger: '.o_cw_popover_delete',
content: "Delete the event",
trigger: ".o_cw_popover_delete",
},
{
content: 'Validate the deletion',
trigger:'button:contains("Delete")',
content: "Validate the deletion",
trigger: 'button:contains("Delete")',
async run() {
$('button:contains("Delete")').click();
await new Promise((r) => setTimeout(r, 1000));
}
},
},
]});
registry.category("web_tour.tours").add('test_calendar_decline_tour', {
test: true,
steps: () => [
],
});
registry.category("web_tour.tours").add("test_calendar_decline_tour", {
test: true,
steps: () => [
{
content: 'Click on the event (focus + waiting)',
content: "Click on the event (focus + waiting)",
trigger: 'a .fc-content:contains("Test Event")',
async run() {
$('a .fc-content:contains("Test Event")').click();
await new Promise((r) => setTimeout(r, 1000));
$('a .fc-content:contains("Test Event")').click();
}
},
},
{
content: 'Delete the event',
trigger: '.o_cw_popover_delete',
content: "Delete the event",
trigger: ".o_cw_popover_delete",
},
{
content: 'Wait declined status',
trigger: '.o_attendee_status_declined',
content: "Wait declined status",
trigger: ".o_attendee_status_declined",
isCheck: true,
},
]});
registry.category("web_tour.tours").add('test_calendar_decline_with_everybody_filter_tour', {
test: true,
steps: () => [
],
});
registry.category("web_tour.tours").add("test_calendar_decline_with_everybody_filter_tour", {
test: true,
steps: () => [
{
content: 'Select filter (everybody)',
content: "Select filter (everybody)",
trigger: 'div[data-value="all"] input',
},
{
content: 'Click on the event (focus + waiting)',
content: "Click on the event (focus + waiting)",
trigger: 'a .fc-content:contains("Test Event")',
async run() {
$('a .fc-content:contains("Test Event")').click();
await new Promise((r) => setTimeout(r, 1000));
$('a .fc-content:contains("Test Event")').click();
}
},
},
{
content: 'Delete the event',
trigger: '.o_cw_popover_delete',
content: "Delete the event",
trigger: ".o_cw_popover_delete",
},
{
content: 'Select filter (everybody)',
content: "Select filter (everybody)",
trigger: 'div[data-value="all"] input',
},
{
content: 'Wait declined status',
trigger: '.o_attendee_status_declined',
content: "Wait declined status",
trigger: ".o_attendee_status_declined",
isCheck: true,
},
]});
],
});
@@ -3,39 +3,13 @@
import { AttendeeCalendarCommonRenderer } from "@calendar/views/attendee_calendar/common/attendee_calendar_common_renderer";
import { patch } from "@web/core/utils/patch";
import { renderToString } from "@web/core/utils/render";
const { DateTime } = luxon;
patch(AttendeeCalendarCommonRenderer.prototype, {
get options(){
let a = Object.assign(super.options, {
columnHeaderHtml: function(date) {
const datetime = DateTime.fromJSDate(date);
if (this.props.model.scale === 'week'){
return (
"<div>" +
datetime.toFormat("ccc dd") +
"</div>" +
renderToString(this.constructor.HeaderCalendarTemplate, {
date: date,
today: new Date(),
})
);
}
if (this.props.model.scale === 'day'){
return (
"<div>" +
datetime.toFormat("LLLL dd, yyyy") +
"</div>" +
renderToString(this.constructor.HeaderCalendarTemplate, {
date: date,
today: new Date(),
})
);
}
return "<div>" + datetime.toFormat("cccc") + "</div>";
}
});
return Object.assign(a, {
return {
...super.options,
eventOrder: function(event1, event2){
if (event1.extendedProps.worklocation){
return -1;
@@ -46,8 +20,8 @@ patch(AttendeeCalendarCommonRenderer.prototype, {
return event1.title.localeCompare(event2.title);
}
}
}
})
},
};
},
fcEventToRecord(event) {
const res = super.fcEventToRecord(...arguments);
@@ -133,7 +107,8 @@ patch(AttendeeCalendarCommonRenderer.prototype, {
return;
const content = renderToString(this.constructor.ButtonWorklocationTemplate, {})
const {children } = new DOMParser().parseFromString(content, "text/html").body
box.appendChild(...children)
box.appendChild(...children);
info.homework = true;
}
super.onDayRender(...arguments);
},
@@ -205,7 +180,7 @@ patch(AttendeeCalendarCommonRenderer.prototype, {
}
},
onDateClick(info){
if (info.jsEvent && info.jsEvent.target.tagName ==="BUTTON") {
if (info.jsEvent && info.jsEvent.target.closest(".btnWorklocation")) {
info.homework = true
this.props.createRecord(this.fcEventToRecord(info));
} else {
@@ -215,5 +190,5 @@ patch(AttendeeCalendarCommonRenderer.prototype, {
});
AttendeeCalendarCommonRenderer.WorklocationTemplate = "hr.homeworking.CalendarCommonRenderer.worklocation";
AttendeeCalendarCommonRenderer.HeaderCalendarTemplate = "hr.homeworking.CalendarCommonRenderer.buttonWorklocation";
AttendeeCalendarCommonRenderer.ButtonWorklocationTemplate = "hr.homeworking.CalendarCommonRenderer.buttonWorklocation";
AttendeeCalendarCommonRenderer.headerTemplate = "hr_homeworking.CalendarCommonRendererHeader";
@@ -31,6 +31,12 @@
</t>
</t>
<t t-name="hr_homeworking.CalendarCommonRendererHeader" t-inherit="web.CalendarCommonRendererHeader" t-inherit-mode="primary">
<xpath expr="//span[hasclass('o_cw_day_number')]" position="after">
<t t-if="scale != 'month'" t-call="hr.homeworking.CalendarCommonRenderer.buttonWorklocation"/>
</xpath>
</t>
<t t-name="hr.homeworking.CalendarCommonRenderer.buttonWorklocation">
<div class="btnWorklocation w-100">
<button class='rounded-pill o_month text-nowrap bg-info-light'>
@@ -83,6 +83,7 @@ export class CalendarCommonRenderer extends Component {
eventDrop: this.onEventDrop,
eventLimit: this.props.model.eventLimit,
eventLimitClick: this.onEventLimitClick,
eventLimitText: this.env.isSmall ? "" : "more",
eventMouseEnter: this.onEventMouseEnter,
eventMouseLeave: this.onEventMouseLeave,
eventRender: this.onEventRender,
@@ -113,6 +114,7 @@ export class CalendarCommonRenderer extends Component {
weekNumbers: true,
weekNumbersWithinDays: !this.env.isSmall,
windowResize: this.onWindowResizeDebounced,
columnHeaderHtml: this.getHeaderHtml,
};
}
@@ -175,7 +177,12 @@ export class CalendarCommonRenderer extends Component {
);
}
updateSize() {
const height = window.innerHeight - this.fc.el.getBoundingClientRect().top;
let headerHeight = 0;
if (!this.env.isSmall) {
headerHeight =
document.querySelector(".o_calendar_header")?.getBoundingClientRect()?.height ?? 0;
}
const height = window.innerHeight - this.fc.el.getBoundingClientRect().top - headerHeight;
this.fc.el.style.height = `${height}px`;
this.fc.api.updateSize();
}
@@ -205,6 +212,7 @@ export class CalendarCommonRenderer extends Component {
}
onEventClick(info) {
this.click(info);
info.el.classList.toggle("o_calendar_popover_open");
}
onEventRender(info) {
const { el, event } = info;
@@ -310,9 +318,25 @@ export class CalendarCommonRenderer extends Component {
onWindowResize() {
this.updateSize();
}
getHeaderHtml(date) {
const scale = this.props.model.scale;
const {
weekdayShort: weekdayShort,
weekdayLong: weekdayLong,
day,
} = DateTime.fromJSDate(date);
return renderToString(this.constructor.headerTemplate, {
weekdayShort,
weekdayLong,
day,
scale,
});
}
}
CalendarCommonRenderer.components = {
Popover: CalendarCommonPopover,
};
CalendarCommonRenderer.template = "web.CalendarCommonRenderer";
CalendarCommonRenderer.eventTemplate = "web.CalendarCommonRenderer.event";
CalendarCommonRenderer.headerTemplate = "web.CalendarCommonRendererHeader";
@@ -15,4 +15,8 @@
</div>
</t>
<t t-name="web.CalendarCommonRendererHeader">
<span class="o_cw_day_name" t-esc="scale == 'day' ? weekdayLong : weekdayShort "/> <span t-if="scale != 'month'" class="fc-day-number o_cw_day_number" t-esc="day"/>
</t>
</templates>
@@ -67,7 +67,7 @@ QUnit.module("CalendarView - CommonRenderer", ({ beforeEach }) => {
QUnit.test("Day: check date", async (assert) => {
await start({ model: { scale: "day" } });
assert.containsOnce(target, ".fc-day-header");
assert.strictEqual(target.querySelector(".fc-day-header").textContent, "July 16, 2021");
assert.strictEqual(target.querySelector(".fc-day-header").textContent, "Fri 16");
});
QUnit.test("Day: click all day slot", async (assert) => {
@@ -3994,7 +3994,7 @@ QUnit.module("Views", ({ beforeEach }) => {
);
assert.strictEqual(
target.querySelector(".o_calendar_renderer .fc-day-header").textContent,
"January 30, 2016",
"Sat 30",
"should display day passed in the context"
);
});
@@ -4030,12 +4030,12 @@ QUnit.module("Views", ({ beforeEach }) => {
const dayHeaders = target.querySelectorAll(".fc-day-header");
assert.strictEqual(
dayHeaders[0].textContent,
"Sunday",
"Sun 1",
"The first day of the week should be Sunday"
);
assert.strictEqual(
dayHeaders[dayHeaders.length - 1].textContent,
"Saturday",
"Sat 7",
"The last day of the week should be Saturday"
);
@@ -4085,12 +4085,12 @@ QUnit.module("Views", ({ beforeEach }) => {
const dayHeaders = target.querySelectorAll(".fc-day-header");
assert.strictEqual(
dayHeaders[0].textContent,
"Monday",
"Mon 26",
"The first day of the week should be Monday"
);
assert.strictEqual(
dayHeaders[dayHeaders.length - 1].textContent,
"Sunday",
"Sun 1",
"The last day of the week should be Sunday"
);