FEATURE: Topic timer UI revamp (#11912)

This PR revamps the topic timer UI, using the time shortcut selector from the bookmark modal.

* Fixes an issue where the duration of hours/days after last reply or auto delete replies was not enforced to be > 0
* Fixed an issue where the timer dropdown options were not reloaded correctly if the topic status changes in the background (use `MessageBus` to publish topic state in the open/close timer jobs)
* Moved the duration input and the "based on last post" option from the `future-date-input` component, as it was only used for topic timers. Also moved out the notice that is displayed which was also only relevant for topic timers.
This commit is contained in:
Martin Brennan 2021-02-03 10:13:32 +10:00 committed by GitHub
parent f39ae8a903
commit 6d72c8ab19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 396 additions and 435 deletions

View File

@ -6,26 +6,27 @@ import {
OPEN_STATUS_TYPE,
PUBLISH_TO_CATEGORY_STATUS_TYPE,
} from "discourse/controllers/edit-topic-timer";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { FORMAT } from "select-kit/components/future-date-input-selector";
import discourseComputed from "discourse-common/utils/decorators";
import { equal, or, readOnly } from "@ember/object/computed";
import I18n from "I18n";
import { action } from "@ember/object";
import Component from "@ember/component";
import { schedule } from "@ember/runloop";
import { isEmpty } from "@ember/utils";
import { now, startOfDay, thisWeekend } from "discourse/lib/time-utils";
export default Component.extend({
selection: readOnly("topicTimer.status_type"),
autoOpen: equal("selection", OPEN_STATUS_TYPE),
autoClose: equal("selection", CLOSE_STATUS_TYPE),
autoDelete: equal("selection", DELETE_STATUS_TYPE),
autoBump: equal("selection", BUMP_TYPE),
publishToCategory: equal("selection", PUBLISH_TO_CATEGORY_STATUS_TYPE),
autoDeleteReplies: equal("selection", DELETE_REPLIES_TYPE),
statusType: readOnly("topicTimer.status_type"),
autoOpen: equal("statusType", OPEN_STATUS_TYPE),
autoClose: equal("statusType", CLOSE_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",
"autoDeleteReplies"
),
showFutureDateInput: or("showTimeOnly", "publishToCategory", "autoClose"),
useDuration: or("isBasedOnLastPost", "autoDeleteReplies"),
originalTopicTimerTime: null,
@discourseComputed("autoDeleteReplies")
durationType(autoDeleteReplies) {
@ -39,26 +40,142 @@ export default Component.extend({
}
},
@observes("selection")
_updateBasedOnLastPost() {
if (!this.autoClose) {
schedule("afterRender", () => {
this.set("topicTimer.based_on_last_post", false);
});
@discourseComputed("includeBasedOnLastPost")
customTimeShortcutOptions(includeBasedOnLastPost) {
return [
{
icon: "bed",
id: "this_weekend",
label: "topic.auto_update_input.this_weekend",
time: thisWeekend(),
timeFormatKey: "dates.time_short_day",
},
{
icon: "far-clock",
id: "two_weeks",
label: "topic.auto_update_input.two_weeks",
time: startOfDay(now().add(2, "weeks")),
timeFormatKey: "dates.long_no_year",
},
{
icon: "far-calendar-plus",
id: "three_months",
label: "topic.auto_update_input.three_months",
time: startOfDay(now().add(3, "months")),
timeFormatKey: "dates.long_no_year",
},
{
icon: "far-calendar-plus",
id: "six_months",
label: "topic.auto_update_input.six_months",
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,
},
];
},
@discourseComputed
hiddenTimeShortcutOptions() {
return ["none", "start_of_next_business_week"];
},
isCustom: equal("timerType", "custom"),
isBasedOnLastPost: equal("timerType", "set_based_on_last_post"),
includeBasedOnLastPost: equal("statusType", CLOSE_STATUS_TYPE),
@discourseComputed(
"topicTimer.updateTime",
"topicTimer.duration",
"useDuration",
"durationType"
)
executeAt(updateTime, duration, useDuration, durationType) {
if (useDuration) {
return moment().add(parseFloat(duration), durationType).format(FORMAT);
} else {
return updateTime;
}
},
didReceiveAttrs() {
this._super(...arguments);
@discourseComputed(
"isBasedOnLastPost",
"topicTimer.duration",
"topic.last_posted_at"
)
willCloseImmediately(isBasedOnLastPost, duration, lastPostedAt) {
if (isBasedOnLastPost && duration) {
let closeDate = moment(lastPostedAt);
closeDate = closeDate.add(duration, "hours");
return closeDate < moment();
}
},
// TODO: get rid of this hack
schedule("afterRender", () => {
if (!this.get("topicTimer.status_type")) {
this.set(
"topicTimer.status_type",
this.get("timerTypes.firstObject.id")
);
@discourseComputed("isBasedOnLastPost", "topic.last_posted_at")
willCloseI18n(isBasedOnLastPost, lastPostedAt) {
if (isBasedOnLastPost) {
const diff = Math.round(
(new Date() - new Date(lastPostedAt)) / (1000 * 60 * 60)
);
return I18n.t("topic.auto_close_momentarily", { count: diff });
}
},
@discourseComputed("durationType")
durationLabel(durationType) {
return I18n.t(`topic.topic_status_update.num_of_${durationType}`);
},
@discourseComputed(
"statusType",
"isCustom",
"topicTimer.updateTime",
"willCloseImmediately",
"topicTimer.category_id",
"useDuration",
"topicTimer.duration"
)
showTopicStatusInfo(
statusType,
isCustom,
updateTime,
willCloseImmediately,
categoryId,
useDuration,
duration
) {
if (!statusType || willCloseImmediately) {
return false;
}
if (statusType === PUBLISH_TO_CATEGORY_STATUS_TYPE && isEmpty(categoryId)) {
return false;
}
if (isCustom && updateTime) {
if (moment(updateTime) < moment()) {
return false;
}
} else if (useDuration) {
return duration;
}
return updateTime;
},
@action
onTimeSelected(type, time) {
this.setProperties({
"topicTimer.based_on_last_post": type === "set_based_on_last_post",
timerType: type,
});
this.onChangeInput(type, time);
},
});

View File

@ -1,42 +1,30 @@
import { and, empty, equal, or } from "@ember/object/computed";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import { and, empty, equal } from "@ember/object/computed";
import { observes } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { FORMAT } from "select-kit/components/future-date-input-selector";
import I18n from "I18n";
import { PUBLISH_TO_CATEGORY_STATUS_TYPE } from "discourse/controllers/edit-topic-timer";
import { isEmpty } from "@ember/utils";
export default Component.extend({
selection: null,
date: null,
time: null,
includeDateTime: true,
duration: null,
durationType: "hours",
isCustom: equal("selection", "pick_date_and_time"),
isBasedOnLastPost: equal("selection", "set_based_on_last_post"),
displayDateAndTimePicker: and("includeDateTime", "isCustom"),
displayLabel: null,
labelClasses: null,
displayNumberInput: or("isBasedOnLastPost", "isBasedOnDuration"),
init() {
this._super(...arguments);
if (this.input) {
if (this.basedOnLastPost) {
this.set("selection", "set_based_on_last_post");
} else if (this.isBasedOnDuration) {
this.set("selection", null);
} else {
const datetime = moment(this.input);
this.setProperties({
selection: "pick_date_and_time",
date: datetime.format("YYYY-MM-DD"),
time: datetime.format("HH:mm"),
});
this._updateInput();
}
const datetime = moment(this.input);
this.setProperties({
selection: "pick_date_and_time",
date: datetime.format("YYYY-MM-DD"),
time: datetime.format("HH:mm"),
});
this._updateInput();
}
},
@ -59,49 +47,6 @@ export default Component.extend({
}
},
@observes("isBasedOnLastPost")
_updateBasedOnLastPost() {
this.set("basedOnLastPost", this.isBasedOnLastPost);
},
@observes("duration")
_updateDuration() {
this.attrs.onChangeDuration &&
this.attrs.onChangeDuration(parseInt(this.duration, 0));
},
@discourseComputed(
"input",
"duration",
"isBasedOnLastPost",
"isBasedOnDuration",
"durationType"
)
executeAt(
input,
duration,
isBasedOnLastPost,
isBasedOnDuration,
durationType
) {
if (isBasedOnLastPost || isBasedOnDuration) {
return moment(input)
.add(parseInt(duration, 0), durationType)
.format(FORMAT);
} else {
return input;
}
},
@discourseComputed("durationType")
durationLabel(durationType) {
return I18n.t(
`topic.topic_status_update.num_of_${
durationType === "hours" ? "hours" : "days"
}`
);
},
didReceiveAttrs() {
this._super(...arguments);
@ -109,65 +54,4 @@ export default Component.extend({
this.set("displayLabel", I18n.t(this.label));
}
},
@discourseComputed(
"statusType",
"input",
"isCustom",
"date",
"time",
"willCloseImmediately",
"categoryId",
"displayNumberInput",
"duration"
)
showTopicStatusInfo(
statusType,
input,
isCustom,
date,
time,
willCloseImmediately,
categoryId,
displayNumberInput,
duration
) {
if (!statusType || willCloseImmediately) {
return false;
}
if (statusType === PUBLISH_TO_CATEGORY_STATUS_TYPE && isEmpty(categoryId)) {
return false;
}
if (isCustom) {
if (date) {
return moment(`${date}${time ? " " + time : ""}`).isAfter(moment());
}
return time;
} else if (displayNumberInput) {
return duration;
} else {
return input;
}
},
@discourseComputed("isBasedOnLastPost", "input", "lastPostedAt")
willCloseImmediately(isBasedOnLastPost, input, lastPostedAt) {
if (isBasedOnLastPost && input) {
let closeDate = moment(lastPostedAt);
closeDate = closeDate.add(input, "hours");
return closeDate < moment();
}
},
@discourseComputed("isBasedOnLastPost", "lastPostedAt")
willCloseI18n(isBasedOnLastPost, lastPostedAt) {
if (isBasedOnLastPost) {
const diff = Math.round(
(new Date() - new Date(lastPostedAt)) / (1000 * 60 * 60)
);
return I18n.t("topic.auto_close_immediate", { count: diff });
}
},
});

View File

@ -92,33 +92,50 @@ export default Component.extend({
});
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.parsePrefilledDatetime();
}
this._bindKeyboardShortcuts();
this._loadLastUsedCustomDatetime();
},
@observes("prefilledDatetime")
prefilledDatetimeChanged() {
if (this.prefilledDatetime) {
this.parsePrefilledDatetime();
} else {
this.setProperties({
customDate: null,
customTime: null,
selectedShortcut: null,
});
}
},
@on("willDestroyElement")
_resetKeyboardShortcuts() {
KeyboardShortcuts.unbind(BINDINGS);
KeyboardShortcuts.unpause(GLOBAL_SHORTCUTS_TO_PAUSE);
},
parsePrefilledDatetime() {
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,
});
},
_loadLastUsedCustomDatetime() {
let lastTime = localStorage.lastCustomTime;
let lastDate = localStorage.lastCustomDate;
@ -196,6 +213,12 @@ export default Component.extend({
lastCustom.hidden = false;
}
customOptions.forEach((opt) => {
if (!opt.timeFormatted && opt.time) {
opt.timeFormatted = opt.time.format(I18n.t(opt.timeFormatKey));
}
});
let customOptionIndex = options.findIndex(
(opt) => opt.id === TIME_SHORTCUT_TYPES.CUSTOM
);

View File

@ -39,6 +39,10 @@ export default Component.extend({
return;
}
if (this.isDestroyed) {
return;
}
const topicStatus = this.topicClosed ? "close" : "open";
const topicStatusKnown = this.topicClosed !== undefined;
if (topicStatusKnown && topicStatus === this.statusType) {

View File

@ -83,11 +83,12 @@ export default Controller.extend(ModalFunctionality, {
this.set("model.closed", result.closed);
} else {
this.set("model.topic_timer", EmberObject.create({}));
this.set(
"model.topic_timer",
EmberObject.create({ status_type: this.defaultStatusType })
);
this.setProperties({
selection: null,
});
this.send("onChangeInput", null, null);
}
})
.catch(popupAjaxError)
@ -106,7 +107,16 @@ export default Controller.extend(ModalFunctionality, {
}
}
this.send("onChangeInput", time);
this.send("onChangeInput", null, time);
if (!this.get("topicTimer.status_type")) {
this.send("onChangeStatusType", this.defaultStatusType);
}
},
@discourseComputed("publicTimerTypes")
defaultStatusType(publicTimerTypes) {
return publicTimerTypes[0].id;
},
actions: {
@ -114,8 +124,11 @@ export default Controller.extend(ModalFunctionality, {
this.set("topicTimer.status_type", value);
},
onChangeInput(value) {
this.set("topicTimer.updateTime", value);
onChangeInput(_type, time) {
if (moment.isMoment(time)) {
time = time.format(FORMAT);
}
this.set("topicTimer.updateTime", time);
},
onChangeDuration(value) {
@ -134,6 +147,18 @@ export default Controller.extend(ModalFunctionality, {
return;
}
if (
this.get("topicTimer.duration") &&
!this.get("topicTimer.updateTime") &&
this.get("topicTimer.duration") < 1
) {
this.flash(
I18n.t("topic.topic_status_update.min_duration"),
"alert-error"
);
return;
}
this._setTimer(
this.get("topicTimer.updateTime"),
this.get("topicTimer.duration"),

View File

@ -5,6 +5,7 @@ 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 const MOMENT_SATURDAY = 6;
export function now(timezone) {
return moment.tz(timezone);
@ -18,6 +19,10 @@ export function tomorrow(timezone) {
return startOfDay(now(timezone).add(1, "day"));
}
export function thisWeekend(timezone) {
return startOfDay(now(timezone).day(MOMENT_SATURDAY));
}
export function laterToday(timezone) {
let later = now(timezone).add(3, "hours");
if (later.hour() >= LATER_TODAY_MAX_HOUR) {

View File

@ -4,12 +4,12 @@
class="timer-type"
onChange=onChangeStatusType
content=timerTypes
value=selection
value=statusType
}}
</div>
{{#if publishToCategory}}
<div class="control-group">
<label>{{i18n "topic.topic_status_update.publish_to"}}</label>
<label class="control-label">{{i18n "topic.topic_status_update.publish_to"}}</label>
{{category-chooser
value=topicTimer.category_id
excludeCategoryId=excludeCategoryId
@ -18,21 +18,31 @@
</div>
{{/if}}
{{#if showFutureDateInput}}
<div class="control-group">
{{future-date-input
input=(readonly topicTimer.updateTime)
duration=(readonly topicTimer.duration)
label="topic.topic_status_update.when"
statusType=selection
includeWeekend=true
<label class="control-label">{{i18n "topic.topic_status_update.when"}}</label>
{{time-shortcut-picker prefilledDatetime=topicTimer.execute_at onTimeSelected=onTimeSelected customOptions=customTimeShortcutOptions hiddenOptions=hiddenTimeShortcutOptions}}
{{/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=topicTimer.duration min="1"}}
</div>
{{/if}}
{{#if willCloseImmediately}}
<div class="warning">
{{d-icon "exclamation-triangle"}}
{{willCloseI18n}}
</div>
{{/if}}
{{#if showTopicStatusInfo}}
<div class="alert alert-info">
{{topic-timer-info
statusType=statusType
executeAt=executeAt
basedOnLastPost=topicTimer.based_on_last_post
onChangeInput=onChangeInput
onChangeDuration=onChangeDuration
duration=topicTimer.duration
categoryId=topicTimer.category_id
lastPostedAt=model.last_posted_at
isBasedOnDuration=autoDeleteReplies
durationType=durationType
}}
</div>
{{/if}}
</form>

View File

@ -1,26 +1,24 @@
<div class="future-date-input">
{{#unless isBasedOnDuration}}
<div class="control-group">
<label class={{labelClasses}}>{{displayLabel}}</label>
{{future-date-input-selector
minimumResultsForSearch=-1
statusType=statusType
value=(readonly selection)
input=(readonly input)
includeDateTime=includeDateTime
includeWeekend=includeWeekend
includeFarFuture=includeFarFuture
includeMidFuture=includeMidFuture
includeNow=includeNow
clearable=clearable
onChangeInput=onChangeInput
onChange=(action (mut selection))
options=(hash
none="topic.auto_update_input.none"
)
}}
</div>
{{/unless}}
<div class="control-group">
<label class={{labelClasses}}>{{displayLabel}}</label>
{{future-date-input-selector
minimumResultsForSearch=-1
statusType=statusType
value=(readonly selection)
input=(readonly input)
includeDateTime=includeDateTime
includeWeekend=includeWeekend
includeFarFuture=includeFarFuture
includeMidFuture=includeMidFuture
includeNow=includeNow
clearable=clearable
onChangeInput=onChangeInput
onChange=(action (mut selection))
options=(hash
none="topic.auto_update_input.none"
)
}}
</div>
{{#if displayDateAndTimePicker}}
<div class="control-group">
@ -37,32 +35,4 @@
{{input placeholder="--:--" type="time" class="time-input" value=time disabled=timeInputDisabled}}
</div>
{{/if}}
{{#if displayNumberInput}}
<div class="control-group">
<label>
{{durationLabel}}
{{text-field value=duration type="number"}}
</label>
</div>
{{#if willCloseImmediately}}
<div class="warning">
{{d-icon "exclamation-triangle"}}
{{willCloseI18n}}
</div>
{{/if}}
{{/if}}
{{#if showTopicStatusInfo}}
<div class="alert alert-info">
{{topic-timer-info
statusType=statusType
executeAt=executeAt
basedOnLastPost=basedOnLastPost
duration=duration
categoryId=categoryId
}}
</div>
{{/if}}
</div>

View File

@ -1,26 +1,26 @@
{{#d-modal-body title="topic.topic_status_update.title" autoFocus="false"}}
{{#d-modal-body title="topic.topic_status_update.title" autoFocus="false" id="topic-timer-modal"}}
{{edit-topic-timer-form
topic=model
topicTimer=topicTimer
timerTypes=publicTimerTypes
updateTime=updateTime
onChangeStatusType=(action "onChangeStatusType")
onChangeInput=(action "onChangeInput")
onChangeDuration=(action "onChangeDuration")
}}
<div class="modal-footer control-group edit-topic-timer-buttons">
{{d-button class="btn-primary"
disabled=saveDisabled
label="topic.topic_status_update.save"
action=(action "saveTimer")}}
{{conditional-loading-spinner size="small" condition=loading}}
{{#if topicTimer.execute_at}}
{{d-button class="pull-right btn-danger"
action=(action "removeTimer")
label="topic.topic_status_update.remove"}}
{{/if}}
</div>
{{/d-modal-body}}
<div class="modal-footer">
{{d-button class="btn-primary"
disabled=saveDisabled
label="topic.topic_status_update.save"
action=(action "saveTimer")}}
{{conditional-loading-spinner size="small" condition=loading}}
{{#if topicTimer.execute_at}}
{{d-button class="pull-right btn-danger"
action=(action "removeTimer")
label="topic.topic_status_update.remove"}}
{{/if}}
</div>

View File

@ -4,7 +4,7 @@ import {
updateCurrentUser,
} from "discourse/tests/helpers/qunit-helpers";
import { click, fillIn, visit } from "@ember/test-helpers";
import { skip, test } from "qunit";
import { test } from "qunit";
import selectKit from "discourse/tests/helpers/select-kit-helper";
acceptance("Topic - Edit timer", function (needs) {
@ -24,108 +24,56 @@ acceptance("Topic - Edit timer", function (needs) {
);
});
test("default", async function (assert) {
updateCurrentUser({ moderator: true });
const futureDateInputSelector = selectKit(".future-date-input-selector");
await visit("/t/internationalization-localization");
await click(".toggle-admin-menu");
await click(".topic-admin-status-update button");
assert.equal(
futureDateInputSelector.header().label(),
"Select a timeframe"
);
assert.equal(futureDateInputSelector.header().value(), null);
});
test("autoclose - specific time", async function (assert) {
updateCurrentUser({ moderator: true });
const futureDateInputSelector = selectKit(".future-date-input-selector");
await visit("/t/internationalization-localization");
await click(".toggle-admin-menu");
await click(".topic-admin-status-update button");
await futureDateInputSelector.expand();
await futureDateInputSelector.selectRowByValue("next_week");
assert.ok(futureDateInputSelector.header().label().includes("Next week"));
assert.equal(futureDateInputSelector.header().value(), "next_week");
await click("#tap_tile_next_week");
const regex = /will automatically close in/g;
const html = queryAll(".future-date-input .topic-status-info")
const html = queryAll(".edit-topic-timer-modal .topic-status-info")
.html()
.trim();
assert.ok(regex.test(html));
});
skip("autoclose", async function (assert) {
test("autoclose", async function (assert) {
updateCurrentUser({ moderator: true });
const futureDateInputSelector = selectKit(".future-date-input-selector");
await visit("/t/internationalization-localization");
await click(".toggle-admin-menu");
await click(".topic-admin-status-update button");
await futureDateInputSelector.expand();
await futureDateInputSelector.selectRowByValue("next_week");
assert.ok(futureDateInputSelector.header().label().includes("Next week"));
assert.equal(futureDateInputSelector.header().value(), "next_week");
await click("#tap_tile_next_week");
const regex1 = /will automatically close in/g;
const html1 = queryAll(".future-date-input .topic-status-info")
const html1 = queryAll(".edit-topic-timer-modal .topic-status-info")
.html()
.trim();
assert.ok(regex1.test(html1));
await futureDateInputSelector.expand();
await futureDateInputSelector.selectRowByValue("pick_date_and_time");
await fillIn(".future-date-input .date-picker", "2099-11-24");
assert.ok(
futureDateInputSelector.header().label().includes("Pick date and time")
);
assert.equal(
futureDateInputSelector.header().value(),
"pick_date_and_time"
);
await click("#tap_tile_custom");
await fillIn(".tap-tile-date-input .date-picker", "2099-11-24");
const regex2 = /will automatically close in/g;
const html2 = queryAll(".future-date-input .topic-status-info")
const html2 = queryAll(".edit-topic-timer-modal .topic-status-info")
.html()
.trim();
assert.ok(regex2.test(html2));
await futureDateInputSelector.expand();
await futureDateInputSelector.selectRowByValue("set_based_on_last_post");
await click("#tap_tile_set_based_on_last_post");
await fillIn("#topic_timer_duration", "2");
await fillIn(".future-date-input input[type=number]", "2");
assert.ok(
futureDateInputSelector
.header()
.label()
.includes("Close based on last post")
);
assert.equal(
futureDateInputSelector.header().value(),
"set_based_on_last_post"
);
const regex3 = /This topic will close.*after the last reply/g;
const html3 = queryAll(".future-date-input .topic-status-info")
.html()
.trim();
const regex3 = /last post in the topic is already/g;
const html3 = queryAll(".edit-topic-timer-modal .warning").html().trim();
assert.ok(regex3.test(html3));
});
test("close temporarily", async function (assert) {
updateCurrentUser({ moderator: true });
const timerType = selectKit(".select-kit.timer-type");
const futureDateInputSelector = selectKit(".future-date-input-selector");
await visit("/t/internationalization-localization");
await click(".toggle-admin-menu");
@ -134,40 +82,19 @@ acceptance("Topic - Edit timer", function (needs) {
await timerType.expand();
await timerType.selectRowByValue("open");
assert.equal(
futureDateInputSelector.header().label(),
"Select a timeframe"
);
assert.equal(futureDateInputSelector.header().value(), null);
await futureDateInputSelector.expand();
await futureDateInputSelector.selectRowByValue("next_week");
assert.ok(futureDateInputSelector.header().label().includes("Next week"));
assert.equal(futureDateInputSelector.header().value(), "next_week");
await click("#tap_tile_next_week");
const regex1 = /will automatically open in/g;
const html1 = queryAll(".future-date-input .topic-status-info")
const html1 = queryAll(".edit-topic-timer-modal .topic-status-info")
.html()
.trim();
assert.ok(regex1.test(html1));
await futureDateInputSelector.expand();
await futureDateInputSelector.selectRowByValue("pick_date_and_time");
await fillIn(".future-date-input .date-picker", "2099-11-24");
assert.equal(
futureDateInputSelector.header().label(),
"Pick date and time"
);
assert.equal(
futureDateInputSelector.header().value(),
"pick_date_and_time"
);
await click("#tap_tile_custom");
await fillIn(".tap-tile-date-input .date-picker", "2099-11-24");
const regex2 = /will automatically open in/g;
const html2 = queryAll(".future-date-input .topic-status-info")
const html2 = queryAll(".edit-topic-timer-modal .topic-status-info")
.html()
.trim();
assert.ok(regex2.test(html2));
@ -177,7 +104,6 @@ acceptance("Topic - Edit timer", function (needs) {
updateCurrentUser({ moderator: true });
const timerType = selectKit(".select-kit.timer-type");
const categoryChooser = selectKit(".modal-body .category-chooser");
const futureDateInputSelector = selectKit(".future-date-input-selector");
await visit("/t/internationalization-localization");
await click(".toggle-admin-menu");
@ -189,23 +115,13 @@ acceptance("Topic - Edit timer", function (needs) {
assert.equal(categoryChooser.header().label(), "uncategorized");
assert.equal(categoryChooser.header().value(), null);
assert.equal(
futureDateInputSelector.header().label(),
"Select a timeframe"
);
assert.equal(futureDateInputSelector.header().value(), null);
await categoryChooser.expand();
await categoryChooser.selectRowByValue("7");
await futureDateInputSelector.expand();
await futureDateInputSelector.selectRowByValue("next_week");
assert.ok(futureDateInputSelector.header().label().includes("Next week"));
assert.equal(futureDateInputSelector.header().value(), "next_week");
await click("#tap_tile_next_week");
const regex = /will be published to #dev/g;
const text = queryAll(".future-date-input .topic-status-info")
const text = queryAll(".edit-topic-timer-modal .topic-status-info")
.text()
.trim();
assert.ok(regex.test(text));
@ -228,7 +144,6 @@ acceptance("Topic - Edit timer", function (needs) {
test("auto delete", async function (assert) {
updateCurrentUser({ moderator: true });
const timerType = selectKit(".select-kit.timer-type");
const futureDateInputSelector = selectKit(".future-date-input-selector");
await visit("/t/internationalization-localization");
await click(".toggle-admin-menu");
@ -237,20 +152,10 @@ acceptance("Topic - Edit timer", function (needs) {
await timerType.expand();
await timerType.selectRowByValue("delete");
assert.equal(
futureDateInputSelector.header().label(),
"Select a timeframe"
);
assert.equal(futureDateInputSelector.header().value(), null);
await futureDateInputSelector.expand();
await futureDateInputSelector.selectRowByValue("two_weeks");
assert.ok(futureDateInputSelector.header().label().includes("Two Weeks"));
assert.equal(futureDateInputSelector.header().value(), "two_weeks");
await click("#tap_tile_two_weeks");
const regex = /will be automatically deleted/g;
const html = queryAll(".future-date-input .topic-status-info")
const html = queryAll(".edit-topic-timer-modal .topic-status-info")
.html()
.trim();
assert.ok(regex.test(html));
@ -258,14 +163,12 @@ acceptance("Topic - Edit timer", function (needs) {
test("Inline delete timer", async function (assert) {
updateCurrentUser({ moderator: true });
const futureDateInputSelector = selectKit(".future-date-input-selector");
await visit("/t/internationalization-localization");
await click(".toggle-admin-menu");
await click(".topic-admin-status-update button");
await futureDateInputSelector.expand();
await futureDateInputSelector.selectRowByValue("next_week");
await click(".modal-footer button.btn-primary");
await click("#tap_tile_next_week");
await click(".edit-topic-timer-buttons button.btn-primary");
const removeTimerButton = queryAll(
".topic-status-info .topic-timer-remove"

View File

@ -1,4 +1,3 @@
import { CLOSE_STATUS_TYPE } from "discourse/controllers/edit-topic-timer";
import ComboBoxComponent from "select-kit/components/combo-box";
import DatetimeMixin from "select-kit/components/future-date-input-selector/mixin";
import I18n from "I18n";
@ -125,11 +124,6 @@ export const TIMEFRAMES = [
enabled: (opts) => opts.includeDateTime,
icon: "far-calendar-plus",
}),
buildTimeframe({
id: "set_based_on_last_post",
enabled: (opts) => opts.includeBasedOnLastPost,
icon: "far-clock",
}),
];
let _timeframeById = null;
@ -147,7 +141,6 @@ export default ComboBoxComponent.extend(DatetimeMixin, {
pluginApiIdentifiers: ["future-date-input-selector"],
classNames: ["future-date-input-selector"],
isCustom: equal("value", "pick_date_and_time"),
isBasedOnLastPost: equal("value", "set_based_on_last_post"),
selectKitOptions: {
autoInsertNoneItem: false,
@ -168,7 +161,6 @@ export default ComboBoxComponent.extend(DatetimeMixin, {
includeMidFuture: this.includeMidFuture || true,
includeFarFuture: this.includeFarFuture,
includeDateTime: this.includeDateTime,
includeBasedOnLastPost: this.statusType === CLOSE_STATUS_TYPE,
canScheduleNow: this.includeNow || false,
canScheduleToday: 24 - now.hour() > 6,
};
@ -185,7 +177,7 @@ export default ComboBoxComponent.extend(DatetimeMixin, {
actions: {
onChange(value) {
if (value !== "pick_date_and_time" || !this.isBasedOnLastPost) {
if (value !== "pick_date_and_time") {
const { time } = this._updateAt(value);
if (time && !isEmpty(value)) {
this.attrs.onChangeInput &&

View File

@ -15,7 +15,7 @@
@import "directory";
@import "discourse";
@import "edit-category";
@import "edit-topic-status-update-modal";
@import "edit-topic-timer-modal";
@import "ember-select";
@import "emoji";
@import "exception";

View File

@ -1,33 +1,34 @@
.edit-topic-timer-modal {
.select-kit.combo-box {
width: 100%;
}
.modal-footer {
margin: 0;
border-top: 0;
padding: 10px 0;
}
.modal-body {
max-height: none;
overflow: visible !important; /* inline JS styles */
}
.control-group {
display: flex;
align-items: center;
width: 375px;
> .d-icon {
margin-right: 5px;
.control-label {
font-weight: 700;
}
}
input.date-picker,
input[type="time"] {
width: 200px;
text-align: left;
}
.radios {
margin-bottom: 10px;
}
label {
display: inline-flex;
padding-right: 5px;
margin-bottom: 0;
align-items: center;
input {
margin-top: 0;
}
}
.topic-timer-duration {
width: 100%;
}
.btn.pull-right {
margin-right: 10px;
}

View File

@ -21,6 +21,7 @@
@import "svg";
@import "tap-tile";
@import "time-input";
@import "time-shortcut-picker";
@import "user-card";
@import "user-info";
@import "user-stream-item";

View File

@ -31,39 +31,6 @@
}
}
.custom-date-time-wrap {
padding: 1em 1em 0.5em;
border: 1px solid var(--primary-low);
border-top: none;
background: var(--primary-very-low);
.d-icon {
padding: 0 0.75em 0 0;
color: var(--primary-high);
margin-top: -0.5em;
}
.tap-tile-date-input,
.tap-tile-time-input {
display: flex;
align-items: center;
input {
width: 100%;
min-width: unset;
}
}
.date-picker,
.time-input {
text-align: left;
padding-top: 5px;
}
.time-input,
.date-picker-wrapper {
flex: 1 1 auto;
}
}
.bookmark-name-wrap {
display: inline-flex;
width: 100%;

View File

@ -0,0 +1,32 @@
.custom-date-time-wrap {
padding: 1em 1em 0.5em;
border: 1px solid var(--primary-low);
border-top: none;
background: var(--primary-very-low);
.d-icon {
padding: 0 0.75em 0 0;
color: var(--primary-high);
margin-top: -0.5em;
}
.tap-tile-date-input,
.tap-tile-time-input {
display: flex;
align-items: center;
input {
width: 100%;
min-width: unset;
}
}
.date-picker,
.time-input {
text-align: left;
padding-top: 5px;
}
.time-input,
.date-picker-wrapper {
flex: 1 1 auto;
}
}

View File

@ -208,6 +208,9 @@ sub sub {
.btn.pull-right {
margin-right: 0;
}
.modal-body {
width: 375px;
}
}
.topic-footer-main-buttons {

View File

@ -24,6 +24,8 @@ module Jobs
# this handles deleting the topic timer as wel, see TopicStatusUpdater
topic.update_status('autoclosed', true, user, { silent: silent })
MessageBus.publish("/topic/#{topic.id}", reload_topic: true)
end
end
end

View File

@ -31,6 +31,8 @@ module Jobs
end
topic.inherit_auto_close_from_category(timer_type: :close)
MessageBus.publish("/topic/#{topic.id}", reload_topic: true)
end
end
end

View File

@ -2402,7 +2402,8 @@ en:
remove: "Remove Timer"
publish_to: "Publish To:"
when: "When:"
time_frame_required: Please select a time frame
time_frame_required: "Please select a time frame"
min_duration: "Duration must be greater than 0"
auto_update_input:
none: "Select a timeframe"
now: "Now"
@ -2456,6 +2457,9 @@ en:
auto_close_immediate:
one: "The last post in the topic is already %{count} hour old, so the topic will be closed immediately."
other: "The last post in the topic is already %{count} hours old, so the topic will be closed immediately."
auto_close_momentarily:
one: "The last post in the topic is already %{count} hour old, so the topic will be closed momentarily."
other: "The last post in the topic is already %{count} hours old, so the topic will be closed momentarily."
timeline:
back: "Back"

View File

@ -24,6 +24,14 @@ describe Jobs::CloseTopic do
end
end
it "publishes to the topic message bus so the topic status reloads" do
MessageBus.expects(:publish).at_least_once
MessageBus.expects(:publish).with("/topic/#{topic.id}", reload_topic: true).once
freeze_time(61.minutes.from_now) do
described_class.new.execute(topic_timer_id: topic.public_topic_timer.id)
end
end
describe 'when trying to close a topic that has already been closed' do
it 'should delete the topic timer' do
freeze_time(topic.public_topic_timer.execute_at + 1.minute)

View File

@ -25,6 +25,14 @@ describe Jobs::OpenTopic do
end
end
it "publishes to the topic message bus so the topic status reloads" do
MessageBus.expects(:publish).at_least_once
MessageBus.expects(:publish).with("/topic/#{topic.id}", reload_topic: true).once
freeze_time(61.minutes.from_now) do
described_class.new.execute(topic_timer_id: topic.public_topic_timer.id)
end
end
describe 'when category has auto close configured' do
fab!(:category) do
Fabricate(:category,