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:
		
							parent
							
								
									9dd44e18de
								
							
						
					
					
						commit
						3d6b7ae482
					
				| 
						 | 
				
			
			@ -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() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 [];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
		Loading…
	
		Reference in New Issue