DEV: Allow the 'UpcomingEventsCalendar' Component to be used outside of the 'PostEventUpcomingEventsIndexRoute' route (#706)

* DEV: Allow the 'UpcomingEventsCalendar' Component to be used outside of the 'PostEventUpcomingEventsIndexRoute' route

* add system test to upcoming events

* improved addRecurrentEvents function

* fixed flaky test
This commit is contained in:
Juan David Martínez Cubillos 2025-03-25 10:31:57 -05:00 committed by GitHub
parent 9dd44e18de
commit 3d6b7ae482
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 141 additions and 96 deletions

View File

@ -16,7 +16,6 @@ export default class UpcomingEventsCalendar extends Component {
init() {
super.init(...arguments);
this._calendar = null;
}
@ -33,7 +32,7 @@ export default class UpcomingEventsCalendar extends Component {
this._renderCalendar();
}
_renderCalendar() {
async _renderCalendar() {
const siteSettings = this.site.siteSettings;
const calendarNode = document.getElementById("upcoming-events-calendar");
@ -43,88 +42,88 @@ export default class UpcomingEventsCalendar extends Component {
calendarNode.innerHTML = "";
this._loadCalendar().then(() => {
const fullCalendar = new window.FullCalendar.Calendar(calendarNode, {
...fullCalendarDefaultOptions(),
firstDay: 1,
height: "auto",
eventPositioned: (info) => {
if (siteSettings.events_max_rows === 0) {
return;
}
await this._loadCalendar();
let fcContent = info.el.querySelector(".fc-content");
let computedStyle = window.getComputedStyle(fcContent);
let lineHeight = parseInt(computedStyle.lineHeight, 10);
if (lineHeight === 0) {
lineHeight = 20;
}
let maxHeight = lineHeight * siteSettings.events_max_rows;
if (fcContent) {
fcContent.style.maxHeight = `${maxHeight}px`;
}
let fcTitle = info.el.querySelector(".fc-title");
if (fcTitle) {
fcTitle.style.overflow = "hidden";
fcTitle.style.whiteSpace = "pre-wrap";
}
fullCalendar.updateSize();
},
});
this._calendar = fullCalendar;
const tagsColorsMap = JSON.parse(siteSettings.map_events_to_color);
const originalEventAndRecurrents = addRecurrentEvents(this.events);
(originalEventAndRecurrents || []).forEach((event) => {
const { startsAt, endsAt, post, categoryId } = event;
let backgroundColor;
if (post.topic.tags) {
const tagColorEntry = tagsColorsMap.find(
(entry) =>
entry.type === "tag" && post.topic.tags.includes(entry.slug)
);
backgroundColor = tagColorEntry?.color;
const fullCalendar = new window.FullCalendar.Calendar(calendarNode, {
...fullCalendarDefaultOptions(),
firstDay: 1,
height: "auto",
eventPositioned: (info) => {
if (siteSettings.events_max_rows === 0) {
return;
}
if (!backgroundColor) {
const categoryColorEntry = tagsColorsMap.find(
(entry) =>
entry.type === "category" &&
entry.slug === post.topic.category_slug
);
backgroundColor = categoryColorEntry?.color;
let fcContent = info.el.querySelector(".fc-content");
let computedStyle = window.getComputedStyle(fcContent);
let lineHeight = parseInt(computedStyle.lineHeight, 10);
if (lineHeight === 0) {
lineHeight = 20;
}
let maxHeight = lineHeight * siteSettings.events_max_rows;
if (fcContent) {
fcContent.style.maxHeight = `${maxHeight}px`;
}
const categoryColor = Category.findById(categoryId)?.color;
if (!backgroundColor && categoryColor) {
backgroundColor = `#${categoryColor}`;
let fcTitle = info.el.querySelector(".fc-title");
if (fcTitle) {
fcTitle.style.overflow = "hidden";
fcTitle.style.whiteSpace = "pre-wrap";
}
let classNames;
if (moment(endsAt || startsAt).isBefore(moment())) {
classNames = "fc-past-event";
}
this._calendar.addEvent({
title: formatEventName(event),
start: startsAt,
end: endsAt || startsAt,
allDay: !isNotFullDayEvent(moment(startsAt), moment(endsAt)),
url: getURL(`/t/-/${post.topic.id}/${post.post_number}`),
backgroundColor,
classNames,
});
});
this._calendar.render();
fullCalendar.updateSize();
},
});
this._calendar = fullCalendar;
const tagsColorsMap = JSON.parse(siteSettings.map_events_to_color);
const resolvedEvents = await this.events;
const originalEventAndRecurrents = addRecurrentEvents(resolvedEvents);
(originalEventAndRecurrents || []).forEach((event) => {
const { startsAt, endsAt, post, categoryId } = event;
let backgroundColor;
if (post.topic.tags) {
const tagColorEntry = tagsColorsMap.find(
(entry) =>
entry.type === "tag" && post.topic.tags.includes(entry.slug)
);
backgroundColor = tagColorEntry?.color;
}
if (!backgroundColor) {
const categoryColorEntry = tagsColorsMap.find(
(entry) =>
entry.type === "category" && entry.slug === post.topic.category_slug
);
backgroundColor = categoryColorEntry?.color;
}
const categoryColor = Category.findById(categoryId)?.color;
if (!backgroundColor && categoryColor) {
backgroundColor = `#${categoryColor}`;
}
let classNames;
if (moment(endsAt || startsAt).isBefore(moment())) {
classNames = "fc-past-event";
}
this._calendar.addEvent({
title: formatEventName(event),
start: startsAt,
end: endsAt || startsAt,
allDay: !isNotFullDayEvent(moment(startsAt), moment(endsAt)),
url: getURL(`/t/-/${post.topic.id}/${post.post_number}`),
backgroundColor,
classNames,
});
});
this._calendar.render();
}
_loadCalendar() {

View File

@ -175,7 +175,8 @@ function initializeDiscourseCalendar(api) {
);
const events = await discoursePostEventApiService.events(params);
addRecurrentEvents(events).forEach((event) => {
const recurrentEvents = addRecurrentEvents(events);
recurrentEvents.forEach((event) => {
const { startsAt, endsAt, post, categoryId } = event;
let backgroundColor;

View File

@ -1,18 +1,24 @@
/* eslint-disable no-console */
import DiscoursePostEventEvent from "../models/discourse-post-event-event";
export default function addRecurrentEvents(events) {
return events.flatMap((event) => {
const upcomingEvents =
event.upcomingDates?.map((upcomingDate) =>
DiscoursePostEventEvent.create({
name: event.name,
post: event.post,
category_id: event.categoryId,
starts_at: upcomingDate.starts_at,
ends_at: upcomingDate.ends_at,
})
) || [];
try {
return events.flatMap((event) => {
const upcomingEvents =
event.upcomingDates?.map((upcomingDate) =>
DiscoursePostEventEvent.create({
name: event.name,
post: event.post,
category_id: event.categoryId,
starts_at: upcomingDate.starts_at,
ends_at: upcomingDate.ends_at,
})
) || [];
return [event, ...upcomingEvents];
});
return [event, ...upcomingEvents];
});
} catch (error) {
console.error("Failed to retrieve events:", error);
return [];
}
}

View File

@ -5,6 +5,7 @@ import DiscourseRoute from "discourse/routes/discourse";
export default class PostEventUpcomingEventsIndexRoute extends DiscourseRoute {
@service discoursePostEventApi;
@service discoursePostEventService;
@action
activate() {
@ -14,10 +15,6 @@ export default class PostEventUpcomingEventsIndexRoute extends DiscourseRoute {
}
async model(params) {
if (this.siteSettings.include_expired_events_on_calendar) {
params.include_expired = true;
}
return await this.discoursePostEventApi.events(params);
return await this.discoursePostEventService.fetchEvents(params);
}
}

View File

@ -0,0 +1,14 @@
import Service, { service } from "@ember/service";
export default class DiscoursePostEventService extends Service {
@service siteSettings;
@service discoursePostEventApi;
async fetchEvents(params = {}) {
if (this.siteSettings.include_expired_events_on_calendar) {
params.include_expired = true;
}
const events = await this.discoursePostEventApi.events(params);
return await events;
}
}

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
describe "Upcoming Events", type: :system do
fab!(:admin)
fab!(:user)
fab!(:category)
fab!(:event)
let(:composer) { PageObjects::Components::Composer.new }
let(:topic_page) { PageObjects::Pages::Topic.new }
before do
SiteSetting.calendar_enabled = true
SiteSetting.discourse_post_event_enabled = true
sign_in(admin)
end
context "when user is signed in" do
before { sign_in(admin) }
it "shows the upcoming events" do
visit("/upcoming-events")
expect(page).to have_css("#upcoming-events-calendar")
calendar = find("#upcoming-events-calendar")
expect(calendar).to have_css(".fc-event-container")
end
end
end