mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: Relative time input for timers and bookmarks and promote auto-close after last post timer (#12063)
This PR adds a new relative-time component, that is an input box with a SK dropdown of minutes, hours, days, and months which outputs the duration selected in minutes. This new component is used in the time shortcuts list (used by bookmarks and topic timers) as a new Relative Time shortcut. Also in this PR, I have made the "Auto-Close After Last Post" timer into a top level timer type in the UI, and removed the "based on last post" custom time shortcut.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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}`;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
{{/if}}
|
||||
{{#if useDuration}}
|
||||
<div class="controls">
|
||||
<label class="control-label">{{durationLabel}}</label>
|
||||
{{text-field id="topic_timer_duration" class="topic-timer-duration" type="number" value=duration min="0.1" step="0.1" onChange=durationChanged}}
|
||||
<label class="control-label">Duration</label>
|
||||
{{relative-time onChange=(action "durationChanged") durationMinutes=(readonly topicTimer.duration_minutes)}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if willCloseImmediately}}
|
||||
@@ -34,7 +34,7 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if showTopicStatusInfo}}
|
||||
<div class="alert alert-info">
|
||||
<div class="alert alert-info modal-topic-timer-info">
|
||||
{{topic-timer-info
|
||||
statusType=statusType
|
||||
executeAt=executeAt
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="relative-time">
|
||||
{{input class="relative-time-duration" min="0.1" step="0.01" type="number" value=duration onChange=(action "onChangeDuration")}}
|
||||
{{combo-box
|
||||
content=intervals
|
||||
value=selectedInterval
|
||||
class="relative-time-intervals"
|
||||
onChange=(action "onChangeInterval")
|
||||
}}
|
||||
</div>
|
||||
@@ -25,5 +25,13 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if option.isRelativeTimeShortcut}}
|
||||
{{#if relativeTimeSelected}}
|
||||
<div class="control-group custom-date-time-wrap">
|
||||
{{relative-time onChange=(action "relativeTimeChanged")}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/tap-tile-grid}}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -58,6 +58,12 @@
|
||||
.pika-single {
|
||||
position: absolute !important; /* inline JS styles */
|
||||
}
|
||||
|
||||
.modal-topic-timer-info {
|
||||
.topic-status-info {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mobile styles
|
||||
|
||||
@@ -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";
|
||||
|
||||
18
app/assets/stylesheets/common/components/relative-time.scss
Normal file
18
app/assets/stylesheets/common/components/relative-time.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user