FEATURE: Display calendar events adjusted for timezones (#432)
Adds the option to enable a timezone adjustment for calendar events. This will make it so events render offset from the grid to reflect the appropriate start and end moments according to the viewer's timezone.
This commit is contained in:
parent
776cc411cd
commit
3fa341639b
|
|
@ -236,12 +236,18 @@ function initializeDiscourseCalendar(api) {
|
|||
calendar.render();
|
||||
_setStaticCalendarEvents(calendar, $calendar, post);
|
||||
} else {
|
||||
_setDynamicCalendarEvents(calendar, post, fullDay);
|
||||
_setDynamicCalendarEvents(calendar, post, fullDay, timezone);
|
||||
calendar.render();
|
||||
_setDynamicCalendarOptions(calendar, $calendar);
|
||||
}
|
||||
|
||||
_setupTimezonePicker(calendar, timezone);
|
||||
const resetDynamicEvents = () => {
|
||||
const selectedTimezone = calendar.getOption("timeZone");
|
||||
calendar.getEvents().forEach((event) => event.remove());
|
||||
_setDynamicCalendarEvents(calendar, post, fullDay, selectedTimezone);
|
||||
};
|
||||
|
||||
_setupTimezonePicker(calendar, timezone, resetDynamicEvents);
|
||||
}
|
||||
|
||||
function attachCalendar($elem, helper) {
|
||||
|
|
@ -293,6 +299,7 @@ function initializeDiscourseCalendar(api) {
|
|||
center: "title",
|
||||
right: "month,basicWeek,listNextYear",
|
||||
},
|
||||
eventOrder: ["start", _orderByTz, "-duration", "allDay", "title"],
|
||||
datesRender: (info) => {
|
||||
if (showAddToCalendar) {
|
||||
_insertAddToCalendarLinks(info);
|
||||
|
|
@ -300,9 +307,24 @@ function initializeDiscourseCalendar(api) {
|
|||
|
||||
$calendarTitle.innerText = info.view.title;
|
||||
},
|
||||
|
||||
eventPositioned: (info) => {
|
||||
_setTimezoneOffset(info);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function _orderByTz(a, b) {
|
||||
if (!siteSettings.enable_timezone_offset_for_calendar_events) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const offsetA = a.extendedProps.timezoneOffset;
|
||||
const offsetB = b.extendedProps.timezoneOffset;
|
||||
|
||||
return offsetA === offsetB ? 0 : offsetA < offsetB ? -1 : 1;
|
||||
}
|
||||
|
||||
function _convertHtmlToDate(html) {
|
||||
const date = html.attr("data-date");
|
||||
|
||||
|
|
@ -325,19 +347,26 @@ function initializeDiscourseCalendar(api) {
|
|||
|
||||
function _buildEventObject(from, to) {
|
||||
const hasTimeSpecified = (d) => {
|
||||
if (!d) {
|
||||
return false;
|
||||
}
|
||||
return d.hours() !== 0 || d.minutes() !== 0 || d.seconds() !== 0;
|
||||
};
|
||||
|
||||
const hasTime =
|
||||
hasTimeSpecified(to?.dateTime) || hasTimeSpecified(from?.dateTime);
|
||||
const dateFormat = hasTime ? "YYYY-MM-DD HH:mm:ss Z" : "YYYY-MM-DD";
|
||||
|
||||
let event = {
|
||||
start: from.dateTime.toDate(),
|
||||
start: from.dateTime.format(dateFormat),
|
||||
allDay: false,
|
||||
};
|
||||
|
||||
if (to) {
|
||||
if (hasTimeSpecified(to.dateTime) || hasTimeSpecified(from.dateTime)) {
|
||||
event.end = to.dateTime.toDate();
|
||||
if (hasTime) {
|
||||
event.end = to.dateTime.format(dateFormat);
|
||||
} else {
|
||||
event.end = to.dateTime.add(1, "days").toDate();
|
||||
event.end = to.dateTime.add(1, "days").format(dateFormat);
|
||||
event.allDay = true;
|
||||
}
|
||||
} else {
|
||||
|
|
@ -478,6 +507,10 @@ function initializeDiscourseCalendar(api) {
|
|||
event.classNames = ["holiday"];
|
||||
}
|
||||
|
||||
if (detail.timezoneOffset) {
|
||||
event.extendedProps.timezoneOffset = detail.timezoneOffset;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
|
|
@ -514,50 +547,143 @@ function initializeDiscourseCalendar(api) {
|
|||
calendar.addEvent(event);
|
||||
}
|
||||
|
||||
function _addGroupedEvent(calendar, post, detail) {
|
||||
let htmlContent = "";
|
||||
let usernames = [];
|
||||
let localEventNames = [];
|
||||
function _addGroupedEvent(calendar, post, detail, fullDay, calendarTz) {
|
||||
const groupedEventData =
|
||||
siteSettings.enable_timezone_offset_for_calendar_events && fullDay
|
||||
? _splitGroupEventByTimezone(detail, calendarTz)
|
||||
: [detail];
|
||||
|
||||
Object.keys(detail.localEvents)
|
||||
.sort()
|
||||
.forEach((key) => {
|
||||
const localEvent = detail.localEvents[key];
|
||||
htmlContent += `<b>${key}</b>: ${localEvent.usernames
|
||||
.sort()
|
||||
.join(", ")}<br>`;
|
||||
usernames = usernames.concat(localEvent.usernames);
|
||||
localEventNames.push(key);
|
||||
});
|
||||
groupedEventData.forEach((eventData) => {
|
||||
let htmlContent = "";
|
||||
let users = [];
|
||||
let localEventNames = [];
|
||||
|
||||
const event = _buildEvent(detail);
|
||||
event.classNames = ["grouped-event"];
|
||||
Object.keys(eventData.localEvents)
|
||||
.sort()
|
||||
.forEach((key) => {
|
||||
const localEvent = eventData.localEvents[key];
|
||||
htmlContent += `<b>${key}</b>: ${localEvent.users
|
||||
.map((u) => u.username)
|
||||
.sort()
|
||||
.join(", ")}<br>`;
|
||||
users = users.concat(localEvent.users);
|
||||
localEventNames.push(key);
|
||||
});
|
||||
|
||||
if (usernames.length > 2) {
|
||||
event.title = `(${usernames.length}) ${localEventNames[0]}`;
|
||||
} else if (usernames.length === 1) {
|
||||
event.title = usernames[0];
|
||||
} else {
|
||||
event.title = isMobileView
|
||||
? `(${usernames.length}) ${localEventNames[0]}`
|
||||
: `(${usernames.length}) ` + usernames.join(", ");
|
||||
}
|
||||
const event = _buildEvent(eventData);
|
||||
event.classNames = ["grouped-event"];
|
||||
|
||||
if (localEventNames.length > 1) {
|
||||
event.extendedProps.htmlContent = htmlContent;
|
||||
} else {
|
||||
if (usernames.length > 1) {
|
||||
if (users.length > 2) {
|
||||
event.title = `(${users.length}) ${localEventNames[0]}`;
|
||||
} else if (users.length === 1) {
|
||||
event.title = users[0].username;
|
||||
} else {
|
||||
event.title = isMobileView
|
||||
? `(${users.length}) ${localEventNames[0]}`
|
||||
: `(${users.length}) ` + users.map((u) => u.username).join(", ");
|
||||
}
|
||||
|
||||
if (localEventNames.length > 1) {
|
||||
event.extendedProps.htmlContent = htmlContent;
|
||||
} else {
|
||||
event.extendedProps.htmlContent = localEventNames[0];
|
||||
if (users.length > 1) {
|
||||
event.extendedProps.htmlContent = htmlContent;
|
||||
} else {
|
||||
event.extendedProps.htmlContent = localEventNames[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
calendar.addEvent(event);
|
||||
calendar.addEvent(event);
|
||||
});
|
||||
}
|
||||
|
||||
function _setDynamicCalendarEvents(calendar, post, fullDay) {
|
||||
function _splitGroupEventByTimezone(detail, calendarTz) {
|
||||
const calendarUtcOffset = moment.tz(calendarTz).utcOffset();
|
||||
let timezonesOffsets = [];
|
||||
let splittedEvents = [];
|
||||
|
||||
Object.values(detail.localEvents).forEach((event) => {
|
||||
event.users.forEach((user) => {
|
||||
const userUtcOffset = moment.tz(user.timezone).utcOffset();
|
||||
const timezoneOffset = (calendarUtcOffset - userUtcOffset) / 60;
|
||||
user.timezoneOffset = timezoneOffset;
|
||||
timezonesOffsets.push(timezoneOffset);
|
||||
});
|
||||
});
|
||||
|
||||
[...new Set(timezonesOffsets)].forEach((offset, i) => {
|
||||
let filteredLocalEvents = {};
|
||||
let eventTimezones = [];
|
||||
|
||||
Object.keys(detail.localEvents).forEach((key) => {
|
||||
const threshold =
|
||||
siteSettings.split_grouped_events_by_timezone_threshold;
|
||||
|
||||
const filtered = detail.localEvents[key].users.filter(
|
||||
(u) =>
|
||||
Math.abs(u.timezoneOffset - (offset + threshold * i)) <= threshold
|
||||
);
|
||||
if (filtered.length > 0) {
|
||||
filteredLocalEvents[key] = {
|
||||
users: filtered,
|
||||
};
|
||||
filtered.forEach((u) => {
|
||||
detail.localEvents[key].users.splice(
|
||||
detail.localEvents[key].users.findIndex(
|
||||
(e) => e.username === u.username
|
||||
),
|
||||
1
|
||||
);
|
||||
if (
|
||||
!eventTimezones.find((t) => t.timezoneOffset === u.timezoneOffset)
|
||||
) {
|
||||
eventTimezones.push({
|
||||
timezone: u.timezone,
|
||||
timezoneOffset: u.timezoneOffset,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(filteredLocalEvents).length > 0) {
|
||||
const eventTimezone = _findAverageTimezone(eventTimezones);
|
||||
|
||||
let from = moment.tz(detail.from, eventTimezone.timezone);
|
||||
let to = moment.tz(detail.to, eventTimezone.timezone);
|
||||
|
||||
_modifyDatesForTimezoneOffset(from, to, eventTimezone.timezoneOffset);
|
||||
|
||||
splittedEvents.push({
|
||||
timezoneOffset: eventTimezone.timezoneOffset,
|
||||
localEvents: filteredLocalEvents,
|
||||
from: from.format("YYYY-MM-DD"),
|
||||
to: to.format("YYYY-MM-DD"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return splittedEvents;
|
||||
}
|
||||
|
||||
function _findAverageTimezone(eventTimezones) {
|
||||
const totalOffsets = eventTimezones.reduce(
|
||||
(sum, timezone) => sum + timezone.timezoneOffset,
|
||||
0
|
||||
);
|
||||
const averageOffset = totalOffsets / eventTimezones.length;
|
||||
|
||||
return eventTimezones.reduce((closest, timezone) => {
|
||||
const difference = Math.abs(timezone.timezoneOffset - averageOffset);
|
||||
return difference < Math.abs(closest.timezoneOffset - averageOffset)
|
||||
? timezone
|
||||
: closest;
|
||||
});
|
||||
}
|
||||
|
||||
function _setDynamicCalendarEvents(calendar, post, fullDay, calendarTz) {
|
||||
const groupedEvents = [];
|
||||
const calendarUtcOffset = moment.tz(calendarTz).utcOffset();
|
||||
|
||||
(post.calendar_details || []).forEach((detail) => {
|
||||
switch (detail.type) {
|
||||
|
|
@ -566,14 +692,24 @@ function initializeDiscourseCalendar(api) {
|
|||
break;
|
||||
case "standalone":
|
||||
if (fullDay && detail.timezone) {
|
||||
detail.from = moment
|
||||
.tz(detail.from, detail.timezone)
|
||||
.format("YYYY-MM-DD");
|
||||
detail.to = moment
|
||||
.tz(detail.to, detail.timezone)
|
||||
.format("YYYY-MM-DD");
|
||||
const eventDetail = { ...detail };
|
||||
let from = moment.tz(detail.from, detail.timezone);
|
||||
let to = moment.tz(detail.to, detail.timezone);
|
||||
|
||||
if (siteSettings.enable_timezone_offset_for_calendar_events) {
|
||||
const eventUtcOffset = moment.tz(detail.timezone).utcOffset();
|
||||
const timezoneOffset = (calendarUtcOffset - eventUtcOffset) / 60;
|
||||
eventDetail.timezoneOffset = timezoneOffset;
|
||||
|
||||
_modifyDatesForTimezoneOffset(from, to, timezoneOffset);
|
||||
}
|
||||
eventDetail.from = from.format("YYYY-MM-DD");
|
||||
eventDetail.to = to.format("YYYY-MM-DD");
|
||||
|
||||
_addStandaloneEvent(calendar, post, eventDetail);
|
||||
} else {
|
||||
_addStandaloneEvent(calendar, post, detail);
|
||||
}
|
||||
_addStandaloneEvent(calendar, post, detail);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
|
@ -601,24 +737,44 @@ function initializeDiscourseCalendar(api) {
|
|||
|
||||
formattedGroupedEvents[identifier].localEvents[groupedEvent.name] =
|
||||
formattedGroupedEvents[identifier].localEvents[groupedEvent.name] || {
|
||||
usernames: [],
|
||||
users: [],
|
||||
};
|
||||
|
||||
formattedGroupedEvents[identifier].localEvents[
|
||||
groupedEvent.name
|
||||
].usernames.push.apply(
|
||||
formattedGroupedEvents[identifier].localEvents[groupedEvent.name]
|
||||
.usernames,
|
||||
groupedEvent.usernames
|
||||
].users.push.apply(
|
||||
formattedGroupedEvents[identifier].localEvents[groupedEvent.name].users,
|
||||
groupedEvent.users
|
||||
);
|
||||
});
|
||||
|
||||
Object.keys(formattedGroupedEvents).forEach((key) => {
|
||||
const formattedGroupedEvent = formattedGroupedEvents[key];
|
||||
_addGroupedEvent(calendar, post, formattedGroupedEvent);
|
||||
_addGroupedEvent(
|
||||
calendar,
|
||||
post,
|
||||
formattedGroupedEvent,
|
||||
fullDay,
|
||||
calendarTz
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function _modifyDatesForTimezoneOffset(from, to, timezoneOffset) {
|
||||
if (timezoneOffset > 0) {
|
||||
if (to.isValid()) {
|
||||
to.add(1, "day");
|
||||
} else {
|
||||
to = from.clone().add(1, "day");
|
||||
}
|
||||
} else if (timezoneOffset < 0) {
|
||||
if (!to.isValid()) {
|
||||
to = from.clone();
|
||||
}
|
||||
from.subtract(1, "day");
|
||||
}
|
||||
}
|
||||
|
||||
function _getTimeZone($calendar, currentUser) {
|
||||
let defaultTimezone = $calendar.attr("data-calendar-default-timezone");
|
||||
const isValidDefaultTimezone = !!moment.tz.zone(defaultTimezone);
|
||||
|
|
@ -629,19 +785,23 @@ function initializeDiscourseCalendar(api) {
|
|||
return defaultTimezone || currentUser?.timezone || moment.tz.guess();
|
||||
}
|
||||
|
||||
function _setupTimezonePicker(calendar, timezone) {
|
||||
function _setupTimezonePicker(calendar, timezone, resetDynamicEvents) {
|
||||
const tzPicker = document.querySelector(
|
||||
".discourse-calendar-timezone-picker"
|
||||
);
|
||||
if (tzPicker) {
|
||||
tzPicker.addEventListener("change", function (event) {
|
||||
calendar.setOption("timeZone", event.target.value);
|
||||
resetDynamicEvents();
|
||||
_insertAddToCalendarLinks(calendar);
|
||||
});
|
||||
|
||||
moment.tz.names().forEach((tz) => {
|
||||
tzPicker.appendChild(new Option(tz, tz));
|
||||
});
|
||||
moment.tz
|
||||
.names()
|
||||
.filter((t) => !t.startsWith("Etc/GMT"))
|
||||
.forEach((tz) => {
|
||||
tzPicker.appendChild(new Option(tz, tz));
|
||||
});
|
||||
|
||||
tzPicker.value = timezone;
|
||||
} else {
|
||||
|
|
@ -663,6 +823,63 @@ function initializeDiscourseCalendar(api) {
|
|||
}
|
||||
}
|
||||
|
||||
function _setTimezoneOffset(info) {
|
||||
if (
|
||||
!siteSettings.enable_timezone_offset_for_calendar_events ||
|
||||
info.view.type === "listNextYear"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The timezone offset works by calculating the hour difference
|
||||
// between a target event and the calendar event. This is used to
|
||||
// determine whether to add an extra day before or after the event.
|
||||
// Then, it applies inline styling to resize the event to its
|
||||
// original size while adjusting it to the respective timezone.
|
||||
|
||||
const timezoneOffset = info.event.extendedProps.timezoneOffset;
|
||||
const segmentDuration = info.el.parentNode?.colSpan;
|
||||
|
||||
const basePctOffset = 100 / segmentDuration;
|
||||
// Base margin required to shrink down the event by one day
|
||||
const basePxOffset = 5.5 - segmentDuration;
|
||||
// Default space between two consecutive events
|
||||
// 5.5px = ( ( ( 2px margin + 3px padding ) * 2 ) + 1px border ) / 2
|
||||
|
||||
// K factors are used to adjust each side of the event based on the hour difference
|
||||
// A '2' is added to the pxOffset to account for the default margin
|
||||
|
||||
if (timezoneOffset > 0) {
|
||||
// When the event extends into the next day
|
||||
if (info.isStart) {
|
||||
const leftK = Math.abs(timezoneOffset) / 24;
|
||||
const pctOffset = `${basePctOffset * leftK}%`;
|
||||
const pxOffset = `${basePxOffset * leftK + 2}px`;
|
||||
info.el.style.marginLeft = `calc(${pctOffset} + ${pxOffset})`;
|
||||
}
|
||||
if (info.isEnd) {
|
||||
const rightK = (24 - Math.abs(timezoneOffset)) / 24;
|
||||
const pctOffset = `${basePctOffset * rightK}%`;
|
||||
const pxOffset = `${basePxOffset * rightK + 2}px`;
|
||||
info.el.style.marginRight = `calc(${pctOffset} + ${pxOffset})`;
|
||||
}
|
||||
} else if (timezoneOffset < 0) {
|
||||
// When the event starts on the previous day
|
||||
if (info.isStart) {
|
||||
const leftK = (24 - Math.abs(timezoneOffset)) / 24;
|
||||
const pctOffset = `${basePctOffset * leftK}%`;
|
||||
const pxOffset = `${basePxOffset * leftK + 2}px`;
|
||||
info.el.style.marginLeft = `calc(${pctOffset} + ${pxOffset})`;
|
||||
}
|
||||
if (info.isEnd) {
|
||||
const rightK = Math.abs(timezoneOffset) / 24;
|
||||
const pctOffset = `${basePctOffset * rightK}%`;
|
||||
const pxOffset = `${basePxOffset * rightK + 2}px`;
|
||||
info.el.style.marginRight = `calc(${pctOffset} + ${pxOffset})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _insertAddToCalendarLinkForEvent(event, eventSegmentDefMap) {
|
||||
const eventTitle = event.eventRange.def.title;
|
||||
let map = eventSegmentDefMap[event.eventRange.def.defId];
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.fc-event-container {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.fc-widget-header span {
|
||||
padding: 3px 3px 3px 0.5em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,14 @@ discourse_calendar:
|
|||
sidebar_show_upcoming_events:
|
||||
default: true
|
||||
client: true
|
||||
enable_timezone_offset_for_calendar_events:
|
||||
default: false
|
||||
client: true
|
||||
hidden: true
|
||||
split_grouped_events_by_timezone_threshold:
|
||||
default: 0
|
||||
client: true
|
||||
hidden: true
|
||||
|
||||
discourse_post_event:
|
||||
discourse_post_event_enabled:
|
||||
|
|
|
|||
17
plugin.rb
17
plugin.rb
|
|
@ -363,22 +363,23 @@ after_initialize do
|
|||
else
|
||||
identifier = "#{event.region.split("_").first}-#{event.start_date.strftime("%j")}"
|
||||
|
||||
grouped[identifier] ||= {
|
||||
type: :grouped,
|
||||
from: event.start_date,
|
||||
name: [],
|
||||
usernames: [],
|
||||
}
|
||||
grouped[identifier] ||= { type: :grouped, from: event.start_date, name: [], users: [] }
|
||||
|
||||
user = User.find_by_username(event.username)
|
||||
|
||||
grouped[identifier][:name] << event.description
|
||||
grouped[identifier][:usernames] << event.username
|
||||
grouped[identifier][:users] << {
|
||||
username: event.username,
|
||||
timezone: user.present? ? user.user_option.timezone : nil,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
grouped.each do |_, v|
|
||||
v[:name].sort!.uniq!
|
||||
v[:name] = v[:name].join(", ")
|
||||
v[:usernames].sort!.uniq!
|
||||
v[:users].sort! { |a, b| a[:username] <=> b[:username] }
|
||||
v[:users].uniq! { |u| u[:username] }
|
||||
end
|
||||
|
||||
standalones + grouped.values
|
||||
|
|
|
|||
|
|
@ -35,9 +35,11 @@ describe PostSerializer do
|
|||
it "groups calendar events correctly" do
|
||||
user = Fabricate(:user)
|
||||
user.upsert_custom_fields(::DiscourseCalendar::REGION_CUSTOM_FIELD => "ar")
|
||||
user.user_option.update!(timezone: "America/Buenos_Aires")
|
||||
|
||||
user2 = Fabricate(:user)
|
||||
user2.upsert_custom_fields(::DiscourseCalendar::REGION_CUSTOM_FIELD => "ar")
|
||||
user2.user_option.update!(timezone: "America/Buenos_Aires")
|
||||
|
||||
post = create_post(raw: "[calendar]\n[/calendar]")
|
||||
SiteSetting.holiday_calendar_topic_id = post.topic.id
|
||||
|
|
@ -52,8 +54,11 @@ describe PostSerializer do
|
|||
"Feriado puente turístico",
|
||||
"Día de la Independencia",
|
||||
)
|
||||
expect(json[:post][:calendar_details].map { |x| x[:usernames] }).to all (
|
||||
contain_exactly(user.username, user2.username)
|
||||
expect(json[:post][:calendar_details].map { |x| x[:users] }).to all (
|
||||
contain_exactly(
|
||||
{ username: user.username, timezone: "America/Buenos_Aires" },
|
||||
{ username: user2.username, timezone: "America/Buenos_Aires" },
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,363 @@
|
|||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
|
||||
const topicResponse = {
|
||||
post_stream: {
|
||||
posts: [
|
||||
{
|
||||
id: 375,
|
||||
name: null,
|
||||
username: "jan",
|
||||
avatar_template: "/letter_avatar_proxy/v4/letter/j/ce7236/{size}.png",
|
||||
created_at: "2023-09-08T16:50:07.638Z",
|
||||
raw: '[calendar weekends=true tzPicker="true" fullDay="true" showAddToCalendar="false" defaultTimezone="Europe/Lisbon"] [/calendar]',
|
||||
cooked:
|
||||
'\u003cdiv class="discourse-calendar-wrap"\u003e\n\u003cdiv class="discourse-calendar-header"\u003e\n\u003ch2 class="discourse-calendar-title"\u003e\u003c/h2\u003e\n\u003cspan class="discourse-calendar-timezone-wrap"\u003e\n\u003cselect class="discourse-calendar-timezone-picker"\u003e\u003c/select\u003e\n\u003c/span\u003e\n\u003c/div\u003e\n\u003cdiv class="calendar" data-calendar-type="dynamic" data-calendar-default-timezone="Europe/Lisbon" data-weekends="true" data-calendar-show-add-to-calendar="false" data-calendar-full-day="true"\u003e\u003c/div\u003e\n\u003c/div\u003e',
|
||||
post_number: 1,
|
||||
post_type: 1,
|
||||
updated_at: "2023-09-08T16:50:07.638Z",
|
||||
reply_count: 0,
|
||||
reply_to_post_number: null,
|
||||
quote_count: 0,
|
||||
incoming_link_count: 2,
|
||||
reads: 1,
|
||||
readers_count: 0,
|
||||
score: 10.2,
|
||||
yours: true,
|
||||
topic_id: 252,
|
||||
topic_slug: "awesome-calendar",
|
||||
display_username: null,
|
||||
primary_group_name: null,
|
||||
flair_name: null,
|
||||
flair_url: null,
|
||||
flair_bg_color: null,
|
||||
flair_color: null,
|
||||
flair_group_id: null,
|
||||
version: 1,
|
||||
can_edit: true,
|
||||
can_delete: false,
|
||||
can_recover: false,
|
||||
can_see_hidden_post: true,
|
||||
can_wiki: true,
|
||||
read: true,
|
||||
user_title: null,
|
||||
bookmarked: false,
|
||||
actions_summary: [
|
||||
{ id: 3, can_act: true },
|
||||
{ id: 4, can_act: true },
|
||||
{ id: 8, can_act: true },
|
||||
{ id: 7, can_act: true },
|
||||
],
|
||||
moderator: false,
|
||||
admin: true,
|
||||
staff: true,
|
||||
user_id: 1,
|
||||
hidden: false,
|
||||
trust_level: 1,
|
||||
deleted_at: null,
|
||||
user_deleted: false,
|
||||
edit_reason: null,
|
||||
can_view_edit_history: true,
|
||||
wiki: false,
|
||||
reviewable_id: 0,
|
||||
reviewable_score_count: 0,
|
||||
reviewable_score_pending_count: 0,
|
||||
mentioned_users: [],
|
||||
calendar_details: [
|
||||
{
|
||||
type: "standalone",
|
||||
post_number: 2,
|
||||
message: "Cordoba",
|
||||
from: "2023-09-14T00:00:00.000Z",
|
||||
to: "2023-09-14T00:00:00.000Z",
|
||||
username: "jan",
|
||||
recurring: null,
|
||||
post_url: "/t/-/252/2",
|
||||
timezone: "America/Cordoba",
|
||||
},
|
||||
{
|
||||
type: "standalone",
|
||||
post_number: 4,
|
||||
message: "Moscow",
|
||||
from: "2023-09-17T00:00:00.000Z",
|
||||
to: "2023-09-18T00:00:00.000Z",
|
||||
username: "jan",
|
||||
recurring: null,
|
||||
post_url: "/t/-/252/3",
|
||||
timezone: "Europe/Moscow",
|
||||
},
|
||||
{
|
||||
type: "standalone",
|
||||
post_number: 3,
|
||||
message: "Tokyo",
|
||||
from: "2023-09-20T00:00:00.000Z",
|
||||
to: "2023-09-21T00:00:00.000Z",
|
||||
username: "jan",
|
||||
recurring: null,
|
||||
post_url: "/t/-/252/4",
|
||||
timezone: "Asia/Tokyo",
|
||||
},
|
||||
{
|
||||
type: "standalone",
|
||||
post_number: 5,
|
||||
message: "Lisbon",
|
||||
from: "2023-09-28T00:00:00.000Z",
|
||||
to: "2023-09-28T00:00:00.000Z",
|
||||
username: "jan",
|
||||
recurring: null,
|
||||
post_url: "/t/-/252/5",
|
||||
timezone: "Europe/Lisbon",
|
||||
},
|
||||
{
|
||||
type: "grouped",
|
||||
from: "2023-09-04T05:00:00.000Z",
|
||||
name: "Labor Day",
|
||||
users: [
|
||||
{
|
||||
username: "gmt-5_user",
|
||||
timezone: "America/Chicago",
|
||||
},
|
||||
{
|
||||
username: "gmt-6_user",
|
||||
timezone: "America/Denver",
|
||||
},
|
||||
{
|
||||
username: "gmt-7_user",
|
||||
timezone: "America/Los_Angeles",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
timeline_lookup: [[1, 0]],
|
||||
tags: [],
|
||||
tags_descriptions: {},
|
||||
id: 252,
|
||||
title: "Awesome Calendar",
|
||||
fancy_title: "Awesome Calendar",
|
||||
posts_count: 5,
|
||||
created_at: "2023-09-08T16:50:07.371Z",
|
||||
views: 1,
|
||||
reply_count: 0,
|
||||
like_count: 0,
|
||||
last_posted_at: "2023-09-08T16:50:52.936Z",
|
||||
visible: true,
|
||||
closed: false,
|
||||
archived: false,
|
||||
has_summary: false,
|
||||
archetype: "regular",
|
||||
slug: "awesome-calendar",
|
||||
category_id: 5,
|
||||
word_count: 56,
|
||||
deleted_at: null,
|
||||
user_id: 1,
|
||||
featured_link: null,
|
||||
pinned_globally: false,
|
||||
pinned_at: null,
|
||||
pinned_until: null,
|
||||
image_url: null,
|
||||
slow_mode_seconds: 0,
|
||||
draft: null,
|
||||
draft_key: "topic_252",
|
||||
draft_sequence: 9,
|
||||
posted: true,
|
||||
unpinned: null,
|
||||
pinned: false,
|
||||
current_post_number: 1,
|
||||
highest_post_number: 4,
|
||||
last_read_post_number: 4,
|
||||
last_read_post_id: 378,
|
||||
deleted_by: null,
|
||||
has_deleted: false,
|
||||
actions_summary: [
|
||||
{ id: 4, count: 0, hidden: false, can_act: true },
|
||||
{ id: 8, count: 0, hidden: false, can_act: true },
|
||||
{ id: 7, count: 0, hidden: false, can_act: true },
|
||||
],
|
||||
chunk_size: 20,
|
||||
bookmarked: false,
|
||||
bookmarks: [],
|
||||
topic_timer: null,
|
||||
message_bus_last_id: 16,
|
||||
participant_count: 1,
|
||||
show_read_indicator: false,
|
||||
thumbnails: null,
|
||||
slow_mode_enabled_until: null,
|
||||
summarizable: false,
|
||||
details: {
|
||||
can_edit: true,
|
||||
notification_level: 3,
|
||||
notifications_reason_id: 1,
|
||||
can_move_posts: true,
|
||||
can_delete: true,
|
||||
can_remove_allowed_users: true,
|
||||
can_invite_to: true,
|
||||
can_invite_via_email: true,
|
||||
can_create_post: true,
|
||||
can_reply_as_new_topic: true,
|
||||
can_flag_topic: true,
|
||||
can_convert_topic: true,
|
||||
can_review_topic: true,
|
||||
can_close_topic: true,
|
||||
can_archive_topic: true,
|
||||
can_split_merge_topic: true,
|
||||
can_edit_staff_notes: true,
|
||||
can_toggle_topic_visibility: true,
|
||||
can_pin_unpin_topic: true,
|
||||
can_moderate_category: true,
|
||||
can_remove_self_id: 1,
|
||||
participants: [
|
||||
{
|
||||
id: 1,
|
||||
username: "jan",
|
||||
name: null,
|
||||
avatar_template: "/letter_avatar_proxy/v4/letter/j/ce7236/{size}.png",
|
||||
post_count: 4,
|
||||
primary_group_name: null,
|
||||
flair_name: null,
|
||||
flair_url: null,
|
||||
flair_color: null,
|
||||
flair_bg_color: null,
|
||||
flair_group_id: null,
|
||||
admin: true,
|
||||
trust_level: 1,
|
||||
},
|
||||
],
|
||||
created_by: {
|
||||
id: 1,
|
||||
username: "jan",
|
||||
name: null,
|
||||
avatar_template: "/letter_avatar_proxy/v4/letter/j/ce7236/{size}.png",
|
||||
},
|
||||
last_poster: {
|
||||
id: 1,
|
||||
username: "jan",
|
||||
name: null,
|
||||
avatar_template: "/letter_avatar_proxy/v4/letter/j/ce7236/{size}.png",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function getEventByText(text) {
|
||||
const events = [...document.querySelectorAll(".fc-day-grid-event")].filter(
|
||||
(event) => event.textContent.includes(text)
|
||||
);
|
||||
return events.length === 1 ? events[0] : events;
|
||||
}
|
||||
|
||||
function getRoundedPct(marginString) {
|
||||
return Math.round(marginString.match(/(\d+(\.\d+)?)%/)[1]);
|
||||
}
|
||||
|
||||
acceptance("Discourse Calendar - Timezone Offset", function (needs) {
|
||||
needs.settings({
|
||||
calendar_enabled: true,
|
||||
enable_timezone_offset_for_calendar_events: true,
|
||||
});
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/t/252.json", () => {
|
||||
return helper.response(topicResponse);
|
||||
});
|
||||
});
|
||||
|
||||
test("doesn't apply an offset for events in the same timezone", async (assert) => {
|
||||
await visit("/t/252");
|
||||
|
||||
const eventElement = getEventByText("Lisbon");
|
||||
|
||||
assert.notOk(eventElement.style.marginLeft);
|
||||
assert.notOk(eventElement.style.marginRight);
|
||||
});
|
||||
|
||||
test("applies the correct offset for events that extend into the next day", async (assert) => {
|
||||
await visit("/t/252");
|
||||
|
||||
const eventElement = getEventByText("Cordoba");
|
||||
|
||||
assert.strictEqual(getRoundedPct(eventElement.style.marginLeft), 8); // ( ( 1 - (-3) ) / 24 ) * 50%
|
||||
assert.strictEqual(getRoundedPct(eventElement.style.marginRight), 42); // ( ( 24 - ( 1 - (-3) ) ) / 24 ) * 50%
|
||||
});
|
||||
|
||||
test("applies the correct offset for events that start on the previous day", async (assert) => {
|
||||
await visit("/t/252");
|
||||
|
||||
const eventElement = getEventByText("Tokyo");
|
||||
|
||||
assert.strictEqual(getRoundedPct(eventElement.style.marginLeft), 22); // ( ( 24 - ( 9 - 1 ) ) / 24 ) * 33.33%
|
||||
assert.strictEqual(getRoundedPct(eventElement.style.marginRight), 11); // ( ( 9 - 1 ) / 24 ) * 33.33%
|
||||
});
|
||||
|
||||
test("applies the correct offset for multiline events", async (assert) => {
|
||||
await visit("/t/252");
|
||||
|
||||
const eventElement = getEventByText("Moscow");
|
||||
|
||||
assert.strictEqual(getRoundedPct(eventElement[0].style.marginLeft), 46); // ( ( 24 - ( 1 - (-1) ) ) / 24 ) * 50%
|
||||
assert.notOk(eventElement[0].style.marginRight);
|
||||
|
||||
assert.notOk(eventElement[1].style.marginLeft);
|
||||
assert.strictEqual(getRoundedPct(eventElement[1].style.marginRight), 8); // ( ( 1 - (-1) ) / 24 ) * 100%
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Discourse Calendar - Splitted Grouped Events", function (needs) {
|
||||
needs.settings({
|
||||
calendar_enabled: true,
|
||||
enable_timezone_offset_for_calendar_events: true,
|
||||
split_grouped_events_by_timezone_threshold: 0,
|
||||
});
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/t/252.json", () => {
|
||||
return helper.response(topicResponse);
|
||||
});
|
||||
});
|
||||
|
||||
test("splits holidays events by timezone", async (assert) => {
|
||||
await visit("/t/252");
|
||||
|
||||
const eventElement = document.querySelectorAll(
|
||||
".fc-day-grid-event.grouped-event"
|
||||
);
|
||||
assert.ok(eventElement.length === 3);
|
||||
|
||||
assert.strictEqual(getRoundedPct(eventElement[0].style.marginLeft), 13); // ( ( 1 - (-5) ) / 24 ) * 50%
|
||||
assert.strictEqual(getRoundedPct(eventElement[0].style.marginRight), 38); // ( ( 24 - ( 1 - (-5) ) ) / 24 ) * 50%
|
||||
|
||||
assert.strictEqual(getRoundedPct(eventElement[1].style.marginLeft), 15); // ( ( 1 - (-6) ) / 24 ) * 50%
|
||||
assert.strictEqual(getRoundedPct(eventElement[1].style.marginRight), 35); // ( ( 24 - ( 1 - (-6) ) ) / 24 ) * 50%
|
||||
|
||||
assert.strictEqual(getRoundedPct(eventElement[2].style.marginLeft), 17); // ( ( 1 - (-7) ) / 24 ) * 50%
|
||||
assert.strictEqual(getRoundedPct(eventElement[2].style.marginRight), 33); // ( ( 24 - ( 1 - (-7) ) ) / 24 ) * 50%
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Discourse Calendar - Grouped Events", function (needs) {
|
||||
needs.settings({
|
||||
calendar_enabled: true,
|
||||
enable_timezone_offset_for_calendar_events: true,
|
||||
split_grouped_events_by_timezone_threshold: 2,
|
||||
});
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/t/252.json", () => {
|
||||
return helper.response(topicResponse);
|
||||
});
|
||||
});
|
||||
|
||||
test("groups holidays events according to threshold", async (assert) => {
|
||||
await visit("/t/252");
|
||||
|
||||
const eventElement = document.querySelectorAll(
|
||||
".fc-day-grid-event.grouped-event"
|
||||
);
|
||||
assert.ok(eventElement.length === 1);
|
||||
|
||||
assert.strictEqual(getRoundedPct(eventElement[0].style.marginLeft), 15); // ( ( 1 - (-6) ) / 24 ) * 50%
|
||||
assert.strictEqual(getRoundedPct(eventElement[0].style.marginRight), 35); // ( ( 24 - ( 1 - (-6) ) ) / 24 ) * 50%
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue