diff --git a/app/assets/javascripts/discourse/app/components/bookmark-local-date.js b/app/assets/javascripts/discourse/app/components/bookmark-local-date.js deleted file mode 100644 index d89c2000737..00000000000 --- a/app/assets/javascripts/discourse/app/components/bookmark-local-date.js +++ /dev/null @@ -1,57 +0,0 @@ -import Component from "@ember/component"; -import I18n from "I18n"; -import { action } from "@ember/object"; -import { getOwner } from "discourse-common/lib/get-owner"; -import { or } from "@ember/object/computed"; - -export default Component.extend({ - tagName: "", - - init() { - this._super(...arguments); - - this.loadLocalDates(); - }, - - get postLocalDateFormatted() { - return this.postLocalDate().format(I18n.t("dates.long_no_year")); - }, - - showPostLocalDate: or("postDetectedLocalDate", "postDetectedLocalTime"), - - loadLocalDates() { - let postEl = document.querySelector(`[data-post-id="${this.postId}"]`); - let localDateEl = null; - if (postEl) { - localDateEl = postEl.querySelector(".discourse-local-date"); - } - - this.setProperties({ - postDetectedLocalDate: localDateEl ? localDateEl.dataset.date : null, - postDetectedLocalTime: localDateEl ? localDateEl.dataset.time : null, - postDetectedLocalTimezone: localDateEl - ? localDateEl.dataset.timezone - : null, - }); - }, - - postLocalDate() { - const bookmarkController = getOwner(this).lookup("controller:bookmark"); - let parsedPostLocalDate = bookmarkController._parseCustomDateTime( - this.postDetectedLocalDate, - this.postDetectedLocalTime, - this.postDetectedLocalTimezone - ); - - if (!this.postDetectedLocalTime) { - return bookmarkController.startOfDay(parsedPostLocalDate); - } - - return parsedPostLocalDate; - }, - - @action - setReminder() { - return this.onChange(this.postLocalDate()); - }, -}); diff --git a/app/assets/javascripts/discourse/app/components/bookmark.js b/app/assets/javascripts/discourse/app/components/bookmark.js new file mode 100644 index 00000000000..67c95ddc779 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/bookmark.js @@ -0,0 +1,416 @@ +import { + LATER_TODAY_CUTOFF_HOUR, + MOMENT_THURSDAY, + laterToday, + now, + parseCustomDatetime, + startOfDay, + tomorrow, +} from "discourse/lib/time-utils"; + +import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark"; +import Component from "@ember/component"; +import I18n from "I18n"; +import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts"; +import { Promise } from "rsvp"; +import { TIME_SHORTCUT_TYPES } from "discourse/lib/time-shortcut"; + +import { action } from "@ember/object"; +import { ajax } from "discourse/lib/ajax"; +import bootbox from "bootbox"; +import discourseComputed, { on } from "discourse-common/utils/decorators"; +import { formattedReminderTime } from "discourse/lib/bookmark"; +import { and, notEmpty, or } from "@ember/object/computed"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +// global shortcuts that interfere with these modal shortcuts, they are rebound when the +// modal is closed +// +// d deletePost +const GLOBAL_SHORTCUTS_TO_PAUSE = ["d"]; +const BOOKMARK_BINDINGS = { + enter: { handler: "saveAndClose" }, + "d d": { handler: "delete" }, +}; + +export default Component.extend({ + tagName: "", + + errorMessage: null, + selectedReminderType: null, + _closeWithoutSaving: null, + _savingBookmarkManually: null, + _saving: null, + _deleting: null, + postDetectedLocalDate: null, + postDetectedLocalTime: null, + postDetectedLocalTimezone: null, + prefilledDatetime: null, + userTimezone: null, + showOptions: null, + model: null, + + afterSave: null, + + @on("init") + _setup() { + this.setProperties({ + errorMessage: null, + selectedReminderType: TIME_SHORTCUT_TYPES.NONE, + _closeWithoutSaving: false, + _savingBookmarkManually: false, + _saving: false, + _deleting: false, + postDetectedLocalDate: null, + postDetectedLocalTime: null, + postDetectedLocalTimezone: null, + prefilledDatetime: null, + userTimezone: this.currentUser.resolvedTimezone(this.currentUser), + showOptions: false, + }); + + this.registerOnCloseHandler(this._onModalClose.bind(this)); + + this._loadBookmarkOptions(); + this._bindKeyboardShortcuts(); + + if (this.editingExistingBookmark) { + this._initializeExistingBookmarkData(); + } + + this._loadPostLocalDates(); + }, + + @on("didInsertElement") + _prepareUI() { + if (this.site.isMobileDevice) { + document.getElementById("bookmark-name").blur(); + } + + // we want to make sure the options panel opens so the user + // knows they have set these options previously. + if (this.autoDeletePreference) { + this.toggleOptionsPanel(); + } + }, + + _initializeExistingBookmarkData() { + if (this.existingBookmarkHasReminder) { + this.set("prefilledDatetime", this.model.reminderAt); + + let parsedDatetime = parseCustomDatetime( + this.prefilledDatetime, + null, + this.userTimezone + ); + + this.set("selectedDatetime", parsedDatetime); + } + }, + + _loadBookmarkOptions() { + this.set( + "autoDeletePreference", + this.model.autoDeletePreference || this._preferredDeleteOption() || 0 + ); + }, + + _preferredDeleteOption() { + let preferred = localStorage.bookmarkDeleteOption; + if (preferred && preferred !== "") { + preferred = parseInt(preferred, 10); + } + return preferred; + }, + + _bindKeyboardShortcuts() { + KeyboardShortcuts.pause(GLOBAL_SHORTCUTS_TO_PAUSE); + Object.keys(BOOKMARK_BINDINGS).forEach((shortcut) => { + KeyboardShortcuts.addShortcut(shortcut, () => { + let binding = BOOKMARK_BINDINGS[shortcut]; + if (binding.args) { + return this.send(binding.handler, ...binding.args); + } + this.send(binding.handler); + }); + }); + }, + + _unbindKeyboardShortcuts() { + KeyboardShortcuts.unbind(BOOKMARK_BINDINGS); + }, + + _restoreGlobalShortcuts() { + KeyboardShortcuts.unpause(GLOBAL_SHORTCUTS_TO_PAUSE); + }, + + _loadPostLocalDates() { + let postEl = document.querySelector( + `[data-post-id="${this.model.postId}"]` + ); + let localDateEl; + if (postEl) { + localDateEl = postEl.querySelector(".discourse-local-date"); + } + + if (localDateEl) { + this.setProperties({ + postDetectedLocalDate: localDateEl.dataset.date, + postDetectedLocalTime: localDateEl.dataset.time, + postDetectedLocalTimezone: localDateEl.dataset.timezone, + }); + } + }, + + _saveBookmark() { + let reminderAt; + if (this.selectedReminderType) { + reminderAt = this.selectedDatetime; + } + + const reminderAtISO = reminderAt ? reminderAt.toISOString() : null; + + if (this.selectedReminderType === TIME_SHORTCUT_TYPES.CUSTOM) { + if (!reminderAt) { + return Promise.reject(I18n.t("bookmarks.invalid_custom_datetime")); + } + } + + localStorage.bookmarkDeleteOption = this.autoDeletePreference; + + let reminderType; + if (this.selectedReminderType === TIME_SHORTCUT_TYPES.NONE) { + reminderType = null; + } else if ( + this.selectedReminderType === TIME_SHORTCUT_TYPES.LAST_CUSTOM || + this.selectedReminderType === TIME_SHORTCUT_TYPES.POST_LOCAL_DATE + ) { + reminderType = TIME_SHORTCUT_TYPES.CUSTOM; + } else { + reminderType = this.selectedReminderType; + } + + const data = { + reminder_type: reminderType, + reminder_at: reminderAtISO, + name: this.model.name, + post_id: this.model.postId, + id: this.model.id, + auto_delete_preference: this.autoDeletePreference, + }; + + if (this.editingExistingBookmark) { + return ajax(`/bookmarks/${this.model.id}`, { + type: "PUT", + data, + }).then(() => { + this._executeAfterSave(reminderAtISO); + }); + } else { + return ajax("/bookmarks", { type: "POST", data }).then((response) => { + this.set("model.id", response.id); + this._executeAfterSave(reminderAtISO); + }); + } + }, + + _executeAfterSave(reminderAtISO) { + if (!this.afterSave) { + return; + } + this.afterSave({ + reminderAt: reminderAtISO, + reminderType: this.selectedReminderType, + autoDeletePreference: this.autoDeletePreference, + id: this.model.id, + name: this.model.name, + }); + }, + + _deleteBookmark() { + return ajax("/bookmarks/" + this.model.id, { + type: "DELETE", + }).then((response) => { + if (this.afterDelete) { + this.afterDelete(response.topic_bookmarked); + } + }); + }, + + _postLocalDate() { + let parsedPostLocalDate = parseCustomDatetime( + this.postDetectedLocalDate, + this.postDetectedLocalTime, + this.userTimezone, + this.postDetectedLocalTimezone + ); + + if (!this.postDetectedLocalTime) { + return startOfDay(parsedPostLocalDate); + } + + return parsedPostLocalDate; + }, + + _handleSaveError(e) { + this._savingBookmarkManually = false; + if (typeof e === "string") { + bootbox.alert(e); + } else { + popupAjaxError(e); + } + }, + + _onModalClose(initiatedByCloseButton) { + // we want to close without saving if the user already saved + // manually or deleted the bookmark, as well as when the modal + // is just closed with the X button + this._closeWithoutSaving = + this._closeWithoutSaving || initiatedByCloseButton; + + this._unbindKeyboardShortcuts(); + this._restoreGlobalShortcuts(); + + if (!this._closeWithoutSaving && !this._savingBookmarkManually) { + this._saveBookmark().catch((e) => this._handleSaveError(e)); + } + if (this.onCloseWithoutSaving && this._closeWithoutSaving) { + this.onCloseWithoutSaving(); + } + }, + + showExistingReminderAt: notEmpty("model.reminderAt"), + showDelete: notEmpty("model.id"), + userHasTimezoneSet: notEmpty("userTimezone"), + showPostLocalDate: or("postDetectedLocalDate", "postDetectedLocalTime"), + editingExistingBookmark: and("model", "model.id"), + existingBookmarkHasReminder: and("model", "model.reminderAt"), + + @discourseComputed() + autoDeletePreferences: () => { + return Object.keys(AUTO_DELETE_PREFERENCES).map((key) => { + return { + id: AUTO_DELETE_PREFERENCES[key], + name: I18n.t(`bookmarks.auto_delete_preference.${key.toLowerCase()}`), + }; + }); + }, + + @discourseComputed() + customTimeShortcutOptions() { + let customOptions = []; + + if (this.showPostLocalDate) { + customOptions.push({ + icon: "globe-americas", + id: TIME_SHORTCUT_TYPES.POST_LOCAL_DATE, + label: "bookmarks.reminders.post_local_date", + time: this._postLocalDate(), + timeFormatted: this._postLocalDate().format( + I18n.t("dates.long_no_year") + ), + hidden: false, + }); + } + + return customOptions; + }, + + @discourseComputed() + additionalTimeShortcutOptions() { + let additional = []; + + let later = laterToday(this.userTimezone); + if ( + !later.isSame(tomorrow(this.userTimezone), "date") && + now(this.userTimezone).hour() < LATER_TODAY_CUTOFF_HOUR + ) { + additional.push(TIME_SHORTCUT_TYPES.LATER_TODAY); + } + + if (now(this.userTimezone).day() < MOMENT_THURSDAY) { + additional.push(TIME_SHORTCUT_TYPES.LATER_THIS_WEEK); + } + + return additional; + }, + + @discourseComputed("model.reminderAt") + existingReminderAtFormatted(existingReminderAt) { + return formattedReminderTime(existingReminderAt, this.userTimezone); + }, + + @action + saveAndClose() { + if (this._saving || this._deleting) { + return; + } + + this._saving = true; + this._savingBookmarkManually = true; + return this._saveBookmark() + .then(() => this.closeModal()) + .catch((e) => this._handleSaveError(e)) + .finally(() => (this._saving = false)); + }, + + @action + toggleOptionsPanel() { + if (this.showOptions) { + $(".bookmark-options-panel").slideUp("fast"); + } else { + $(".bookmark-options-panel").slideDown("fast"); + } + this.toggleProperty("showOptions"); + }, + + @action + delete() { + this._deleting = true; + let deleteAction = () => { + this._closeWithoutSaving = true; + this._deleteBookmark() + .then(() => { + this._deleting = false; + this.closeModal(); + }) + .catch((e) => this._handleSaveError(e)); + }; + + if (this.existingBookmarkHasReminder) { + bootbox.confirm(I18n.t("bookmarks.confirm_delete"), (result) => { + if (result) { + deleteAction(); + } + }); + } else { + deleteAction(); + } + }, + + @action + closeWithoutSavingBookmark() { + this._closeWithoutSaving = true; + this.closeModal(); + }, + + @action + onTimeSelected(type, time) { + this.setProperties({ selectedReminderType: type, selectedDatetime: time }); + + // if the type is custom, we need to wait for the user to click save, as + // they could still be adjusting the date and time + if (type !== TIME_SHORTCUT_TYPES.CUSTOM) { + return this.saveAndClose(); + } + }, + + @action + selectPostLocalDate(date) { + this.setProperties({ + selectedReminderType: this.reminderTypes.POST_LOCAL_DATE, + postLocalDate: date, + }); + return this.saveAndClose(); + }, +}); diff --git a/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js new file mode 100644 index 00000000000..cdd1be06a19 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js @@ -0,0 +1,242 @@ +import { + START_OF_DAY_HOUR, + laterToday, + now, + parseCustomDatetime, +} from "discourse/lib/time-utils"; +import { + TIME_SHORTCUT_TYPES, + defaultShortcutOptions, +} from "discourse/lib/time-shortcut"; +import discourseComputed, { + observes, + on, +} from "discourse-common/utils/decorators"; + +import Component from "@ember/component"; +import I18n from "I18n"; +import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts"; +import { action } from "@ember/object"; +import { and, equal } from "@ember/object/computed"; + +// global shortcuts that interfere with these modal shortcuts, they are rebound when the +// component is destroyed +// +// c createTopic +// r replyToPost +// l toggle like +// t replyAsNewTopic +const GLOBAL_SHORTCUTS_TO_PAUSE = ["c", "r", "l", "t"]; +const BINDINGS = { + "l t": { + handler: "selectShortcut", + args: [TIME_SHORTCUT_TYPES.LATER_TODAY], + }, + "l w": { + handler: "selectShortcut", + args: [TIME_SHORTCUT_TYPES.LATER_THIS_WEEK], + }, + "n d": { + handler: "selectShortcut", + args: [TIME_SHORTCUT_TYPES.TOMORROW], + }, + "n w": { + handler: "selectShortcut", + args: [TIME_SHORTCUT_TYPES.NEXT_WEEK], + }, + "n b w": { + handler: "selectShortcut", + args: [TIME_SHORTCUT_TYPES.START_OF_NEXT_BUSINESS_WEEK], + }, + "n m": { + handler: "selectShortcut", + args: [TIME_SHORTCUT_TYPES.NEXT_MONTH], + }, + "c r": { handler: "selectShortcut", args: [TIME_SHORTCUT_TYPES.CUSTOM] }, + "n r": { handler: "selectShortcut", args: [TIME_SHORTCUT_TYPES.NONE] }, +}; + +export default Component.extend({ + tagName: "", + + userTimezone: null, + + onTimeSelected: null, + + selectedShortcut: null, + selectedTime: null, + selectedDate: null, + selectedDatetime: null, + prefilledDatetime: null, + + additionalOptionsToShow: null, + hiddenOptions: null, + customOptions: null, + + lastCustomDate: null, + lastCustomTime: null, + parsedLastCustomDatetime: null, + customDate: null, + customTime: null, + + defaultCustomReminderTime: `0${START_OF_DAY_HOUR}:00`, + + @on("init") + _setupPicker() { + this.setProperties({ + customTime: this.defaultCustomReminderTime, + userTimezone: this.currentUser.resolvedTimezone(this.currentUser), + additionalOptionsToShow: this.additionalOptionsToShow || [], + hiddenOptions: this.hiddenOptions || [], + customOptions: this.customOptions || [], + }); + + if (this.prefilledDatetime) { + let parsedDatetime = parseCustomDatetime( + this.prefilledDatetime, + null, + this.userTimezone + ); + + if (parsedDatetime.isSame(laterToday())) { + return this.set("selectedShortcut", TIME_SHORTCUT_TYPES.LATER_TODAY); + } + + this.setProperties({ + customDate: parsedDatetime.format("YYYY-MM-DD"), + customTime: parsedDatetime.format("HH:mm"), + selectedShortcut: TIME_SHORTCUT_TYPES.CUSTOM, + }); + } + + this._bindKeyboardShortcuts(); + this._loadLastUsedCustomDatetime(); + }, + + @on("willDestroyElement") + _resetKeyboardShortcuts() { + KeyboardShortcuts.unbind(BINDINGS); + KeyboardShortcuts.unpause(GLOBAL_SHORTCUTS_TO_PAUSE); + }, + + _loadLastUsedCustomDatetime() { + let lastTime = localStorage.lastCustomTime; + let lastDate = localStorage.lastCustomDate; + + if (lastTime && lastDate) { + let parsed = parseCustomDatetime(lastDate, lastTime, this.userTimezone); + + if (parsed < now(this.userTimezone)) { + return; + } + + this.setProperties({ + lastCustomDate: lastDate, + lastCustomTime: lastTime, + parsedLastCustomDatetime: parsed, + }); + } + }, + + _bindKeyboardShortcuts() { + KeyboardShortcuts.pause(GLOBAL_SHORTCUTS_TO_PAUSE); + Object.keys(BINDINGS).forEach((shortcut) => { + KeyboardShortcuts.addShortcut(shortcut, () => { + let binding = BINDINGS[shortcut]; + if (binding.args) { + return this.send(binding.handler, ...binding.args); + } + this.send(binding.handler); + }); + }); + }, + + customDatetimeSelected: equal("selectedShortcut", TIME_SHORTCUT_TYPES.CUSTOM), + customDatetimeFilled: and("customDate", "customTime"), + + @observes("customDate", "customTime") + customDatetimeChanged() { + if (!this.customDatetimeFilled) { + return; + } + this.selectShortcut(TIME_SHORTCUT_TYPES.CUSTOM); + }, + + @discourseComputed( + "additionalOptionsToShow", + "hiddenOptions", + "customOptions", + "userTimezone" + ) + options(additionalOptionsToShow, hiddenOptions, customOptions, userTimezone) { + let options = defaultShortcutOptions(userTimezone); + + if (additionalOptionsToShow.length > 0) { + options.forEach((opt) => { + if (additionalOptionsToShow.includes(opt.id)) { + opt.hidden = false; + } + }); + } + + if (hiddenOptions.length > 0) { + options.forEach((opt) => { + if (hiddenOptions.includes(opt.id)) { + opt.hidden = true; + } + }); + } + + if (this.lastCustomDate && this.lastCustomTime) { + let lastCustom = options.findBy("id", TIME_SHORTCUT_TYPES.LAST_CUSTOM); + lastCustom.time = this.parsedLastCustomDatetime; + lastCustom.timeFormatted = this.parsedLastCustomDatetime.format( + I18n.t("dates.long_no_year") + ); + lastCustom.hidden = false; + } + + let customOptionIndex = options.findIndex( + (opt) => opt.id === TIME_SHORTCUT_TYPES.CUSTOM + ); + + options.splice(customOptionIndex, 0, ...customOptions); + + return options; + }, + + @action + selectShortcut(type) { + if (this.options.filterBy("hidden").mapBy("id").includes(type)) { + return; + } + + let dateTime = null; + if (type === TIME_SHORTCUT_TYPES.CUSTOM) { + this.set("customTime", this.customTime || this.defaultCustomReminderTime); + const customDatetime = parseCustomDatetime( + this.customDate, + this.customTime, + this.userTimezone + ); + + if (customDatetime.isValid()) { + dateTime = customDatetime; + + localStorage.lastCustomTime = this.customTime; + localStorage.lastCustomDate = this.customDate; + } + } else { + dateTime = this.options.findBy("id", type).time; + } + + this.setProperties({ + selectedShortcut: type, + selectedDatetime: dateTime, + }); + + if (this.onTimeSelected) { + this.onTimeSelected(type, dateTime); + } + }, +}); diff --git a/app/assets/javascripts/discourse/app/controllers/bookmark.js b/app/assets/javascripts/discourse/app/controllers/bookmark.js index ec0e0010bbc..6b57d740c7c 100644 --- a/app/assets/javascripts/discourse/app/controllers/bookmark.js +++ b/app/assets/javascripts/discourse/app/controllers/bookmark.js @@ -1,105 +1,18 @@ -import { REMINDER_TYPES, formattedReminderTime } from "discourse/lib/bookmark"; -import { isEmpty, isPresent } from "@ember/utils"; -import { next, schedule } from "@ember/runloop"; -import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark"; import Controller from "@ember/controller"; -import I18n from "I18n"; -import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts"; import ModalFunctionality from "discourse/mixins/modal-functionality"; -import { Promise } from "rsvp"; import { action } from "@ember/object"; -import { ajax } from "discourse/lib/ajax"; -import { and } from "@ember/object/computed"; -import bootbox from "bootbox"; -import discourseComputed from "discourse-common/utils/decorators"; -import { popupAjaxError } from "discourse/lib/ajax-error"; - -// global shortcuts that interfere with these modal shortcuts, they are rebound when the -// modal is closed -// -// c createTopic -// r replyToPost -// l toggle like -// d deletePost -// t replyAsNewTopic -const GLOBAL_SHORTCUTS_TO_PAUSE = ["c", "r", "l", "d", "t"]; -const START_OF_DAY_HOUR = 8; -const LATER_TODAY_CUTOFF_HOUR = 17; -const LATER_TODAY_MAX_HOUR = 18; -const MOMENT_MONDAY = 1; -const MOMENT_THURSDAY = 4; -const BOOKMARK_BINDINGS = { - enter: { handler: "saveAndClose" }, - "l t": { handler: "selectReminderType", args: [REMINDER_TYPES.LATER_TODAY] }, - "l w": { - handler: "selectReminderType", - args: [REMINDER_TYPES.LATER_THIS_WEEK], - }, - "n b d": { - handler: "selectReminderType", - args: [REMINDER_TYPES.NEXT_BUSINESS_DAY], - }, - "n d": { handler: "selectReminderType", args: [REMINDER_TYPES.TOMORROW] }, - "n w": { handler: "selectReminderType", args: [REMINDER_TYPES.NEXT_WEEK] }, - "n b w": { - handler: "selectReminderType", - args: [REMINDER_TYPES.START_OF_NEXT_BUSINESS_WEEK], - }, - "n m": { handler: "selectReminderType", args: [REMINDER_TYPES.NEXT_MONTH] }, - "c r": { handler: "selectReminderType", args: [REMINDER_TYPES.CUSTOM] }, - "n r": { handler: "selectReminderType", args: [REMINDER_TYPES.NONE] }, - "d d": { handler: "delete" }, -}; export default Controller.extend(ModalFunctionality, { - loading: false, - errorMessage: null, - selectedReminderType: null, - _closeWithoutSaving: false, - _savingBookmarkManually: false, - onCloseWithoutSaving: null, - customReminderDate: null, - customReminderTime: null, - lastCustomReminderDate: null, - lastCustomReminderTime: null, - postLocalDate: null, - mouseTrap: null, - userTimezone: null, - showOptions: false, - onShow() { this.setProperties({ - errorMessage: null, - selectedReminderType: REMINDER_TYPES.NONE, - _closeWithoutSaving: false, - _savingBookmarkManually: false, - customReminderDate: null, - customReminderTime: this._defaultCustomReminderTime(), - lastCustomReminderDate: null, - lastCustomReminderTime: null, - postDetectedLocalDate: null, - postDetectedLocalTime: null, - postDetectedLocalTimezone: null, - userTimezone: this.currentUser.resolvedTimezone(this.currentUser), - showOptions: false, model: this.model || {}, + allowSave: true, }); + }, - this._loadBookmarkOptions(); - this._bindKeyboardShortcuts(); - this._loadLastUsedCustomReminderDatetime(); - - if (this._editingExistingBookmark()) { - this._initializeExistingBookmarkData(); - } - - this.loadLocalDates(); - - schedule("afterRender", () => { - if (this.site.isMobileDevice) { - document.getElementById("bookmark-name").blur(); - } - }); + @action + registerOnCloseHandler(handlerFn) { + this.set("onCloseHandler", handlerFn); }, /** @@ -107,473 +20,8 @@ export default Controller.extend(ModalFunctionality, { * clicks the save or cancel button to mimic browser behaviour. */ onClose(opts = {}) { - if (opts.initiatedByCloseButton) { - this._closeWithoutSaving = true; + if (this.onCloseHandler) { + this.onCloseHandler(opts.initiatedByCloseButton); } - - this._unbindKeyboardShortcuts(); - this._restoreGlobalShortcuts(); - if (!this._closeWithoutSaving && !this._savingBookmarkManually) { - this._saveBookmark().catch((e) => this._handleSaveError(e)); - } - if (this.onCloseWithoutSaving && this._closeWithoutSaving) { - this.onCloseWithoutSaving(); - } - }, - - _initializeExistingBookmarkData() { - if (this._existingBookmarkHasReminder()) { - let parsedReminderAt = this._parseCustomDateTime(this.model.reminderAt); - - if (parsedReminderAt.isSame(this.laterToday())) { - return this.set("selectedReminderType", REMINDER_TYPES.LATER_TODAY); - } - - this.setProperties({ - customReminderDate: parsedReminderAt.format("YYYY-MM-DD"), - customReminderTime: parsedReminderAt.format("HH:mm"), - selectedReminderType: REMINDER_TYPES.CUSTOM, - }); - } - }, - - _editingExistingBookmark() { - return isPresent(this.model) && isPresent(this.model.id); - }, - - _existingBookmarkHasReminder() { - return isPresent(this.model) && isPresent(this.model.reminderAt); - }, - - _loadBookmarkOptions() { - this.set( - "autoDeletePreference", - this.model.autoDeletePreference || this._preferredDeleteOption() || 0 - ); - - // we want to make sure the options panel opens so the user - // knows they have set these options previously. run next otherwise - // the modal is not visible when it tries to slide down the options - if (this.autoDeletePreference) { - next(() => this.toggleOptionsPanel()); - } - }, - - _preferredDeleteOption() { - let preferred = localStorage.bookmarkDeleteOption; - if (preferred && preferred !== "") { - preferred = parseInt(preferred, 10); - } - return preferred; - }, - - _loadLastUsedCustomReminderDatetime() { - let lastTime = localStorage.lastCustomBookmarkReminderTime; - let lastDate = localStorage.lastCustomBookmarkReminderDate; - - if (lastTime && lastDate) { - let parsed = this._parseCustomDateTime(lastDate, lastTime); - - // can't set reminders in the past - if (parsed < this.now()) { - return; - } - - this.setProperties({ - lastCustomReminderDate: lastDate, - lastCustomReminderTime: lastTime, - parsedLastCustomReminderDatetime: parsed, - }); - } - }, - - _bindKeyboardShortcuts() { - KeyboardShortcuts.pause(GLOBAL_SHORTCUTS_TO_PAUSE); - Object.keys(BOOKMARK_BINDINGS).forEach((shortcut) => { - KeyboardShortcuts.addShortcut(shortcut, () => { - let binding = BOOKMARK_BINDINGS[shortcut]; - if (binding.args) { - return this.send(binding.handler, ...binding.args); - } - this.send(binding.handler); - }); - }); - }, - - _unbindKeyboardShortcuts() { - KeyboardShortcuts.unbind(BOOKMARK_BINDINGS); - }, - - _restoreGlobalShortcuts() { - KeyboardShortcuts.unpause(GLOBAL_SHORTCUTS_TO_PAUSE); - }, - - @discourseComputed("model.reminderAt") - showExistingReminderAt(existingReminderAt) { - return isPresent(existingReminderAt); - }, - - @discourseComputed("model.id") - showDelete(id) { - return isPresent(id); - }, - - @discourseComputed("selectedReminderType") - customDateTimeSelected(selectedReminderType) { - return selectedReminderType === REMINDER_TYPES.CUSTOM; - }, - - @discourseComputed() - reminderTypes: () => { - return REMINDER_TYPES; - }, - - @discourseComputed() - autoDeletePreferences: () => { - return Object.keys(AUTO_DELETE_PREFERENCES).map((key) => { - return { - id: AUTO_DELETE_PREFERENCES[key], - name: I18n.t(`bookmarks.auto_delete_preference.${key.toLowerCase()}`), - }; - }); - }, - - showLastCustom: and("lastCustomReminderTime", "lastCustomReminderDate"), - - get showLaterToday() { - let later = this.laterToday(); - return ( - !later.isSame(this.tomorrow(), "date") && - this.now().hour() < LATER_TODAY_CUTOFF_HOUR - ); - }, - - get showLaterThisWeek() { - return this.now().day() < MOMENT_THURSDAY; - }, - - @discourseComputed("parsedLastCustomReminderDatetime") - lastCustomFormatted(parsedLastCustomReminderDatetime) { - return parsedLastCustomReminderDatetime.format( - I18n.t("dates.long_no_year") - ); - }, - - @discourseComputed("model.reminderAt") - existingReminderAtFormatted(existingReminderAt) { - return formattedReminderTime(existingReminderAt, this.userTimezone); - }, - - get startNextBusinessWeekLabel() { - if (this.now().day() === MOMENT_MONDAY) { - return I18n.t("bookmarks.reminders.start_of_next_business_week_alt"); - } - return I18n.t("bookmarks.reminders.start_of_next_business_week"); - }, - - get startNextBusinessWeekFormatted() { - return this.nextWeek() - .day(MOMENT_MONDAY) - .format(I18n.t("dates.long_no_year")); - }, - - get laterTodayFormatted() { - return this.laterToday().format(I18n.t("dates.time")); - }, - - get tomorrowFormatted() { - return this.tomorrow().format(I18n.t("dates.time_short_day")); - }, - - get nextWeekFormatted() { - return this.nextWeek().format(I18n.t("dates.long_no_year")); - }, - - get laterThisWeekFormatted() { - return this.laterThisWeek().format(I18n.t("dates.time_short_day")); - }, - - get nextMonthFormatted() { - return this.nextMonth().format(I18n.t("dates.long_no_year")); - }, - - loadLocalDates() { - let postEl = document.querySelector( - `[data-post-id="${this.model.postId}"]` - ); - let localDateEl = null; - if (postEl) { - localDateEl = postEl.querySelector(".discourse-local-date"); - } - - if (localDateEl) { - this.setProperties({ - postDetectedLocalDate: localDateEl.dataset.date, - postDetectedLocalTime: localDateEl.dataset.time, - postDetectedLocalTimezone: localDateEl.dataset.timezone, - }); - } - }, - - @discourseComputed("userTimezone") - userHasTimezoneSet(userTimezone) { - return !isEmpty(userTimezone); - }, - - _saveBookmark() { - const reminderAt = this._reminderAt(); - const reminderAtISO = reminderAt ? reminderAt.toISOString() : null; - - if (this.selectedReminderType === REMINDER_TYPES.CUSTOM) { - if (!reminderAt) { - return Promise.reject(I18n.t("bookmarks.invalid_custom_datetime")); - } - - localStorage.lastCustomBookmarkReminderTime = this.customReminderTime; - localStorage.lastCustomBookmarkReminderDate = this.customReminderDate; - } - - localStorage.bookmarkDeleteOption = this.autoDeletePreference; - - let reminderType; - if (this.selectedReminderType === REMINDER_TYPES.NONE) { - reminderType = null; - } else if ( - this.selectedReminderType === REMINDER_TYPES.LAST_CUSTOM || - this.selectedReminderType === REMINDER_TYPES.POST_LOCAL_DATE - ) { - reminderType = REMINDER_TYPES.CUSTOM; - } else { - reminderType = this.selectedReminderType; - } - - const data = { - reminder_type: reminderType, - reminder_at: reminderAtISO, - name: this.model.name, - post_id: this.model.postId, - id: this.model.id, - auto_delete_preference: this.autoDeletePreference, - }; - - if (this._editingExistingBookmark()) { - return ajax("/bookmarks/" + this.model.id, { - type: "PUT", - data, - }).then(() => { - if (this.afterSave) { - this.afterSave({ - reminderAt: reminderAtISO, - reminderType: this.selectedReminderType, - autoDeletePreference: this.autoDeletePreference, - id: this.model.id, - name: this.model.name, - }); - } - }); - } else { - return ajax("/bookmarks", { type: "POST", data }).then((response) => { - if (this.afterSave) { - this.afterSave({ - reminderAt: reminderAtISO, - reminderType: this.selectedReminderType, - autoDeletePreference: this.autoDeletePreference, - id: response.id, - name: this.model.name, - }); - } - }); - } - }, - - _deleteBookmark() { - return ajax("/bookmarks/" + this.model.id, { - type: "DELETE", - }).then((response) => { - if (this.afterDelete) { - this.afterDelete(response.topic_bookmarked); - } - }); - }, - - _parseCustomDateTime(date, time, parseTimezone = this.userTimezone) { - let dateTime = isPresent(time) ? date + " " + time : date; - let parsed = moment.tz(dateTime, parseTimezone); - - if (parseTimezone !== this.userTimezone) { - parsed = parsed.tz(this.userTimezone); - } - - return parsed; - }, - - _defaultCustomReminderTime() { - return `0${START_OF_DAY_HOUR}:00`; - }, - - _reminderAt() { - if (!this.selectedReminderType) { - return; - } - - switch (this.selectedReminderType) { - case REMINDER_TYPES.LATER_TODAY: - return this.laterToday(); - case REMINDER_TYPES.NEXT_BUSINESS_DAY: - return this.nextBusinessDay(); - case REMINDER_TYPES.TOMORROW: - return this.tomorrow(); - case REMINDER_TYPES.NEXT_WEEK: - return this.nextWeek(); - case REMINDER_TYPES.START_OF_NEXT_BUSINESS_WEEK: - return this.nextWeek().day(MOMENT_MONDAY); - case REMINDER_TYPES.LATER_THIS_WEEK: - return this.laterThisWeek(); - case REMINDER_TYPES.NEXT_MONTH: - return this.nextMonth(); - case REMINDER_TYPES.CUSTOM: - this.set( - "customReminderTime", - this.customReminderTime || this._defaultCustomReminderTime() - ); - const customDateTime = this._parseCustomDateTime( - this.customReminderDate, - this.customReminderTime - ); - if (!customDateTime.isValid()) { - this.setProperties({ - customReminderTime: null, - customReminderDate: null, - }); - return; - } - return customDateTime; - case REMINDER_TYPES.LAST_CUSTOM: - return this.parsedLastCustomReminderDatetime; - case REMINDER_TYPES.POST_LOCAL_DATE: - return this.postLocalDate; - } - }, - - nextWeek() { - return this.startOfDay(this.now().add(7, "days")); - }, - - nextMonth() { - return this.startOfDay(this.now().add(1, "month")); - }, - - tomorrow() { - return this.startOfDay(this.now().add(1, "day")); - }, - - startOfDay(momentDate) { - return momentDate.hour(START_OF_DAY_HOUR).startOf("hour"); - }, - - now() { - return moment.tz(this.userTimezone); - }, - - laterToday() { - let later = this.now().add(3, "hours"); - if (later.hour() >= LATER_TODAY_MAX_HOUR) { - return later.hour(LATER_TODAY_MAX_HOUR).startOf("hour"); - } - return later.minutes() < 30 - ? later.startOf("hour") - : later.add(30, "minutes").startOf("hour"); - }, - - laterThisWeek() { - if (!this.showLaterThisWeek) { - return; - } - return this.startOfDay(this.now().add(2, "days")); - }, - - _handleSaveError(e) { - this._savingBookmarkManually = false; - if (typeof e === "string") { - bootbox.alert(e); - } else { - popupAjaxError(e); - } - }, - - @action - toggleOptionsPanel() { - if (this.showOptions) { - $(".bookmark-options-panel").slideUp("fast"); - } else { - $(".bookmark-options-panel").slideDown("fast"); - } - this.toggleProperty("showOptions"); - }, - - @action - saveAndClose() { - if (this._saving || this._deleting) { - return; - } - - this._saving = true; - this._savingBookmarkManually = true; - return this._saveBookmark() - .then(() => this.send("closeModal")) - .catch((e) => this._handleSaveError(e)) - .finally(() => (this._saving = false)); - }, - - @action - delete() { - this._deleting = true; - let deleteAction = () => { - this._closeWithoutSaving = true; - this._deleteBookmark() - .then(() => { - this._deleting = false; - this.send("closeModal"); - }) - .catch((e) => this._handleSaveError(e)); - }; - - if (this._existingBookmarkHasReminder()) { - bootbox.confirm(I18n.t("bookmarks.confirm_delete"), (result) => { - if (result) { - deleteAction(); - } - }); - } else { - deleteAction(); - } - }, - - @action - closeWithoutSavingBookmark() { - this._closeWithoutSaving = true; - this.send("closeModal"); - }, - - @action - selectReminderType(type) { - if (type === REMINDER_TYPES.LATER_TODAY && !this.showLaterToday) { - return; - } - - this.set("selectedReminderType", type); - - if (type !== REMINDER_TYPES.CUSTOM) { - return this.saveAndClose(); - } - }, - - @action - selectPostLocalDate(date) { - this.setProperties({ - selectedReminderType: this.reminderTypes.POST_LOCAL_DATE, - postLocalDate: date, - }); - return this.saveAndClose(); }, }); diff --git a/app/assets/javascripts/discourse/app/lib/bookmark.js b/app/assets/javascripts/discourse/app/lib/bookmark.js index 8fd496083cd..3ed24cfb111 100644 --- a/app/assets/javascripts/discourse/app/lib/bookmark.js +++ b/app/assets/javascripts/discourse/app/lib/bookmark.js @@ -16,17 +16,3 @@ export function formattedReminderTime(reminderAt, timezone) { date_time: reminderAtDate.format(I18n.t("dates.long_with_year")), }); } - -export const REMINDER_TYPES = { - LATER_TODAY: "later_today", - NEXT_BUSINESS_DAY: "next_business_day", - TOMORROW: "tomorrow", - NEXT_WEEK: "next_week", - NEXT_MONTH: "next_month", - CUSTOM: "custom", - LAST_CUSTOM: "last_custom", - NONE: "none", - START_OF_NEXT_BUSINESS_WEEK: "start_of_next_business_week", - LATER_THIS_WEEK: "later_this_week", - POST_LOCAL_DATE: "post_local_date", -}; diff --git a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js index eda349b253e..8a366ffc6a8 100644 --- a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js +++ b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js @@ -124,7 +124,15 @@ export default { this.container = null; }, + isTornDown() { + return this.keyTrapper == null || this.container == null; + }, + bindKey(key, binding = null) { + if (this.isTornDown()) { + return; + } + if (!binding) { binding = DEFAULT_BINDINGS[key]; } @@ -152,11 +160,21 @@ export default { // for cases when you want to disable global keyboard shortcuts // so that you can override them (e.g. inside a modal) pause(combinations) { + if (this.isTornDown()) { + return; + } combinations.forEach((combo) => this.keyTrapper.unbind(combo)); }, // restore global shortcuts that you have paused unpause(combinations) { + if (this.isTornDown()) { + return; + } + // if the keytrapper has already been torn down this will error + if (this.keyTrapper == null) { + return; + } combinations.forEach((combo) => this.bindKey(combo)); }, diff --git a/app/assets/javascripts/discourse/app/lib/time-shortcut.js b/app/assets/javascripts/discourse/app/lib/time-shortcut.js new file mode 100644 index 00000000000..6c85d19fcc4 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/time-shortcut.js @@ -0,0 +1,103 @@ +import { + MOMENT_MONDAY, + laterThisWeek, + laterToday, + nextBusinessWeekStart, + nextMonth, + nextWeek, + now, + tomorrow, +} from "discourse/lib/time-utils"; +import I18n from "I18n"; + +export const TIME_SHORTCUT_TYPES = { + LATER_TODAY: "later_today", + TOMORROW: "tomorrow", + NEXT_WEEK: "next_week", + NEXT_MONTH: "next_month", + CUSTOM: "custom", + LAST_CUSTOM: "last_custom", + NONE: "none", + START_OF_NEXT_BUSINESS_WEEK: "start_of_next_business_week", + LATER_THIS_WEEK: "later_this_week", + POST_LOCAL_DATE: "post_local_date", +}; + +export function defaultShortcutOptions(timezone) { + return [ + { + icon: "angle-right", + id: TIME_SHORTCUT_TYPES.LATER_TODAY, + label: "time_shortcut.later_today", + time: laterToday(timezone), + timeFormatted: laterToday(timezone).format(I18n.t("dates.time")), + hidden: true, + }, + { + icon: "far-sun", + id: TIME_SHORTCUT_TYPES.TOMORROW, + label: "time_shortcut.tomorrow", + time: tomorrow(timezone), + timeFormatted: tomorrow(timezone).format(I18n.t("dates.time_short_day")), + }, + { + icon: "angle-double-right", + id: TIME_SHORTCUT_TYPES.LATER_THIS_WEEK, + label: "time_shortcut.later_this_week", + time: laterThisWeek(timezone), + timeFormatted: laterThisWeek(timezone).format( + I18n.t("dates.time_short_day") + ), + hidden: true, + }, + { + icon: "briefcase", + id: TIME_SHORTCUT_TYPES.START_OF_NEXT_BUSINESS_WEEK, + label: + now(timezone).day() === MOMENT_MONDAY + ? "time_shortcut.start_of_next_business_week_alt" + : "time_shortcut.start_of_next_business_week", + time: nextBusinessWeekStart(timezone), + timeFormatted: nextBusinessWeekStart(timezone).format( + I18n.t("dates.long_no_year") + ), + }, + { + icon: "far-clock", + id: TIME_SHORTCUT_TYPES.NEXT_WEEK, + label: "time_shortcut.next_week", + time: nextWeek(timezone), + timeFormatted: nextWeek(timezone).format(I18n.t("dates.long_no_year")), + }, + { + icon: "far-calendar-plus", + id: TIME_SHORTCUT_TYPES.NEXT_MONTH, + label: "time_shortcut.next_month", + time: nextMonth(timezone), + timeFormatted: nextMonth(timezone).format(I18n.t("dates.long_no_year")), + }, + { + icon: "calendar-alt", + id: TIME_SHORTCUT_TYPES.CUSTOM, + label: "time_shortcut.custom", + time: null, + timeFormatted: null, + isCustomTimeShortcut: true, + }, + { + icon: "undo", + id: TIME_SHORTCUT_TYPES.LAST_CUSTOM, + label: "time_shortcut.last_custom", + time: null, + timeFormatted: null, + hidden: true, + }, + { + icon: "ban", + id: TIME_SHORTCUT_TYPES.NONE, + label: "time_shortcut.none", + time: null, + timeFormatted: null, + }, + ]; +} diff --git a/app/assets/javascripts/discourse/app/lib/time-utils.js b/app/assets/javascripts/discourse/app/lib/time-utils.js new file mode 100644 index 00000000000..99a7ce504f7 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/time-utils.js @@ -0,0 +1,63 @@ +import { isPresent } from "@ember/utils"; + +export const START_OF_DAY_HOUR = 8; +export const LATER_TODAY_CUTOFF_HOUR = 17; +export const LATER_TODAY_MAX_HOUR = 18; +export const MOMENT_MONDAY = 1; +export const MOMENT_THURSDAY = 4; + +export function now(timezone) { + return moment.tz(timezone); +} + +export function startOfDay(momentDate, startOfDayHour = START_OF_DAY_HOUR) { + return momentDate.hour(startOfDayHour).startOf("hour"); +} + +export function tomorrow(timezone) { + return startOfDay(now(timezone).add(1, "day")); +} + +export function laterToday(timezone) { + let later = now(timezone).add(3, "hours"); + if (later.hour() >= LATER_TODAY_MAX_HOUR) { + return later.hour(LATER_TODAY_MAX_HOUR).startOf("hour"); + } + return later.minutes() < 30 + ? later.startOf("hour") + : later.add(30, "minutes").startOf("hour"); +} + +export function laterThisWeek(timezone) { + return startOfDay(now(timezone).add(2, "days")); +} + +export function nextWeek(timezone) { + return startOfDay(now(timezone).add(7, "days")); +} + +export function nextMonth(timezone) { + return startOfDay(now(timezone).add(1, "month")); +} + +export function nextBusinessWeekStart(timezone) { + return nextWeek(timezone).day(MOMENT_MONDAY); +} + +export function parseCustomDatetime( + date, + time, + currentTimezone, + parseTimezone = null +) { + let dateTime = isPresent(time) ? `${date} ${time}` : date; + parseTimezone = parseTimezone || currentTimezone; + + let parsed = moment.tz(dateTime, parseTimezone); + + if (parseTimezone !== currentTimezone) { + parsed = parsed.tz(currentTimezone); + } + + return parsed; +} diff --git a/app/assets/javascripts/discourse/app/templates/components/bookmark-local-date.hbs b/app/assets/javascripts/discourse/app/templates/components/bookmark-local-date.hbs deleted file mode 100644 index a30363826de..00000000000 --- a/app/assets/javascripts/discourse/app/templates/components/bookmark-local-date.hbs +++ /dev/null @@ -1,6 +0,0 @@ -{{#if showPostLocalDate}} - {{#tap-tile icon="globe-americas" tileId=tileId activeTile=activeTile onChange=(action "setReminder")}} -
{{i18n "bookmarks.reminders.post_local_date"}}
-
{{postLocalDateFormatted}}
- {{/tap-tile}} -{{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs b/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs new file mode 100644 index 00000000000..f2dace34511 --- /dev/null +++ b/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs @@ -0,0 +1,53 @@ +{{#conditional-loading-spinner condition=loading}} + {{#if errorMessage}} +
+
+
{{errorMessage}}
+
+
+ {{/if}} + +
+ {{input id="bookmark-name" value=model.name name="bookmark-name" class="bookmark-name" enter=(action "saveAndClose") placeholder=(i18n "post.bookmarks.name_placeholder") maxlength="100"}} + {{d-button icon="cog" action=(action "toggleOptionsPanel") class="bookmark-options-button"}} +
+ +
+ + {{combo-box + content=autoDeletePreferences + value=autoDeletePreference + class="bookmark-option-selector" + onChange=(action (mut autoDeletePreference)) + }} +
+ + {{#if showExistingReminderAt }} +
+ {{d-icon "far-clock"}} + {{i18n "bookmarks.reminders.existing_reminder" at_date_time=existingReminderAtFormatted}} +
+ {{/if}} + +
+ + + {{#if userHasTimezoneSet}} + {{time-shortcut-picker prefilledDatetime=prefilledDatetime onTimeSelected=(action "onTimeSelected") customOptions=customTimeShortcutOptions additionalOptionsToShow=additionalTimeShortcutOptions}} + {{else}} +
{{html-safe (i18n "bookmarks.no_timezone" basePath=(base-path))}}
+ {{/if}} +
+ +
+ {{d-button id="save-bookmark" label="bookmarks.save" class="btn-primary" action=(action "saveAndClose")}} + {{d-modal-cancel close=(action "closeWithoutSavingBookmark")}} + {{#if showDelete}} +
+ {{d-button id="delete-bookmark" icon="trash-alt" class="btn-danger" action=(action "delete")}} +
+ {{/if}} +
+{{/conditional-loading-spinner}} diff --git a/app/assets/javascripts/discourse/app/templates/components/time-shortcut-picker.hbs b/app/assets/javascripts/discourse/app/templates/components/time-shortcut-picker.hbs new file mode 100644 index 00000000000..69d6fdf9bd1 --- /dev/null +++ b/app/assets/javascripts/discourse/app/templates/components/time-shortcut-picker.hbs @@ -0,0 +1,29 @@ +{{#tap-tile-grid activeTile=selectedShortcut as |grid|}} + {{#each options as |option|}} + {{#unless option.hidden}} + {{#tap-tile icon=option.icon tileId=option.id activeTile=grid.activeTile onChange=(action "selectShortcut")}} +
{{i18n option.label}}
+
{{option.timeFormatted}}
+ {{/tap-tile}} + {{/unless}} + + {{#if option.isCustomTimeShortcut}} + {{#if customDatetimeSelected}} +
+
+ {{d-icon "calendar-alt"}} + {{date-picker-future + value=customDate + onSelect=(action (mut customDate)) + id="custom-date" + }} +
+
+ {{d-icon "far-clock"}} + {{input placeholder="--:--" id="custom-time" type="time" class="time-input" value=customTime}} +
+
+ {{/if}} + {{/if}} + {{/each}} +{{/tap-tile-grid}} diff --git a/app/assets/javascripts/discourse/app/templates/modal/bookmark.hbs b/app/assets/javascripts/discourse/app/templates/modal/bookmark.hbs index 46049bb9b7a..b50137ab303 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/bookmark.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/bookmark.hbs @@ -1,117 +1,8 @@ {{#d-modal-body id="bookmark-reminder-modal"}} - {{#conditional-loading-spinner condition=loading}} - {{#if errorMessage}} -
-
-
{{errorMessage}}
-
-
- {{/if}} - -
- {{input id="bookmark-name" value=model.name name="bookmark-name" class="bookmark-name" enter=(action "saveAndClose") placeholder=(i18n "post.bookmarks.name_placeholder") maxlength="100"}} - {{d-button icon="cog" action=(action "toggleOptionsPanel") class="bookmark-options-button"}} -
- -
- - {{combo-box - content=autoDeletePreferences - value=autoDeletePreference - class="bookmark-option-selector" - onChange=(action (mut autoDeletePreference)) - }} -
- - {{#if showExistingReminderAt }} -
- {{d-icon "far-clock"}} - {{i18n "bookmarks.reminders.existing_reminder" at_date_time=existingReminderAtFormatted}} -
- {{/if}} - -
- - - {{#if userHasTimezoneSet}} - {{#tap-tile-grid activeTile=selectedReminderType as |grid|}} - {{#if showLaterToday}} - {{#tap-tile icon="angle-right" tileId=reminderTypes.LATER_TODAY activeTile=grid.activeTile onChange=(action "selectReminderType")}} -
{{i18n "bookmarks.reminders.later_today"}}
-
{{laterTodayFormatted}}
- {{/tap-tile}} - {{/if}} - {{#tap-tile icon="far-sun" tileId=reminderTypes.TOMORROW activeTile=grid.activeTile onChange=(action "selectReminderType")}} -
{{i18n "bookmarks.reminders.tomorrow"}}
-
{{tomorrowFormatted}}
- {{/tap-tile}} - {{#if showLaterThisWeek}} - {{#tap-tile icon="angle-double-right" tileId=reminderTypes.LATER_THIS_WEEK activeTile=grid.activeTile onChange=(action "selectReminderType")}} -
{{i18n "bookmarks.reminders.later_this_week"}}
-
{{laterThisWeekFormatted}}
- {{/tap-tile}} - {{/if}} - {{#tap-tile icon="briefcase" tileId=reminderTypes.START_OF_NEXT_BUSINESS_WEEK activeTile=grid.activeTile onChange=(action "selectReminderType")}} -
{{startNextBusinessWeekLabel}}
-
{{startNextBusinessWeekFormatted}}
- {{/tap-tile}} - {{#tap-tile icon="far-clock" tileId=reminderTypes.NEXT_WEEK activeTile=grid.activeTile onChange=(action "selectReminderType")}} -
{{i18n "bookmarks.reminders.next_week"}}
-
{{nextWeekFormatted}}
- {{/tap-tile}} - {{#tap-tile icon="far-calendar-plus" tileId=reminderTypes.NEXT_MONTH activeTile=grid.activeTile onChange=(action "selectReminderType")}} -
{{i18n "bookmarks.reminders.next_month"}}
-
{{nextMonthFormatted}}
- {{/tap-tile}} - {{bookmark-local-date postId=model.postId tileId=reminderTypes.POST_LOCAL_DATE activeTile=grid.activeTile onChange=(action "selectPostLocalDate")}} - {{#tap-tile icon="calendar-alt" tileId=reminderTypes.CUSTOM activeTile=grid.activeTile onChange=(action "selectReminderType")}} -
{{i18n "bookmarks.reminders.custom"}}
- {{/tap-tile}} - - {{#if customDateTimeSelected}} -
-
- {{d-icon "calendar-alt"}} - {{date-picker-future - value=customReminderDate - onSelect=(action (mut customReminderDate)) - id="bookmark-custom-date" - }} -
-
- {{d-icon "far-clock"}} - {{input placeholder="--:--" id="bookmark-custom-time" type="time" class="time-input" value=customReminderTime}} -
-
- {{/if}} - - {{#if showLastCustom}} - {{#tap-tile icon="undo" tileId=reminderTypes.LAST_CUSTOM activeTile=grid.activeTile onChange=(action "selectReminderType")}} -
{{i18n "bookmarks.reminders.last_custom"}}
-
{{lastCustomFormatted}}
- {{/tap-tile}} - {{/if}} - - {{#tap-tile icon="ban" tileId=reminderTypes.NONE activeTile=grid.activeTile onChange=(action "selectReminderType")}} -
{{i18n "bookmarks.reminders.none"}}
- {{/tap-tile}} - {{/tap-tile-grid}} - - {{else}} -
{{html-safe (i18n "bookmarks.no_timezone" basePath=(base-path))}}
- {{/if}} -
- -
- {{d-button id="save-bookmark" label="bookmarks.save" class="btn-primary" action=(action "saveAndClose")}} - {{d-modal-cancel close=(action "closeWithoutSavingBookmark")}} - {{#if showDelete}} -
- {{d-button id="delete-bookmark" icon="trash-alt" class="btn-danger" action=(action "delete")}} -
- {{/if}} -
- {{/conditional-loading-spinner}} + {{bookmark model=model + afterSave=afterSave + afterDelete=afterDelete + onCloseWithoutSaving=onCloseWithoutSaving + registerOnCloseHandler=(action "registerOnCloseHandler") + closeModal=(action "closeModal")}} {{/d-modal-body}} diff --git a/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js b/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js index cf0e225cdfc..e901ec7a82f 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/bookmarks-test.js @@ -4,11 +4,12 @@ import { loggedInUser, queryAll, } from "discourse/tests/helpers/qunit-helpers"; -import { click, fillIn, visit } from "@ember/test-helpers"; +import { click, fillIn, getApplication, visit } from "@ember/test-helpers"; import I18n from "I18n"; import selectKit from "discourse/tests/helpers/select-kit-helper"; import { test } from "qunit"; import topicFixtures from "discourse/tests/fixtures/topic"; +import KeyboardShortcutInitializer from "discourse/initializers/keyboard-shortcuts"; async function openBookmarkModal() { if (exists(".topic-post:first-child button.show-more-actions")) { @@ -25,7 +26,10 @@ acceptance("Bookmarking", function (needs) { needs.user(); let steps = []; - needs.hooks.beforeEach(() => (steps = [])); + needs.hooks.beforeEach(function () { + KeyboardShortcutInitializer.initialize(getApplication()); + steps = []; + }); const topicResponse = topicFixtures["/t/280/1.json"]; topicResponse.post_stream.posts[0].cooked += ` @@ -206,12 +210,12 @@ acceptance("Bookmarking", function (needs) { "it should prefill the bookmark name" ); assert.equal( - queryAll("#bookmark-custom-date > input").val(), + queryAll("#custom-date > input").val(), tomorrow, "it should prefill the bookmark date" ); assert.equal( - queryAll("#bookmark-custom-time").val(), + queryAll("#custom-time").val(), "08:00", "it should prefill the bookmark time" ); @@ -236,12 +240,12 @@ acceptance("Bookmarking", function (needs) { "it should prefill the bookmark name" ); assert.equal( - queryAll("#bookmark-custom-date > input").val(), + queryAll("#custom-date > input").val(), postDateFormatted, "it should prefill the bookmark date" ); assert.equal( - queryAll("#bookmark-custom-time").val(), + queryAll("#custom-time").val(), "10:35", "it should prefill the bookmark time" ); diff --git a/app/assets/javascripts/discourse/tests/helpers/component-test.js b/app/assets/javascripts/discourse/tests/helpers/component-test.js index 3670d265334..963e0df05e7 100644 --- a/app/assets/javascripts/discourse/tests/helpers/component-test.js +++ b/app/assets/javascripts/discourse/tests/helpers/component-test.js @@ -63,7 +63,10 @@ export default function (name, opts) { const store = createStore(); if (!opts.anonymous) { - const currentUser = User.create({ username: "eviltrout" }); + const currentUser = User.create({ + username: "eviltrout", + timezone: "Australia/Brisbane", + }); this.currentUser = currentUser; this.registry.register("current-user:main", this.currentUser, { instantiate: false, diff --git a/app/assets/javascripts/discourse/tests/integration/components/bookmark-test.js b/app/assets/javascripts/discourse/tests/integration/components/bookmark-test.js new file mode 100644 index 00000000000..1261ceeeb72 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/bookmark-test.js @@ -0,0 +1,160 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { + discourseModule, + fakeTime, + query, +} from "discourse/tests/helpers/qunit-helpers"; +import KeyboardShortcutInitializer from "discourse/initializers/keyboard-shortcuts"; +import { getApplication } from "@ember/test-helpers"; +import sinon from "sinon"; + +let clock = null; + +function mockMomentTz(dateString, timezone) { + clock = fakeTime(dateString, timezone, true); +} + +discourseModule("Integration | Component | bookmark", function (hooks) { + setupRenderingTest(hooks); + + let template = + '{{bookmark model=model afterSave=afterSave afterDelete=afterDelete onCloseWithoutSaving=onCloseWithoutSaving registerOnCloseHandler=(action "registerOnCloseHandler") closeModal=(action "closeModal")}}'; + + hooks.beforeEach(function () { + KeyboardShortcutInitializer.initialize(getApplication()); + this.actions.registerOnCloseHandler = () => {}; + this.actions.closeModal = () => {}; + this.setProperties({ + model: {}, + afterSave: () => {}, + afterDelete: () => {}, + onCloseWithoutSaving: () => {}, + }); + }); + + hooks.afterEach(function () { + if (clock) { + clock.restore(); + } + sinon.restore(); + }); + + componentTest("show later this week option if today is < Thursday", { + template, + skip: true, + + beforeEach() { + mockMomentTz("2019-12-10T08:00:00", this.currentUser._timezone); + }, + + test(assert) { + assert.ok(exists("#tap_tile_later_this_week"), "it has later this week"); + }, + }); + + componentTest( + "does not show later this week option if today is >= Thursday", + { + template, + skip: true, + + beforeEach() { + mockMomentTz("2019-12-13T08:00:00", this.currentUser._timezone); + }, + + test(assert) { + assert.notOk( + exists("#tap_tile_later_this_week"), + "it does not have later this week" + ); + }, + } + ); + + componentTest("later today does not show if later today is tomorrow", { + template, + skip: true, + + beforeEach() { + mockMomentTz("2019-12-11T22:00:00", this.currentUser._timezone); + }, + + test(assert) { + assert.notOk( + exists("#tap_tile_later_today"), + "it does not have later today" + ); + }, + }); + + componentTest("later today shows if it is after 5pm but before 6pm", { + template, + skip: true, + + beforeEach() { + mockMomentTz("2019-12-11T14:30:00", this.currentUser._timezone); + }, + + test(assert) { + assert.ok(exists("#tap_tile_later_today"), "it does have later today"); + }, + }); + + componentTest("later today does not show if it is after 5pm", { + template, + skip: true, + + beforeEach() { + mockMomentTz("2019-12-11T17:00:00", this.currentUser._timezone); + }, + + test(assert) { + assert.notOk( + exists("#tap_tile_later_today"), + "it does not have later today" + ); + }, + }); + + componentTest("later today does show if it is before the end of the day", { + template, + skip: true, + + beforeEach() { + mockMomentTz("2019-12-11T13:00:00", this.currentUser._timezone); + }, + + test(assert) { + assert.ok(exists("#tap_tile_later_today"), "it does have later today"); + }, + }); + + componentTest("prefills the custom reminder type date and time", { + template, + skip: true, + + beforeEach() { + let name = "test"; + let reminderAt = "2020-05-15T09:45:00"; + this.model = { id: 1, name, reminderAt }; + }, + + test(assert) { + assert.equal(query("#bookmark-name").value, "test"); + assert.equal(query("#custom-date > .date-picker").value, "2020-05-15"); + assert.equal(query("#custom-time").value, "09:45"); + }, + }); + + componentTest("defaults to 08:00 for custom time", { + template, + skip: true, + + async test(assert) { + await click("#tap_tile_custom"); + assert.equal(query("#custom-time").value, "08:00"); + }, + }); +}); diff --git a/app/assets/javascripts/discourse/tests/unit/controllers/bookmark-test.js b/app/assets/javascripts/discourse/tests/unit/controllers/bookmark-test.js deleted file mode 100644 index e67342fbb4c..00000000000 --- a/app/assets/javascripts/discourse/tests/unit/controllers/bookmark-test.js +++ /dev/null @@ -1,263 +0,0 @@ -import { - discourseModule, - fakeTime, - logIn, -} from "discourse/tests/helpers/qunit-helpers"; -import KeyboardShortcutInitializer from "discourse/initializers/keyboard-shortcuts"; -import { REMINDER_TYPES } from "discourse/lib/bookmark"; -import User from "discourse/models/user"; -import { getApplication } from "@ember/test-helpers"; -import sinon from "sinon"; -import { test } from "qunit"; - -let BookmarkController; - -function mockMomentTz(dateString) { - fakeTime(dateString, BookmarkController.userTimezone); -} - -discourseModule("Unit | Controller | bookmark", function (hooks) { - hooks.beforeEach(function () { - logIn(); - KeyboardShortcutInitializer.initialize(getApplication()); - - BookmarkController = this.owner.lookup("controller:bookmark"); - BookmarkController.setProperties({ - currentUser: User.current(), - site: { isMobileDevice: false }, - }); - BookmarkController.onShow(); - }); - - hooks.afterEach(function () { - sinon.restore(); - }); - - test("showLaterToday when later today is tomorrow do not show", function (assert) { - mockMomentTz("2019-12-11T22:00:00"); - - assert.equal(BookmarkController.get("showLaterToday"), false); - }); - - test("showLaterToday when later today is after 5pm but before 6pm", function (assert) { - mockMomentTz("2019-12-11T15:00:00"); - assert.equal(BookmarkController.get("showLaterToday"), true); - }); - - test("showLaterToday when now is after the cutoff time (5pm)", function (assert) { - mockMomentTz("2019-12-11T17:00:00"); - assert.equal(BookmarkController.get("showLaterToday"), false); - }); - - test("showLaterToday when later today is before the end of the day, show", function (assert) { - mockMomentTz("2019-12-11T10:00:00"); - - assert.equal(BookmarkController.get("showLaterToday"), true); - }); - - test("nextWeek gets next week correctly", function (assert) { - mockMomentTz("2019-12-11T08:00:00"); - - assert.equal( - BookmarkController.nextWeek().format("YYYY-MM-DD"), - "2019-12-18" - ); - }); - - test("nextMonth gets next month correctly", function (assert) { - mockMomentTz("2019-12-11T08:00:00"); - - assert.equal( - BookmarkController.nextMonth().format("YYYY-MM-DD"), - "2020-01-11" - ); - }); - - test("laterThisWeek gets 2 days from now", function (assert) { - mockMomentTz("2019-12-10T08:00:00"); - - assert.equal( - BookmarkController.laterThisWeek().format("YYYY-MM-DD"), - "2019-12-12" - ); - }); - - test("laterThisWeek returns null if we are at Thursday already", function (assert) { - mockMomentTz("2019-12-12T08:00:00"); - - assert.equal(BookmarkController.laterThisWeek(), null); - }); - - test("showLaterThisWeek returns true if < Thursday", function (assert) { - mockMomentTz("2019-12-10T08:00:00"); - - assert.equal(BookmarkController.showLaterThisWeek, true); - }); - - test("showLaterThisWeek returns false if > Thursday", function (assert) { - mockMomentTz("2019-12-12T08:00:00"); - - assert.equal(BookmarkController.showLaterThisWeek, false); - }); - test("tomorrow gets tomorrow correctly", function (assert) { - mockMomentTz("2019-12-11T08:00:00"); - - assert.equal( - BookmarkController.tomorrow().format("YYYY-MM-DD"), - "2019-12-12" - ); - }); - - test("startOfDay changes the time of the provided date to 8:00am correctly", function (assert) { - let dt = moment.tz( - "2019-12-11T11:37:16", - BookmarkController.currentUser.resolvedTimezone( - BookmarkController.currentUser - ) - ); - - assert.equal( - BookmarkController.startOfDay(dt).format("YYYY-MM-DD HH:mm:ss"), - "2019-12-11 08:00:00" - ); - }); - - test("laterToday gets 3 hours from now and if before half-past, it rounds down", function (assert) { - mockMomentTz("2019-12-11T08:13:00"); - - assert.equal( - BookmarkController.laterToday().format("YYYY-MM-DD HH:mm:ss"), - "2019-12-11 11:00:00" - ); - }); - - test("laterToday gets 3 hours from now and if after half-past, it rounds up to the next hour", function (assert) { - mockMomentTz("2019-12-11T08:43:00"); - - assert.equal( - BookmarkController.laterToday().format("YYYY-MM-DD HH:mm:ss"), - "2019-12-11 12:00:00" - ); - }); - - test("laterToday is capped to 6pm. later today at 3pm = 6pm, 3:30pm = 6pm, 4pm = 6pm, 4:59pm = 6pm", function (assert) { - mockMomentTz("2019-12-11T15:00:00"); - - assert.equal( - BookmarkController.laterToday().format("YYYY-MM-DD HH:mm:ss"), - "2019-12-11 18:00:00", - "3pm should max to 6pm" - ); - - mockMomentTz("2019-12-11T15:31:00"); - - assert.equal( - BookmarkController.laterToday().format("YYYY-MM-DD HH:mm:ss"), - "2019-12-11 18:00:00", - "3:30pm should max to 6pm" - ); - - mockMomentTz("2019-12-11T16:00:00"); - - assert.equal( - BookmarkController.laterToday().format("YYYY-MM-DD HH:mm:ss"), - "2019-12-11 18:00:00", - "4pm should max to 6pm" - ); - - mockMomentTz("2019-12-11T16:59:00"); - - assert.equal( - BookmarkController.laterToday().format("YYYY-MM-DD HH:mm:ss"), - "2019-12-11 18:00:00", - "4:59pm should max to 6pm" - ); - }); - - test("showLaterToday returns false if >= 5PM", function (assert) { - mockMomentTz("2019-12-11T17:00:01"); - assert.equal(BookmarkController.showLaterToday, false); - }); - - test("showLaterToday returns false if >= 5PM", function (assert) { - mockMomentTz("2019-12-11T17:00:01"); - assert.equal(BookmarkController.showLaterToday, false); - }); - - test("reminderAt - custom - defaults to 8:00am if the time is not selected", function (assert) { - BookmarkController.customReminderDate = "2028-12-12"; - BookmarkController.selectedReminderType = - BookmarkController.reminderTypes.CUSTOM; - const reminderAt = BookmarkController._reminderAt(); - assert.equal(BookmarkController.customReminderTime, "08:00"); - assert.equal( - reminderAt.toString(), - moment - .tz( - "2028-12-12 08:00", - BookmarkController.currentUser.resolvedTimezone( - BookmarkController.currentUser - ) - ) - .toString(), - "the custom date and time are parsed correctly with default time" - ); - }); - - test("loadLastUsedCustomReminderDatetime fills the custom reminder date + time if present in localStorage", function (assert) { - mockMomentTz("2019-12-11T08:00:00"); - localStorage.lastCustomBookmarkReminderDate = "2019-12-12"; - localStorage.lastCustomBookmarkReminderTime = "08:00"; - - BookmarkController._loadLastUsedCustomReminderDatetime(); - - assert.equal(BookmarkController.lastCustomReminderDate, "2019-12-12"); - assert.equal(BookmarkController.lastCustomReminderTime, "08:00"); - }); - - test("loadLastUsedCustomReminderDatetime does not fills the custom reminder date + time if the datetime in localStorage is < now", function (assert) { - mockMomentTz("2019-12-11T08:00:00"); - localStorage.lastCustomBookmarkReminderDate = "2019-12-11"; - localStorage.lastCustomBookmarkReminderTime = "07:00"; - - BookmarkController._loadLastUsedCustomReminderDatetime(); - - assert.equal(BookmarkController.lastCustomReminderDate, null); - assert.equal(BookmarkController.lastCustomReminderTime, null); - }); - - test("user timezone updates when the modal is shown", function (assert) { - User.current().changeTimezone(null); - let stub = sinon.stub(moment.tz, "guess").returns("Europe/Moscow"); - BookmarkController.onShow(); - assert.equal(BookmarkController.userHasTimezoneSet, true); - assert.equal( - BookmarkController.userTimezone, - "Europe/Moscow", - "the user does not have their timezone set and a timezone is guessed" - ); - User.current().changeTimezone("Australia/Brisbane"); - BookmarkController.onShow(); - assert.equal(BookmarkController.userHasTimezoneSet, true); - assert.equal( - BookmarkController.userTimezone, - "Australia/Brisbane", - "the user does their timezone set" - ); - stub.restore(); - }); - - test("opening the modal with an existing bookmark with reminder at prefills the custom reminder type", function (assert) { - let name = "test"; - let reminderAt = "2020-05-15T09:45:00"; - BookmarkController.model = { id: 1, name: name, reminderAt: reminderAt }; - BookmarkController.onShow(); - assert.equal( - BookmarkController.selectedReminderType, - REMINDER_TYPES.CUSTOM - ); - assert.equal(BookmarkController.customReminderDate, "2020-05-15"); - assert.equal(BookmarkController.customReminderTime, "09:45"); - assert.equal(BookmarkController.model.name, name); - }); -}); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/time-utils-test.js b/app/assets/javascripts/discourse/tests/unit/lib/time-utils-test.js new file mode 100644 index 00000000000..ce174c0f5cc --- /dev/null +++ b/app/assets/javascripts/discourse/tests/unit/lib/time-utils-test.js @@ -0,0 +1,107 @@ +import { + discourseModule, + fakeTime, +} from "discourse/tests/helpers/qunit-helpers"; + +import { + laterThisWeek, + laterToday, + nextMonth, + nextWeek, + startOfDay, + tomorrow, +} from "discourse/lib/time-utils"; +import { test } from "qunit"; + +const timezone = "Australia/Brisbane"; + +function mockMomentTz(dateString) { + fakeTime(dateString, timezone); +} + +discourseModule("Unit | lib | timeUtils", function () { + test("nextWeek gets next week correctly", function (assert) { + mockMomentTz("2019-12-11T08:00:00"); + + assert.equal(nextWeek(timezone).format("YYYY-MM-DD"), "2019-12-18"); + }); + + test("nextMonth gets next month correctly", function (assert) { + mockMomentTz("2019-12-11T08:00:00"); + + assert.equal(nextMonth(timezone).format("YYYY-MM-DD"), "2020-01-11"); + }); + + test("laterThisWeek gets 2 days from now", function (assert) { + mockMomentTz("2019-12-10T08:00:00"); + + assert.equal(laterThisWeek(timezone).format("YYYY-MM-DD"), "2019-12-12"); + }); + + test("tomorrow gets tomorrow correctly", function (assert) { + mockMomentTz("2019-12-11T08:00:00"); + + assert.equal(tomorrow(timezone).format("YYYY-MM-DD"), "2019-12-12"); + }); + + test("startOfDay changes the time of the provided date to 8:00am correctly", function (assert) { + let dt = moment.tz("2019-12-11T11:37:16", timezone); + + assert.equal( + startOfDay(dt).format("YYYY-MM-DD HH:mm:ss"), + "2019-12-11 08:00:00" + ); + }); + + test("laterToday gets 3 hours from now and if before half-past, it rounds down", function (assert) { + mockMomentTz("2019-12-11T08:13:00"); + + assert.equal( + laterToday(timezone).format("YYYY-MM-DD HH:mm:ss"), + "2019-12-11 11:00:00" + ); + }); + + test("laterToday gets 3 hours from now and if after half-past, it rounds up to the next hour", function (assert) { + mockMomentTz("2019-12-11T08:43:00"); + + assert.equal( + laterToday(timezone).format("YYYY-MM-DD HH:mm:ss"), + "2019-12-11 12:00:00" + ); + }); + + test("laterToday is capped to 6pm. later today at 3pm = 6pm, 3:30pm = 6pm, 4pm = 6pm, 4:59pm = 6pm", function (assert) { + mockMomentTz("2019-12-11T15:00:00"); + + assert.equal( + laterToday(timezone).format("YYYY-MM-DD HH:mm:ss"), + "2019-12-11 18:00:00", + "3pm should max to 6pm" + ); + + mockMomentTz("2019-12-11T15:31:00"); + + assert.equal( + laterToday(timezone).format("YYYY-MM-DD HH:mm:ss"), + "2019-12-11 18:00:00", + "3:30pm should max to 6pm" + ); + + mockMomentTz("2019-12-11T16:00:00"); + + assert.equal( + laterToday(timezone).format("YYYY-MM-DD HH:mm:ss"), + "2019-12-11 18:00:00", + "4pm should max to 6pm" + ); + + mockMomentTz("2019-12-11T16:59:00"); + + assert.equal( + laterToday(timezone).format("YYYY-MM-DD HH:mm:ss"), + "2019-12-11 18:00:00", + "4:59pm should max to 6pm" + ); + }); +}); diff --git a/app/assets/stylesheets/common/components/bookmark-modal.scss b/app/assets/stylesheets/common/components/bookmark-modal.scss index a2ad95ad0e1..a22f6c485af 100644 --- a/app/assets/stylesheets/common/components/bookmark-modal.scss +++ b/app/assets/stylesheets/common/components/bookmark-modal.scss @@ -35,7 +35,6 @@ padding: 1em 1em 0.5em; border: 1px solid var(--primary-low); border-top: none; - margin-top: -0.667em; background: var(--primary-very-low); .d-icon { padding: 0 0.75em 0 0; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 970f32facc5..15057877a7a 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -572,6 +572,20 @@ en: title: "Why are you rejecting this user?" send_email: "Send rejection email" + time_shortcut: + later_today: "Later today" + next_business_day: "Next business day" + tomorrow: "Tomorrow" + next_week: "Next week" + post_local_date: "Date in post" + later_this_week: "Later this week" + start_of_next_business_week: "Monday" + start_of_next_business_week_alt: "Next Monday" + next_month: "Next month" + custom: "Custom date and time" + none: "None needed" + last_custom: "Last" + user_action: user_posted_topic: "%{user} posted the topic" you_posted_topic: "You posted the topic"