FEATURE: save local date to calendar (#14486)

FEATURE: save local date to calendar (#14486)

It allows saving local date to calendar. Modal is giving option to pick between ics and google. User choice can be remembered as a default for the next actions.

diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 0e9654e..6ddaadf 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -31,6 +31,7 @@
 //= require ./discourse/app/lib/text-direction
 //= require ./discourse/app/lib/eyeline
 //= require ./discourse/app/lib/show-modal
+//= require ./discourse/app/lib/download-calendar
 //= require ./discourse/app/mixins/scrolling
 //= require ./discourse/app/lib/ajax-error
 //= require ./discourse/app/models/result-set
diff --git a/app/assets/javascripts/discourse/app/controllers/download-calendar.js b/app/assets/javascripts/discourse/app/controllers/download-calendar.js
new file mode 100644
index 0000000..a956fb1
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/controllers/download-calendar.js
@@ -0,0 +1,26 @@
+import { action } from "@ember/object";
+import Controller from "@ember/controller";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+import { downloadGoogle, downloadIcs } from "discourse/lib/download-calendar";
+
+export default Controller.extend(ModalFunctionality, {
+  selectedCalendar: "ics",
+  remember: false,
+
+  @action
+  downloadCalendar() {
+    if (this.remember) {
+      this.currentUser.setProperties({
+        default_calendar: this.selectedCalendar,
+        user_option: { default_calendar: this.selectedCalendar },
+      });
+      this.currentUser.save(["default_calendar"]);
+    }
+    if (this.selectedCalendar === "ics") {
+      downloadIcs(this.model.postId, this.model.title, this.model.dates);
+    } else {
+      downloadGoogle(this.model.title, this.model.dates);
+    }
+    this.send("closeModal");
+  },
+});
diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/profile.js b/app/assets/javascripts/discourse/app/controllers/preferences/profile.js
index d6a40c3..5356a8a 100644
--- a/app/assets/javascripts/discourse/app/controllers/preferences/profile.js
+++ b/app/assets/javascripts/discourse/app/controllers/preferences/profile.js
@@ -23,6 +23,12 @@ export default Controller.extend({
       "card_background_upload_url",
       "date_of_birth",
       "timezone",
+      "default_calendar",
+    ];
+
+    this.calendarOptions = [
+      { name: I18n.t("download_calendar.google"), value: "google" },
+      { name: I18n.t("download_calendar.ics"), value: "ics" },
     ];
   },
 
@@ -45,6 +51,11 @@ export default Controller.extend({
     }
   },
 
+  @discourseComputed("model.default_calendar")
+  canChangeDefaultCalendar(defaultCalendar) {
+    return defaultCalendar !== "none_selected";
+  },
+
   canChangeBio: readOnly("model.can_change_bio"),
 
   canChangeLocation: readOnly("model.can_change_location"),
diff --git a/app/assets/javascripts/discourse/app/lib/download-calendar.js b/app/assets/javascripts/discourse/app/lib/download-calendar.js
new file mode 100644
index 0000000..de1b615
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/download-calendar.js
@@ -0,0 +1,67 @@
+import User from "discourse/models/user";
+import showModal from "discourse/lib/show-modal";
+import getURL from "discourse-common/lib/get-url";
+
+export function downloadCalendar(postId, title, dates) {
+  const currentUser = User.current();
+
+  const formattedDates = formatDates(dates);
+
+  switch (currentUser.default_calendar) {
+    case "none_selected":
+      _displayModal(postId, title, formattedDates);
+      break;
+    case "ics":
+      downloadIcs(postId, title, formattedDates);
+      break;
+    case "google":
+      downloadGoogle(title, formattedDates);
+      break;
+  }
+}
+
+export function downloadIcs(postId, title, dates) {
+  let datesParam = "";
+  dates.forEach((date, index) => {
+    datesParam = datesParam.concat(
+      `&dates[${index}][starts_at]=${date.startsAt}&dates[${index}][ends_at]=${date.endsAt}`
+    );
+  });
+  const link = getURL(
+    `/calendars.ics?post_id=${postId}&title=${title}&${datesParam}`
+  );
+  window.open(link, "_blank", "noopener", "noreferrer");
+}
+
+export function downloadGoogle(title, dates) {
+  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");
+  });
+}
+
+export function formatDates(dates) {
+  return dates.map((date) => {
+    return {
+      startsAt: date.startsAt,
+      endsAt: date.endsAt
+        ? date.endsAt
+        : moment.utc(date.startsAt).add(1, "hours").format(),
+    };
+  });
+}
+
+function _displayModal(postId, title, dates) {
+  showModal("download-calendar", { model: { title, postId, dates } });
+}
+
+function _formatDateForGoogleApi(date) {
+  return moment(date)
+    .toISOString()
+    .replace(/-|:|\.\d\d\d/g, "");
+}
diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js
index 775b40d..d449e14 100644
--- a/app/assets/javascripts/discourse/app/models/user.js
+++ b/app/assets/javascripts/discourse/app/models/user.js
@@ -97,6 +97,7 @@ let userOptionFields = [
   "title_count_mode",
   "timezone",
   "skip_new_user_tips",
+  "default_calendar",
 ];
 
 export function addSaveableUserOptionField(fieldName) {
diff --git a/app/assets/javascripts/discourse/app/templates/modal/download-calendar.hbs b/app/assets/javascripts/discourse/app/templates/modal/download-calendar.hbs
new file mode 100644
index 0000000..68f5236
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/templates/modal/download-calendar.hbs
@@ -0,0 +1,45 @@
+<div>
+  {{#d-modal-body title="download_calendar.title"}}
+    <div class="control-group">
+      <div class="ics">
+        <label class="radio" for="ics">
+          {{radio-button
+            name="select-calendar"
+            id="ics"
+            value="ics"
+            selection=selectedCalendar
+            onChange=(action (mut selectedCalendar))
+          }}
+          {{i18n "download_calendar.save_ics"}}
+        </label>
+      </div>
+      <div class="google">
+        <label class="radio" for="google">
+          {{radio-button
+            name="select-calendar"
+            id="google"
+            value="google"
+            selection=selectedCalendar
+            onChange=(action (mut selectedCalendar))
+          }}
+          {{i18n "download_calendar.save_google"}}
+        </label>
+      </div>
+    </div>
+
+    <div class="control-group remember">
+      <label>
+        {{input type="checkbox" checked=remember}} <span>{{i18n "download_calendar.remember"}}</span>
+      </label>
+      <span>{{i18n "download_calendar.remember_explanation"}}</span>
+    </div>
+  {{/d-modal-body}}
+  <div class="modal-footer">
+    {{d-button
+      class="btn-primary"
+      action=(action "downloadCalendar")
+      label="download_calendar.download"
+    }}
+    {{d-modal-cancel close=(route-action "closeModal")}}
+  </div>
+</div>
diff --git a/app/assets/javascripts/discourse/app/templates/preferences/profile.hbs b/app/assets/javascripts/discourse/app/templates/preferences/profile.hbs
index f944760..a0f5384 100644
--- a/app/assets/javascripts/discourse/app/templates/preferences/profile.hbs
+++ b/app/assets/javascripts/discourse/app/templates/preferences/profile.hbs
@@ -103,6 +103,24 @@
   </div>
 {{/if}}
 
+{{#if canChangeDefaultCalendar }}
+  <div class="control-group">
+    <label class="control-label">{{i18n "download_calendar.default_calendar"}}</label>
+    <div>
+      {{combo-box
+        valueProperty="value"
+        content=calendarOptions
+        value=model.user_option.default_calendar
+        id="user-default-calendar"
+        onChange=(action (mut model.user_option.default_calendar))
+      }}
+    </div>
+    <div class="instructions">

[... diff too long, it was truncated ...]

GitHub sha: cb5b0cb9d833345798dc93e9eaed6ac68c95e038

This commit appears in #14486 which was approved by jjaffeux. It was merged by lis2.