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:
parent
50bafd48cd
commit
7e013b2120
|
@ -22,12 +22,14 @@ export default class downloadCalendar extends Component {
|
||||||
if (this.selectedCalendar === "ics") {
|
if (this.selectedCalendar === "ics") {
|
||||||
downloadIcs(
|
downloadIcs(
|
||||||
this.args.model.calendar.title,
|
this.args.model.calendar.title,
|
||||||
this.args.model.calendar.dates
|
this.args.model.calendar.dates,
|
||||||
|
this.args.model.calendar.recurrenceRule
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
downloadGoogle(
|
downloadGoogle(
|
||||||
this.args.model.calendar.title,
|
this.args.model.calendar.title,
|
||||||
this.args.model.calendar.dates
|
this.args.model.calendar.dates,
|
||||||
|
this.args.model.calendar.recurrenceRule
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.args.closeModal();
|
this.args.closeModal();
|
||||||
|
|
|
@ -3,7 +3,7 @@ import User from "discourse/models/user";
|
||||||
import { getOwnerWithFallback } from "discourse-common/lib/get-owner";
|
import { getOwnerWithFallback } from "discourse-common/lib/get-owner";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
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 currentUser = User.current();
|
||||||
|
|
||||||
const formattedDates = formatDates(dates);
|
const formattedDates = formatDates(dates);
|
||||||
|
@ -11,20 +11,20 @@ export function downloadCalendar(title, dates) {
|
||||||
|
|
||||||
switch (currentUser.user_option.default_calendar) {
|
switch (currentUser.user_option.default_calendar) {
|
||||||
case "none_selected":
|
case "none_selected":
|
||||||
_displayModal(title, formattedDates);
|
_displayModal(title, formattedDates, recurrenceRule);
|
||||||
break;
|
break;
|
||||||
case "ics":
|
case "ics":
|
||||||
downloadIcs(title, formattedDates);
|
downloadIcs(title, formattedDates, recurrenceRule);
|
||||||
break;
|
break;
|
||||||
case "google":
|
case "google":
|
||||||
downloadGoogle(title, formattedDates);
|
downloadGoogle(title, formattedDates, recurrenceRule);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadIcs(title, dates) {
|
export function downloadIcs(title, dates, recurrenceRule) {
|
||||||
const REMOVE_FILE_AFTER = 20_000;
|
const REMOVE_FILE_AFTER = 20_000;
|
||||||
const file = new File([generateIcsData(title, dates)], {
|
const file = new File([generateIcsData(title, dates, recurrenceRule)], {
|
||||||
type: "text/plain",
|
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
|
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) => {
|
dates.forEach((date) => {
|
||||||
const encodedTitle = encodeURIComponent(title);
|
const link = new URL("https://www.google.com/calendar/event");
|
||||||
const link = getURL(`
|
link.searchParams.append("action", "TEMPLATE");
|
||||||
https://www.google.com/calendar/event?action=TEMPLATE&text=${encodedTitle}&dates=${_formatDateForGoogleApi(
|
link.searchParams.append("text", title);
|
||||||
date.startsAt
|
link.searchParams.append(
|
||||||
)}/${_formatDateForGoogleApi(date.endsAt)}
|
"dates",
|
||||||
`).trim();
|
`${_formatDateForGoogleApi(date.startsAt)}/${_formatDateForGoogleApi(
|
||||||
window.open(link, "_blank", "noopener", "noreferrer");
|
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";
|
let data = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Discourse//EN\n";
|
||||||
dates.forEach((date) => {
|
dates.forEach((date) => {
|
||||||
const startDate = moment(date.startsAt);
|
const startDate = moment(date.startsAt);
|
||||||
|
@ -72,6 +80,7 @@ export function generateIcsData(title, dates) {
|
||||||
`DTSTAMP:${moment().utc().format("YMMDDTHHmmss")}Z\n` +
|
`DTSTAMP:${moment().utc().format("YMMDDTHHmmss")}Z\n` +
|
||||||
`DTSTART:${startDate.utc().format("YMMDDTHHmmss")}Z\n` +
|
`DTSTART:${startDate.utc().format("YMMDDTHHmmss")}Z\n` +
|
||||||
`DTEND:${endDate.utc().format("YMMDDTHHmmss")}Z\n` +
|
`DTEND:${endDate.utc().format("YMMDDTHHmmss")}Z\n` +
|
||||||
|
(recurrenceRule ? `RRULE:${recurrenceRule}\n` : ``) +
|
||||||
`SUMMARY:${title}\n` +
|
`SUMMARY:${title}\n` +
|
||||||
"END:VEVENT\n"
|
"END:VEVENT\n"
|
||||||
);
|
);
|
||||||
|
@ -80,9 +89,11 @@ export function generateIcsData(title, dates) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _displayModal(title, dates) {
|
function _displayModal(title, dates, recurrenceRule) {
|
||||||
const modal = getOwnerWithFallback(this).lookup("service:modal");
|
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) {
|
function _formatDateForGoogleApi(date) {
|
||||||
|
|
|
@ -141,7 +141,7 @@ import { modifySelectKit } from "select-kit/mixins/plugin-api";
|
||||||
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
|
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
|
||||||
// using the format described at https://keepachangelog.com/en/1.0.0/.
|
// 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.
|
// This helper prevents us from applying the same `modifyClass` over and over in test mode.
|
||||||
function canModify(klass, type, resolverName, changes) {
|
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", [
|
* api.downloadCalendar("title of the event", [
|
||||||
|
@ -1843,12 +1843,14 @@ class PluginApi {
|
||||||
startsAt: "2021-10-12T15:00:00.000Z",
|
startsAt: "2021-10-12T15:00:00.000Z",
|
||||||
endsAt: "2021-10-12T16:00:00.000Z",
|
endsAt: "2021-10-12T16:00:00.000Z",
|
||||||
},
|
},
|
||||||
* ]);
|
* ],
|
||||||
|
* "FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
|
||||||
|
* );
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
downloadCalendar(title, dates) {
|
downloadCalendar(title, dates, recurrenceRule = null) {
|
||||||
downloadCalendar(title, dates);
|
downloadCalendar(title, dates, recurrenceRule);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,31 +16,73 @@ module("Unit | Utility | download-calendar", function (hooks) {
|
||||||
sinon.stub(win, "focus");
|
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", [
|
const data = generateIcsData("event test", [
|
||||||
{
|
{
|
||||||
startsAt: "2021-10-12T15:00:00.000Z",
|
startsAt: "2021-10-12T15:00:00.000Z",
|
||||||
endsAt: "2021-10-12T16:00:00.000Z",
|
endsAt: "2021-10-12T16:00:00.000Z",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
assert.ok(
|
assert.equal(
|
||||||
data,
|
data,
|
||||||
`
|
`BEGIN:VCALENDAR
|
||||||
BEGIN:VCALENDAR
|
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
PRODID:-//Discourse//EN
|
PRODID:-//Discourse//EN
|
||||||
BEGIN:VEVENT
|
BEGIN:VEVENT
|
||||||
UID:1634050800000_1634054400000
|
UID:1634050800000_1634054400000
|
||||||
DTSTAMP:20213312T223320Z
|
DTSTAMP:20220404T211500Z
|
||||||
DTSTART:20210012T150000Z
|
DTSTART:20211012T150000Z
|
||||||
DTEND:20210012T160000Z
|
DTEND:20211012T160000Z
|
||||||
SUMMARY:event2
|
SUMMARY:event test
|
||||||
END:VEVENT
|
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) {
|
test("correct url for Google", function (assert) {
|
||||||
downloadGoogle("event", [
|
downloadGoogle("event", [
|
||||||
{
|
{
|
||||||
|
@ -50,7 +92,28 @@ END:VCALENDAR
|
||||||
]);
|
]);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
window.open.calledWith(
|
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",
|
"_blank",
|
||||||
"noopener",
|
"noopener",
|
||||||
"noreferrer"
|
"noreferrer"
|
||||||
|
|
|
@ -7,6 +7,12 @@ in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
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).
|
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
|
## [1.15.0] - 2023-10-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -90,7 +90,7 @@ acceptance(
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
[...arguments],
|
[...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",
|
"_blank",
|
||||||
"noopener",
|
"noopener",
|
||||||
"noreferrer",
|
"noreferrer",
|
||||||
|
|
Loading…
Reference in New Issue