mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08: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:
parent
ad3ec5809f
commit
84c7b2c404
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -150,3 +150,4 @@ dist
|
|||
copyright
|
||||
|
||||
yarn-error.log
|
||||
tags
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 <a href=%{categoryUrl}>#%{categoryName}</a> %{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}."
|
||||
|
|
Loading…
Reference in New Issue
Block a user