diff --git a/.gitignore b/.gitignore index d0359a12ee6..0afee536eea 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,4 @@ dist copyright yarn-error.log +tags diff --git a/app/assets/javascripts/discourse/app/components/bookmark.js b/app/assets/javascripts/discourse/app/components/bookmark.js index 0340401b490..2d794dea073 100644 --- a/app/assets/javascripts/discourse/app/components/bookmark.js +++ b/app/assets/javascripts/discourse/app/components/bookmark.js @@ -404,7 +404,9 @@ export default Component.extend({ // 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) { + if ( + ![TIME_SHORTCUT_TYPES.CUSTOM, TIME_SHORTCUT_TYPES.RELATIVE].includes(type) + ) { return this.saveAndClose(); } }, diff --git a/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js b/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js index cbc8f744a40..4c35310ab86 100644 --- a/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js +++ b/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js @@ -1,5 +1,6 @@ import { BUMP_TYPE, + CLOSE_AFTER_LAST_POST_STATUS_TYPE, CLOSE_STATUS_TYPE, DELETE_REPLIES_TYPE, DELETE_STATUS_TYPE, @@ -19,13 +20,21 @@ export default Component.extend({ statusType: readOnly("topicTimer.status_type"), autoOpen: equal("statusType", OPEN_STATUS_TYPE), autoClose: equal("statusType", CLOSE_STATUS_TYPE), + autoCloseAfterLastPost: equal( + "statusType", + CLOSE_AFTER_LAST_POST_STATUS_TYPE + ), autoDelete: equal("statusType", DELETE_STATUS_TYPE), autoBump: equal("statusType", BUMP_TYPE), publishToCategory: equal("statusType", PUBLISH_TO_CATEGORY_STATUS_TYPE), autoDeleteReplies: equal("statusType", DELETE_REPLIES_TYPE), showTimeOnly: or("autoOpen", "autoDelete", "autoBump"), showFutureDateInput: or("showTimeOnly", "publishToCategory", "autoClose"), - useDuration: or("isBasedOnLastPost", "autoDeleteReplies"), + useDuration: or( + "isBasedOnLastPost", + "autoDeleteReplies", + "autoCloseAfterLastPost" + ), duration: null, @on("init") @@ -52,8 +61,8 @@ export default Component.extend({ } }, - @discourseComputed("includeBasedOnLastPost") - customTimeShortcutOptions(includeBasedOnLastPost) { + @discourseComputed() + customTimeShortcutOptions() { return [ { icon: "bed", @@ -83,14 +92,6 @@ export default Component.extend({ time: startOfDay(now().add(6, "months")), timeFormatKey: "dates.long_no_year", }, - { - icon: "far-clock", - id: "set_based_on_last_post", - label: "topic.auto_update_input.set_based_on_last_post", - time: null, - timeFormatted: "", - hidden: !includeBasedOnLastPost, - }, ]; }, @@ -100,8 +101,7 @@ export default Component.extend({ }, isCustom: equal("timerType", "custom"), - isBasedOnLastPost: equal("timerType", "set_based_on_last_post"), - includeBasedOnLastPost: equal("statusType", CLOSE_STATUS_TYPE), + isBasedOnLastPost: equal("statusType", "close_after_last_post"), @discourseComputed( "topicTimer.updateTime", @@ -183,19 +183,12 @@ export default Component.extend({ @action onTimeSelected(type, time) { - this.setProperties({ - "topicTimer.based_on_last_post": type === "set_based_on_last_post", - timerType: type, - }); + this.set("timerType", type); this.onChangeInput(type, time); }, @action - durationChanged(newDuration) { - if (this.durationType === "days") { - this.set("topicTimer.duration_minutes", newDuration * 60 * 24); - } else { - this.set("topicTimer.duration_minutes", newDuration * 60); - } + durationChanged(newDurationMins) { + this.set("topicTimer.duration_minutes", newDurationMins); }, }); diff --git a/app/assets/javascripts/discourse/app/components/relative-time.js b/app/assets/javascripts/discourse/app/components/relative-time.js new file mode 100644 index 00000000000..2527b09cf40 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/relative-time.js @@ -0,0 +1,87 @@ +import discourseComputed, { on } from "discourse-common/utils/decorators"; + +import Component from "@ember/component"; +import I18n from "I18n"; +import { action } from "@ember/object"; + +export default Component.extend({ + tagName: "", + selectedInterval: "mins", + durationMinutes: null, + duration: null, + + @on("init") + cloneDuration() { + let mins = this.durationMinutes; + if (mins >= 43800) { + this.setProperties({ + duration: Math.floor(mins / 30 / 60 / 24), + selectedInterval: "months", + }); + } else if (mins >= 1440) { + this.setProperties({ + duration: Math.floor(mins / 60 / 24), + selectedInterval: "days", + }); + } else if (mins >= 60) { + this.setProperties({ + duration: Math.floor(mins / 60), + selectedInterval: "hours", + }); + } else { + this.setProperties({ + duration: mins, + selectedInterval: "mins", + }); + } + }, + + @discourseComputed + intervals() { + return [ + { id: "mins", name: I18n.t("relative_time.minutes") }, + { id: "hours", name: I18n.t("relative_time.hours") }, + { id: "days", name: I18n.t("relative_time.days") }, + { id: "months", name: I18n.t("relative_time.months") }, + ]; + }, + + @discourseComputed("selectedInterval", "duration") + calculatedMinutes(interval, duration) { + duration = parseFloat(duration); + + let mins = 0; + + switch (interval) { + case "mins": + mins = duration; + break; + case "hours": + mins = duration * 60; + break; + case "days": + mins = duration * 60 * 24; + break; + case "months": + mins = duration * 60 * 24 * 30; // least accurate because of varying days in months + break; + } + + return mins; + }, + + @action + onChangeInterval(newInterval) { + this.set("selectedInterval", newInterval); + if (this.onChange) { + this.onChange(this.calculatedMinutes); + } + }, + + @action + onChangeDuration() { + if (this.onChange) { + this.onChange(this.calculatedMinutes); + } + }, +}); diff --git a/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js index ae3cb9b78c5..9df40934a21 100644 --- a/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js +++ b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js @@ -169,6 +169,7 @@ export default Component.extend({ }, customDatetimeSelected: equal("selectedShortcut", TIME_SHORTCUT_TYPES.CUSTOM), + relativeTimeSelected: equal("selectedShortcut", TIME_SHORTCUT_TYPES.RELATIVE), customDatetimeFilled: and("customDate", "customTime"), @observes("customDate", "customTime") @@ -219,15 +220,26 @@ export default Component.extend({ } }); - let customOptionIndex = options.findIndex( - (opt) => opt.id === TIME_SHORTCUT_TYPES.CUSTOM + let relativeOptionIndex = options.findIndex( + (opt) => opt.id === TIME_SHORTCUT_TYPES.RELATIVE ); - options.splice(customOptionIndex, 0, ...customOptions); + options.splice(relativeOptionIndex, 0, ...customOptions); return options; }, + @action + relativeTimeChanged(relativeTimeMins) { + let dateTime = now(this.userTimezone).add(relativeTimeMins, "minutes"); + + this.set("selectedDatetime", dateTime); + + if (this.onTimeSelected) { + this.onTimeSelected(TIME_SHORTCUT_TYPES.RELATIVE, dateTime); + } + }, + @action selectShortcut(type) { if (this.options.filterBy("hidden").mapBy("id").includes(type)) { diff --git a/app/assets/javascripts/discourse/app/components/topic-timer-info.js b/app/assets/javascripts/discourse/app/components/topic-timer-info.js index a0c5809a993..ea3e1dc1a0b 100644 --- a/app/assets/javascripts/discourse/app/components/topic-timer-info.js +++ b/app/assets/javascripts/discourse/app/components/topic-timer-info.js @@ -67,7 +67,9 @@ export default Component.extend({ let options = { timeLeft: duration.humanize(true), - duration: moment.duration(durationMinutes, "minutes").humanize(), + duration: moment + .duration(durationMinutes, "minutes") + .humanize({ s: 60, m: 60, h: 24 }), }; const categoryId = this.categoryId; @@ -130,11 +132,10 @@ export default Component.extend({ if (statusType === "silent_close") { statusType = "close"; } - - if (this.basedOnLastPost) { - return `topic.status_update_notice.auto_${statusType}_based_on_last_post`; - } else { - return `topic.status_update_notice.auto_${statusType}`; + if (this.basedOnLastPost && statusType === "close") { + statusType = "close_after_last_post"; } + + return `topic.status_update_notice.auto_${statusType}`; }, }); diff --git a/app/assets/javascripts/discourse/app/controllers/edit-topic-timer.js b/app/assets/javascripts/discourse/app/controllers/edit-topic-timer.js index 1bff8157a5c..a60f541d4fd 100644 --- a/app/assets/javascripts/discourse/app/controllers/edit-topic-timer.js +++ b/app/assets/javascripts/discourse/app/controllers/edit-topic-timer.js @@ -9,6 +9,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; export const CLOSE_STATUS_TYPE = "close"; +export const CLOSE_AFTER_LAST_POST_STATUS_TYPE = "close_after_last_post"; export const OPEN_STATUS_TYPE = "open"; export const PUBLISH_TO_CATEGORY_STATUS_TYPE = "publish_to_category"; export const DELETE_STATUS_TYPE = "delete"; @@ -28,6 +29,14 @@ export default Controller.extend(ModalFunctionality, { closed ? "topic.temp_open.title" : "topic.auto_close.title" ), }, + { + id: CLOSE_AFTER_LAST_POST_STATUS_TYPE, + name: I18n.t( + closed + ? "topic.temp_open.title" + : "topic.auto_close_after_last_post.title" + ), + }, { id: OPEN_STATUS_TYPE, name: I18n.t( @@ -112,6 +121,13 @@ export default Controller.extend(ModalFunctionality, { if (!this.get("topicTimer.status_type")) { this.send("onChangeStatusType", this.defaultStatusType); } + + if ( + this.get("topicTimer.status_type") === CLOSE_STATUS_TYPE && + this.get("topicTimer.based_on_last_post") + ) { + this.send("onChangeStatusType", CLOSE_AFTER_LAST_POST_STATUS_TYPE); + } }, @discourseComputed("publicTimerTypes") @@ -121,10 +137,11 @@ export default Controller.extend(ModalFunctionality, { actions: { onChangeStatusType(value) { - if (value !== CLOSE_STATUS_TYPE) { - this.set("topicTimer.based_on_last_post", false); - } - this.set("topicTimer.status_type", value); + this.setProperties({ + "topicTimer.based_on_last_post": + CLOSE_AFTER_LAST_POST_STATUS_TYPE === value, + "topicTimer.status_type": value, + }); }, onChangeInput(_type, time) { @@ -168,10 +185,15 @@ export default Controller.extend(ModalFunctionality, { } } + let statusType = this.get("topicTimer.status_type"); + if (statusType === CLOSE_AFTER_LAST_POST_STATUS_TYPE) { + statusType = CLOSE_STATUS_TYPE; + } + this._setTimer( this.get("topicTimer.updateTime"), this.get("topicTimer.duration_minutes"), - this.get("topicTimer.status_type"), + statusType, this.get("topicTimer.based_on_last_post"), this.get("topicTimer.category_id") ); diff --git a/app/assets/javascripts/discourse/app/lib/time-shortcut.js b/app/assets/javascripts/discourse/app/lib/time-shortcut.js index 6c85d19fcc4..4691cea5bde 100644 --- a/app/assets/javascripts/discourse/app/lib/time-shortcut.js +++ b/app/assets/javascripts/discourse/app/lib/time-shortcut.js @@ -16,6 +16,7 @@ export const TIME_SHORTCUT_TYPES = { NEXT_WEEK: "next_week", NEXT_MONTH: "next_month", CUSTOM: "custom", + RELATIVE: "relative", LAST_CUSTOM: "last_custom", NONE: "none", START_OF_NEXT_BUSINESS_WEEK: "start_of_next_business_week", @@ -76,6 +77,14 @@ export function defaultShortcutOptions(timezone) { time: nextMonth(timezone), timeFormatted: nextMonth(timezone).format(I18n.t("dates.long_no_year")), }, + { + icon: "far-clock", + id: TIME_SHORTCUT_TYPES.RELATIVE, + label: "time_shortcut.relative", + time: null, + timeFormatted: null, + isRelativeTimeShortcut: true, + }, { icon: "calendar-alt", id: TIME_SHORTCUT_TYPES.CUSTOM, diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs index e3f678f9688..17dafbfb846 100644 --- a/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs @@ -23,8 +23,8 @@ {{/if}} {{#if useDuration}}
- - {{text-field id="topic_timer_duration" class="topic-timer-duration" type="number" value=duration min="0.1" step="0.1" onChange=durationChanged}} + + {{relative-time onChange=(action "durationChanged") durationMinutes=(readonly topicTimer.duration_minutes)}}
{{/if}} {{#if willCloseImmediately}} @@ -34,7 +34,7 @@ {{/if}} {{#if showTopicStatusInfo}} -
+ {{/if}} {{/if}} + + {{#if option.isRelativeTimeShortcut}} + {{#if relativeTimeSelected}} +
+ {{relative-time onChange=(action "relativeTimeChanged")}} +
+ {{/if}} + {{/if}} {{/each}} {{/tap-tile-grid}} diff --git a/app/assets/javascripts/discourse/tests/acceptance/topic-edit-timer-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-edit-timer-test.js index a8fd8944ced..50f44af9a64 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-edit-timer-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-edit-timer-test.js @@ -63,8 +63,14 @@ acceptance("Topic - Edit timer", function (needs) { .trim(); assert.ok(regex2.test(html2)); - await click("#tap_tile_set_based_on_last_post"); - await fillIn("#topic_timer_duration", "2"); + const timerType = selectKit(".select-kit.timer-type"); + await timerType.expand(); + await timerType.selectRowByValue("close_after_last_post"); + + const interval = selectKit(".select-kit.relative-time-intervals"); + await interval.expand(); + await interval.selectRowByValue("hours"); + await fillIn(".relative-time-duration", "2"); const regex3 = /last post in the topic is already/g; const html3 = queryAll(".edit-topic-timer-modal .warning").html().trim(); diff --git a/app/assets/stylesheets/common/base/edit-topic-timer-modal.scss b/app/assets/stylesheets/common/base/edit-topic-timer-modal.scss index 11315c44982..f43d632b9c2 100644 --- a/app/assets/stylesheets/common/base/edit-topic-timer-modal.scss +++ b/app/assets/stylesheets/common/base/edit-topic-timer-modal.scss @@ -58,6 +58,12 @@ .pika-single { position: absolute !important; /* inline JS styles */ } + + .modal-topic-timer-info { + .topic-status-info { + border-top: 0; + } + } } // mobile styles diff --git a/app/assets/stylesheets/common/components/_index.scss b/app/assets/stylesheets/common/components/_index.scss index 79fee8ec7ab..dd37f9edbef 100644 --- a/app/assets/stylesheets/common/components/_index.scss +++ b/app/assets/stylesheets/common/components/_index.scss @@ -17,6 +17,7 @@ @import "ignored-user-list"; @import "keyboard_shortcuts"; @import "navs"; +@import "relative-time"; @import "share-and-invite-modal"; @import "svg"; @import "tap-tile"; diff --git a/app/assets/stylesheets/common/components/relative-time.scss b/app/assets/stylesheets/common/components/relative-time.scss new file mode 100644 index 00000000000..579a477f371 --- /dev/null +++ b/app/assets/stylesheets/common/components/relative-time.scss @@ -0,0 +1,18 @@ +.relative-time { + display: flex; + flex-wrap: wrap; + + input[type="text"] { + flex: 1; + } + + .select-kit { + flex: 1; + width: auto; + margin-left: 5px; + } + + &:last-child { + margin-right: auto; + } +} diff --git a/app/assets/stylesheets/common/components/time-shortcut-picker.scss b/app/assets/stylesheets/common/components/time-shortcut-picker.scss index 86713690554..eabf18307d0 100644 --- a/app/assets/stylesheets/common/components/time-shortcut-picker.scss +++ b/app/assets/stylesheets/common/components/time-shortcut-picker.scss @@ -9,6 +9,11 @@ margin-top: -0.5em; } + .caret-icon { + margin-top: 0; + padding: 0 0 0 5px; + } + .tap-tile-date-input, .tap-tile-time-input { display: flex; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 27ba2562c83..81a96d6071b 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -575,6 +575,12 @@ en: title: "Why are you rejecting this user?" send_email: "Send rejection email" + relative_time: + minutes: "minute(s)" + hours: "hour(s)" + days: "day(s)" + months: "month(s)" + time_shortcut: later_today: "Later today" next_business_day: "Next business day" @@ -586,6 +592,7 @@ en: start_of_next_business_week_alt: "Next Monday" next_month: "Next month" custom: "Custom date and time" + relative: "Relative time" none: "None needed" last_custom: "Last" @@ -2463,6 +2470,8 @@ en: label: "Auto-close topic hours:" error: "Please enter a valid value." based_on_last_post: "Don't close until the last post in the topic is at least this old." + auto_close_after_last_post: + title: "Auto-Close Topic After Last Post" auto_delete: title: "Auto-Delete Topic" auto_bump: @@ -2476,7 +2485,7 @@ en: auto_open: "This topic will automatically open %{timeLeft}." auto_close: "This topic will automatically close %{timeLeft}." auto_publish_to_category: "This topic will be published to #%{categoryName} %{timeLeft}." - auto_close_based_on_last_post: "This topic will close %{duration} after the last reply." + auto_close_after_last_post: "This topic will close %{duration} after the last reply." auto_delete: "This topic will be automatically deleted %{timeLeft}." auto_bump: "This topic will be automatically bumped %{timeLeft}." auto_reminder: "You will be reminded about this topic %{timeLeft}."