DEV: add recurrence rule parameter to downloadCalendar API (#24404)

Add option to create recurrent calendar events. Recurrence rule parameter follows rfc5545 specification: https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10
This commit is contained in:
Krzysztof Kotlarek 2023-11-30 13:56:22 +11:00 committed by GitHub
parent 50bafd48cd
commit 7e013b2120
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 120 additions and 36 deletions

View File

@ -22,12 +22,14 @@ export default class downloadCalendar extends Component {
if (this.selectedCalendar === "ics") {
downloadIcs(
this.args.model.calendar.title,
this.args.model.calendar.dates
this.args.model.calendar.dates,
this.args.model.calendar.recurrenceRule
);
} else {
downloadGoogle(
this.args.model.calendar.title,
this.args.model.calendar.dates
this.args.model.calendar.dates,
this.args.model.calendar.recurrenceRule
);
}
this.args.closeModal();

View File

@ -3,7 +3,7 @@ import User from "discourse/models/user";
import { getOwnerWithFallback } from "discourse-common/lib/get-owner";
import getURL from "discourse-common/lib/get-url";
export function downloadCalendar(title, dates) {
export function downloadCalendar(title, dates, recurrenceRule = null) {
const currentUser = User.current();
const formattedDates = formatDates(dates);
@ -11,20 +11,20 @@ export function downloadCalendar(title, dates) {
switch (currentUser.user_option.default_calendar) {
case "none_selected":
_displayModal(title, formattedDates);
_displayModal(title, formattedDates, recurrenceRule);
break;
case "ics":
downloadIcs(title, formattedDates);
downloadIcs(title, formattedDates, recurrenceRule);
break;
case "google":
downloadGoogle(title, formattedDates);
downloadGoogle(title, formattedDates, recurrenceRule);
break;
}
}
export function downloadIcs(title, dates) {
export function downloadIcs(title, dates, recurrenceRule) {
const REMOVE_FILE_AFTER = 20_000;
const file = new File([generateIcsData(title, dates)], {
const file = new File([generateIcsData(title, dates, recurrenceRule)], {
type: "text/plain",
});
@ -37,15 +37,23 @@ export function downloadIcs(title, dates) {
setTimeout(() => window.URL.revokeObjectURL(file), REMOVE_FILE_AFTER); //remove file to avoid memory leaks
}
export function downloadGoogle(title, dates) {
export function downloadGoogle(title, dates, recurrenceRule) {
dates.forEach((date) => {
const encodedTitle = encodeURIComponent(title);
const link = getURL(`
https://www.google.com/calendar/event?action=TEMPLATE&text=${encodedTitle}&dates=${_formatDateForGoogleApi(
date.startsAt
)}/${_formatDateForGoogleApi(date.endsAt)}
`).trim();
window.open(link, "_blank", "noopener", "noreferrer");
const link = new URL("https://www.google.com/calendar/event");
link.searchParams.append("action", "TEMPLATE");
link.searchParams.append("text", title);
link.searchParams.append(
"dates",
`${_formatDateForGoogleApi(date.startsAt)}/${_formatDateForGoogleApi(
date.endsAt
)}`
);
if (recurrenceRule) {
link.searchParams.append("recur", `RRULE:${recurrenceRule}`);
}
window.open(getURL(link.href).trim(), "_blank", "noopener", "noreferrer");
});
}
@ -60,7 +68,7 @@ export function formatDates(dates) {
});
}
export function generateIcsData(title, dates) {
export function generateIcsData(title, dates, recurrenceRule) {
let data = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Discourse//EN\n";
dates.forEach((date) => {
const startDate = moment(date.startsAt);
@ -72,6 +80,7 @@ export function generateIcsData(title, dates) {
`DTSTAMP:${moment().utc().format("YMMDDTHHmmss")}Z\n` +
`DTSTART:${startDate.utc().format("YMMDDTHHmmss")}Z\n` +
`DTEND:${endDate.utc().format("YMMDDTHHmmss")}Z\n` +
(recurrenceRule ? `RRULE:${recurrenceRule}\n` : ``) +
`SUMMARY:${title}\n` +
"END:VEVENT\n"
);
@ -80,9 +89,11 @@ export function generateIcsData(title, dates) {
return data;
}
function _displayModal(title, dates) {
function _displayModal(title, dates, recurrenceRule) {
const modal = getOwnerWithFallback(this).lookup("service:modal");
modal.show(downloadCalendarModal, { model: { calendar: { title, dates } } });
modal.show(downloadCalendarModal, {
model: { calendar: { title, dates, recurrenceRule } },
});
}
function _formatDateForGoogleApi(date) {

View File

@ -141,7 +141,7 @@ import { modifySelectKit } from "select-kit/mixins/plugin-api";
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
// using the format described at https://keepachangelog.com/en/1.0.0/.
export const PLUGIN_API_VERSION = "1.15.0";
export const PLUGIN_API_VERSION = "1.16.0";
// This helper prevents us from applying the same `modifyClass` over and over in test mode.
function canModify(klass, type, resolverName, changes) {
@ -1835,7 +1835,7 @@ class PluginApi {
}
/**
* Download calendar modal which allow to pick between ICS and Google Calendar
* Download calendar modal which allow to pick between ICS and Google Calendar. Optionally, recurrence rule can be specified - https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10
*
* ```
* api.downloadCalendar("title of the event", [
@ -1843,12 +1843,14 @@ class PluginApi {
startsAt: "2021-10-12T15:00:00.000Z",
endsAt: "2021-10-12T16:00:00.000Z",
},
* ]);
* ],
* "FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
* );
* ```
*
*/
downloadCalendar(title, dates) {
downloadCalendar(title, dates);
downloadCalendar(title, dates, recurrenceRule = null) {
downloadCalendar(title, dates, recurrenceRule);
}
/**

View File

@ -16,31 +16,73 @@ module("Unit | Utility | download-calendar", function (hooks) {
sinon.stub(win, "focus");
});
test("correct data for Ics", function (assert) {
test("correct data for ICS", function (assert) {
const now = moment.tz("2022-04-04 23:15", "Europe/Paris").valueOf();
sinon.useFakeTimers({
now,
toFake: ["Date"],
shouldAdvanceTime: true,
shouldClearNativeTimers: true,
});
const data = generateIcsData("event test", [
{
startsAt: "2021-10-12T15:00:00.000Z",
endsAt: "2021-10-12T16:00:00.000Z",
},
]);
assert.ok(
assert.equal(
data,
`
BEGIN:VCALENDAR
`BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Discourse//EN
BEGIN:VEVENT
UID:1634050800000_1634054400000
DTSTAMP:20213312T223320Z
DTSTART:20210012T150000Z
DTEND:20210012T160000Z
SUMMARY:event2
DTSTAMP:20220404T211500Z
DTSTART:20211012T150000Z
DTEND:20211012T160000Z
SUMMARY:event test
END:VEVENT
END:VCALENDAR
`
END:VCALENDAR`
);
});
test("correct data for ICS when recurring event", function (assert) {
const now = moment.tz("2022-04-04 23:15", "Europe/Paris").valueOf();
sinon.useFakeTimers({
now,
toFake: ["Date"],
shouldAdvanceTime: true,
shouldClearNativeTimers: true,
});
const data = generateIcsData(
"event test",
[
{
startsAt: "2021-10-12T15:00:00.000Z",
endsAt: "2021-10-12T16:00:00.000Z",
},
],
"FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
);
assert.equal(
data,
`BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Discourse//EN
BEGIN:VEVENT
UID:1634050800000_1634054400000
DTSTAMP:20220404T211500Z
DTSTART:20211012T150000Z
DTEND:20211012T160000Z
RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR
SUMMARY:event test
END:VEVENT
END:VCALENDAR`
);
sinon.restore();
});
test("correct url for Google", function (assert) {
downloadGoogle("event", [
{
@ -50,7 +92,28 @@ END:VCALENDAR
]);
assert.ok(
window.open.calledWith(
"https://www.google.com/calendar/event?action=TEMPLATE&text=event&dates=20211012T150000Z/20211012T160000Z",
"https://www.google.com/calendar/event?action=TEMPLATE&text=event&dates=20211012T150000Z%2F20211012T160000Z",
"_blank",
"noopener",
"noreferrer"
)
);
});
test("correct url for Google when recurring event", function (assert) {
downloadGoogle(
"event",
[
{
startsAt: "2021-10-12T15:00:00.000Z",
endsAt: "2021-10-12T16:00:00.000Z",
},
],
"FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
);
assert.ok(
window.open.calledWith(
"https://www.google.com/calendar/event?action=TEMPLATE&text=event&dates=20211012T150000Z%2F20211012T160000Z&recur=RRULE%3AFREQ%3DDAILY%3BBYDAY%3DMO%2CTU%2CWE%2CTH%2CFR",
"_blank",
"noopener",
"noreferrer"

View File

@ -7,6 +7,12 @@ in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.16.0] - 2023-11-17
### Added
- Added `recurrenceRule` option to `downloadCalendar`, this can be used to set recurring events in the calendar. Rule syntax can be found at https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10.
## [1.15.0] - 2023-10-18
### Added

View File

@ -90,7 +90,7 @@ acceptance(
assert.deepEqual(
[...arguments],
[
`https://www.google.com/calendar/event?action=TEMPLATE&text=title%20to%20trim&dates=${startDate}T180000Z/${startDate}T190000Z`,
`https://www.google.com/calendar/event?action=TEMPLATE&text=title+to+trim&dates=${startDate}T180000Z%2F${startDate}T190000Z`,
"_blank",
"noopener",
"noreferrer",