DEV: Convert edit-topic-timer modal to component-based API (#23252)

# Before
<img width="479" alt="Screenshot 2023-08-25 at 10 52 04 AM" src="https://github.com/discourse/discourse/assets/50783505/65ec8f94-d3e3-4520-a3f0-b265af069e83">

# After
<img width="569" alt="Screenshot 2023-08-25 at 10 53 44 AM" src="https://github.com/discourse/discourse/assets/50783505/35d12af7-c287-42f8-bf9c-d0ae1b5a08db">
This commit is contained in:
Isaac Janzen 2023-08-25 12:17:34 -05:00 committed by GitHub
parent 92bc61b4be
commit dcbfb8c54b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 268 additions and 281 deletions

View File

@ -6,7 +6,7 @@ import {
DELETE_STATUS_TYPE, DELETE_STATUS_TYPE,
OPEN_STATUS_TYPE, OPEN_STATUS_TYPE,
PUBLISH_TO_CATEGORY_STATUS_TYPE, PUBLISH_TO_CATEGORY_STATUS_TYPE,
} from "discourse/controllers/edit-topic-timer"; } from "discourse/components/modal/edit-topic-timer";
import { FORMAT } from "select-kit/components/future-date-input-selector"; import { FORMAT } from "select-kit/components/future-date-input-selector";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { equal, or, readOnly } from "@ember/object/computed"; import { equal, or, readOnly } from "@ember/object/computed";

View File

@ -0,0 +1,34 @@
<DModal
@title={{i18n "topic.topic_status_update.title"}}
@flash={{this.flash}}
@closeModal={{@closeModal}}
autoFocus="false"
id="topic-timer-modal"
class="edit-topic-timer-modal"
>
<:body>
<EditTopicTimerForm
@topic={{@model.topic}}
@topicTimer={{this.topicTimer}}
@timerTypes={{this.publicTimerTypes}}
@onChangeStatusType={{this.onChangeStatusType}}
@onChangeInput={{this.onChangeInput}}
/>
</:body>
<:footer>
<DButton
class="btn-primary"
@disabled={{this.saveDisabled}}
@label="topic.topic_status_update.save"
@action={{this.saveTimer}}
@isLoading={{this.loading}}
/>
{{#if this.topicTimer.execute_at}}
<DButton
class="btn-danger"
@action={{this.removeTimer}}
@label="topic.topic_status_update.remove"
/>
{{/if}}
</:footer>
</DModal>

View File

@ -0,0 +1,218 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import I18n from "I18n";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { tracked } from "@glimmer/tracking";
import { FORMAT } from "select-kit/components/future-date-input-selector";
import TopicTimer from "discourse/models/topic-timer";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
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";
export const BUMP_TYPE = "bump";
export const DELETE_REPLIES_TYPE = "delete_replies";
export default class EditTopicTimer extends Component {
@service currentUser;
@tracked
topicTimer = new TrackedObject(
this.args.model.topic?.topic_timer || this.createDefaultTimer()
);
@tracked loading = false;
@tracked flash;
get defaultStatusType() {
return this.publicTimerTypes[0].id;
}
get publicTimerTypes() {
const types = [];
const { closed, category, isPrivateMessage, invisible } =
this.args.model.topic;
if (!closed) {
types.push({
id: CLOSE_STATUS_TYPE,
name: I18n.t("topic.auto_close.title"),
});
types.push({
id: CLOSE_AFTER_LAST_POST_STATUS_TYPE,
name: I18n.t("topic.auto_close_after_last_post.title"),
});
}
if (closed) {
types.push({
id: OPEN_STATUS_TYPE,
name: I18n.t("topic.auto_reopen.title"),
});
}
if (this.args.model.topic.details.can_delete) {
types.push({
id: DELETE_STATUS_TYPE,
name: I18n.t("topic.auto_delete.title"),
});
}
types.push({
id: BUMP_TYPE,
name: I18n.t("topic.auto_bump.title"),
});
if (this.args.model.topic.details.can_delete) {
types.push({
id: DELETE_REPLIES_TYPE,
name: I18n.t("topic.auto_delete_replies.title"),
});
}
if (closed) {
types.push({
id: CLOSE_STATUS_TYPE,
name: I18n.t("topic.temp_open.title"),
});
}
if (!closed) {
types.push({
id: OPEN_STATUS_TYPE,
name: I18n.t("topic.temp_close.title"),
});
}
if (
(category && category.read_restricted) ||
isPrivateMessage ||
invisible
) {
types.push({
id: PUBLISH_TO_CATEGORY_STATUS_TYPE,
name: I18n.t("topic.publish_to_category.title"),
});
}
return types;
}
_setTimer(time, durationMinutes, statusType, basedOnLastPost, categoryId) {
this.loading = true;
TopicTimer.update(
this.args.model.topic.id,
time,
basedOnLastPost,
statusType,
categoryId,
durationMinutes
)
.then((result) => {
if (time || durationMinutes) {
this.args.model.updateTopicTimerProperty(
"execute_at",
result.execute_at
);
this.args.model.updateTopicTimerProperty(
"duration_minutes",
result.duration_minutes
);
this.args.model.updateTopicTimerProperty(
"category_id",
result.category_id
);
this.args.model.updateTopicTimerProperty("closed", result.closed);
this.args.closeModal();
} else {
const topicTimer = this.createDefaultTimer();
this.topicTime = topicTimer;
this.args.model.setTopicTimer(topicTimer);
this.onChangeInput(null, null);
}
})
.catch(popupAjaxError)
.finally(() => (this.loading = false));
}
@action
createDefaultTimer() {
const defaultTimer = TopicTimer.create({
status_type: this.defaultStatusType,
});
this.args.model.setTopicTimer(defaultTimer);
return defaultTimer;
}
@action
onChangeStatusType(value) {
const basedOnLastPost = CLOSE_AFTER_LAST_POST_STATUS_TYPE === value;
this.topicTimer.based_on_last_post = basedOnLastPost;
this.args.model.updateTopicTimerProperty(
"based_on_last_post",
basedOnLastPost
);
this.topicTimer.status_type = value;
this.args.model.updateTopicTimerProperty("status_type", value);
}
@action
onChangeInput(_type, time) {
if (moment.isMoment(time)) {
time = time.format(FORMAT);
}
this.topicTimer.updateTime = time;
this.args.model.updateTopicTimerProperty("updateTime", time);
}
@action
async saveTimer() {
this.flash = null;
if (!this.topicTimer.updateTime && !this.topicTimer.duration_minutes) {
this.flash = I18n.t("topic.topic_status_update.time_frame_required");
return;
}
if (this.topicTimer.duration_minutes && !this.topicTimer.updateTime) {
if (this.topicTimer.duration_minutes <= 0) {
this.flash = I18n.t("topic.topic_status_update.min_duration");
return;
}
// cannot be more than 20 years
if (this.topicTimer.duration_minutes > 20 * 365 * 1440) {
this.flash = I18n.t("topic.topic_status_update.max_duration");
return;
}
}
let statusType = this.topicTimer.status_type;
if (statusType === CLOSE_AFTER_LAST_POST_STATUS_TYPE) {
statusType = CLOSE_STATUS_TYPE;
}
await this._setTimer(
this.topicTimer.updateTime,
this.topicTimer.duration_minutes,
statusType,
this.topicTimer.based_on_last_post,
this.topicTimer.category_id
);
}
@action
async removeTimer() {
let statusType = this.topicTimer.status_type;
if (statusType === CLOSE_AFTER_LAST_POST_STATUS_TYPE) {
statusType = CLOSE_STATUS_TYPE;
}
await this._setTimer(null, null, statusType);
// timer has been removed and we are removing `execute_at`
// which will hide the remove timer button from the modal
this.topicTimer.execute_at = null;
}
}

View File

@ -2,7 +2,7 @@ import { cancel } from "@ember/runloop";
import discourseLater from "discourse-common/lib/later"; import discourseLater from "discourse-common/lib/later";
import Category from "discourse/models/category"; import Category from "discourse/models/category";
import Component from "@ember/component"; import Component from "@ember/component";
import { DELETE_REPLIES_TYPE } from "discourse/controllers/edit-topic-timer"; import { DELETE_REPLIES_TYPE } from "discourse/components/modal/edit-topic-timer";
import I18n from "I18n"; import I18n from "I18n";
import discourseComputed, { on } from "discourse-common/utils/decorators"; import discourseComputed, { on } from "discourse-common/utils/decorators";
import { iconHTML } from "discourse-common/lib/icon-library"; import { iconHTML } from "discourse-common/lib/icon-library";

View File

@ -1,231 +0,0 @@
import EmberObject, { setProperties } from "@ember/object";
import Controller from "@ember/controller";
import { FORMAT } from "select-kit/components/future-date-input-selector";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import TopicTimer from "discourse/models/topic-timer";
import { alias } from "@ember/object/computed";
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";
export const BUMP_TYPE = "bump";
export const DELETE_REPLIES_TYPE = "delete_replies";
export default Controller.extend(ModalFunctionality, {
loading: false,
isPublic: "true",
@discourseComputed(
"model.closed",
"model.category",
"model.isPrivateMessage",
"model.invisible"
)
publicTimerTypes(closed, category, isPrivateMessage, invisible) {
let types = [];
if (!closed) {
types.push({
id: CLOSE_STATUS_TYPE,
name: I18n.t("topic.auto_close.title"),
});
types.push({
id: CLOSE_AFTER_LAST_POST_STATUS_TYPE,
name: I18n.t("topic.auto_close_after_last_post.title"),
});
}
if (closed) {
types.push({
id: OPEN_STATUS_TYPE,
name: I18n.t("topic.auto_reopen.title"),
});
}
if (this.model.details.can_delete) {
types.push({
id: DELETE_STATUS_TYPE,
name: I18n.t("topic.auto_delete.title"),
});
}
types.push({
id: BUMP_TYPE,
name: I18n.t("topic.auto_bump.title"),
});
if (this.model.details.can_delete) {
types.push({
id: DELETE_REPLIES_TYPE,
name: I18n.t("topic.auto_delete_replies.title"),
});
}
if (closed) {
types.push({
id: CLOSE_STATUS_TYPE,
name: I18n.t("topic.temp_open.title"),
});
}
if (!closed) {
types.push({
id: OPEN_STATUS_TYPE,
name: I18n.t("topic.temp_close.title"),
});
}
if (
(category && category.read_restricted) ||
isPrivateMessage ||
invisible
) {
types.push({
id: PUBLISH_TO_CATEGORY_STATUS_TYPE,
name: I18n.t("topic.publish_to_category.title"),
});
}
return types;
},
topicTimer: alias("model.topic_timer"),
_setTimer(time, durationMinutes, statusType, basedOnLastPost, categoryId) {
this.set("loading", true);
TopicTimer.update(
this.get("model.id"),
time,
basedOnLastPost,
statusType,
categoryId,
durationMinutes
)
.then((result) => {
if (time || durationMinutes) {
this.send("closeModal");
setProperties(this.topicTimer, {
execute_at: result.execute_at,
duration_minutes: result.duration_minutes,
category_id: result.category_id,
});
this.set("model.closed", result.closed);
} else {
this.set(
"model.topic_timer",
EmberObject.create({ status_type: this.defaultStatusType })
);
this.send("onChangeInput", null, null);
}
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
},
onShow() {
let time = null;
const executeAt = this.get("topicTimer.execute_at");
if (executeAt) {
const closeTime = moment(executeAt);
if (closeTime > moment()) {
time = closeTime.format(FORMAT);
}
}
this.send("onChangeInput", null, time);
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")
defaultStatusType(publicTimerTypes) {
return publicTimerTypes[0].id;
},
actions: {
onChangeStatusType(value) {
this.setProperties({
"topicTimer.based_on_last_post":
CLOSE_AFTER_LAST_POST_STATUS_TYPE === value,
"topicTimer.status_type": value,
});
},
onChangeInput(_type, time) {
if (moment.isMoment(time)) {
time = time.format(FORMAT);
}
this.set("topicTimer.updateTime", time);
},
saveTimer() {
if (
!this.get("topicTimer.updateTime") &&
!this.get("topicTimer.duration_minutes")
) {
this.flash(
I18n.t("topic.topic_status_update.time_frame_required"),
"error"
);
return;
}
if (
this.get("topicTimer.duration_minutes") &&
!this.get("topicTimer.updateTime")
) {
if (this.get("topicTimer.duration_minutes") <= 0) {
this.flash(I18n.t("topic.topic_status_update.min_duration"), "error");
return;
}
// cannot be more than 20 years
if (this.get("topicTimer.duration_minutes") > 20 * 365 * 1440) {
this.flash(I18n.t("topic.topic_status_update.max_duration"), "error");
return;
}
}
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"),
statusType,
this.get("topicTimer.based_on_last_post"),
this.get("topicTimer.category_id")
);
},
removeTimer() {
let statusType = this.get("topicTimer.status_type");
if (statusType === CLOSE_AFTER_LAST_POST_STATUS_TYPE) {
statusType = CLOSE_STATUS_TYPE;
}
this._setTimer(null, null, statusType);
},
},
});

View File

@ -14,6 +14,7 @@ import HistoryModal from "discourse/components/modal/history";
import PublishPageModal from "discourse/components/modal/publish-page"; import PublishPageModal from "discourse/components/modal/publish-page";
import EditSlowModeModal from "discourse/components/modal/edit-slow-mode"; import EditSlowModeModal from "discourse/components/modal/edit-slow-mode";
import ChangeTimestampModal from "discourse/components/modal/change-timestamp"; import ChangeTimestampModal from "discourse/components/modal/change-timestamp";
import EditTopicTimerModal from "discourse/components/modal/edit-topic-timer";
const SCROLL_DELAY = 500; const SCROLL_DELAY = 500;
@ -123,12 +124,18 @@ const TopicRoute = DiscourseRoute.extend({
@action @action
showTopicTimerModal() { showTopicTimerModal() {
const model = this.modelFor("topic"); const model = this.modelFor("topic");
this.modal.show(EditTopicTimerModal, {
model: {
topic: model,
setTopicTimer: (v) => model.set("topic_timer", v),
updateTopicTimerProperty: this.updateTopicTimerProperty,
},
});
},
if (!model.get("topic_timer")) { @action
model.set("topic_timer", {}); updateTopicTimerProperty(property, value) {
} this.modelFor("topic").set(`topic_timer.${property}`, value);
showModal("edit-topic-timer", { model });
}, },
@action @action

View File

@ -18,7 +18,6 @@ const KNOWN_LEGACY_MODALS = [
"create-account", "create-account",
"create-invite-bulk", "create-invite-bulk",
"create-invite", "create-invite",
"edit-topic-timer",
"explain-reviewable", "explain-reviewable",
"feature-topic-on-profile", "feature-topic-on-profile",
"feature-topic", "feature-topic",

View File

@ -1,32 +0,0 @@
<DModalBody
@title="topic.topic_status_update.title"
@autoFocus="false"
@id="topic-timer-modal"
>
<EditTopicTimerForm
@topic={{this.model}}
@topicTimer={{this.topicTimer}}
@timerTypes={{this.publicTimerTypes}}
@onChangeStatusType={{action "onChangeStatusType"}}
@onChangeInput={{action "onChangeInput"}}
/>
<div class="modal-footer control-group edit-topic-timer-buttons">
<DButton
@class="btn-primary"
@disabled={{this.saveDisabled}}
@label="topic.topic_status_update.save"
@action={{action "saveTimer"}}
/>
<ConditionalLoadingSpinner @size="small" @condition={{this.loading}} />
{{#if this.topicTimer.execute_at}}
<DButton
@class="pull-right btn-danger"
@action={{action "removeTimer"}}
@label="topic.topic_status_update.remove"
/>
{{/if}}
</div>
</DModalBody>

View File

@ -268,7 +268,7 @@ acceptance("Topic - Edit timer", function (needs) {
await click("#tap_tile_custom"); await click("#tap_tile_custom");
await fillIn(".tap-tile-date-input .date-picker", "2100-11-24"); await fillIn(".tap-tile-date-input .date-picker", "2100-11-24");
await fillIn("#custom-time", "10:30"); await fillIn("#custom-time", "10:30");
await click(".edit-topic-timer-buttons button.btn-primary"); await click(".edit-topic-timer-modal button.btn-primary");
await click(".toggle-admin-menu"); await click(".toggle-admin-menu");
await click(".admin-topic-timer-update button"); await click(".admin-topic-timer-update button");
@ -380,7 +380,7 @@ acceptance("Topic - Edit timer", function (needs) {
await click(".toggle-admin-menu"); await click(".toggle-admin-menu");
await click(".admin-topic-timer-update button"); await click(".admin-topic-timer-update button");
await click("#tap_tile_start_of_next_business_week"); await click("#tap_tile_start_of_next_business_week");
await click(".edit-topic-timer-buttons button.btn-primary"); await click(".edit-topic-timer-modal button.btn-primary");
const removeTimerButton = query(".topic-timer-info .topic-timer-remove"); const removeTimerButton = query(".topic-timer-info .topic-timer-remove");
assert.strictEqual(removeTimerButton.getAttribute("title"), "remove timer"); assert.strictEqual(removeTimerButton.getAttribute("title"), "remove timer");

View File

@ -2,11 +2,6 @@
.select-kit.combo-box { .select-kit.combo-box {
width: 100%; width: 100%;
} }
.modal-footer {
margin: 0;
border-top: 0;
padding: 10px 0;
}
.modal-inner-container { .modal-inner-container {
box-sizing: border-box; box-sizing: border-box;
min-width: 310px; min-width: 310px;
@ -38,9 +33,6 @@
.topic-timer-duration { .topic-timer-duration {
width: 100%; width: 100%;
} }
.btn.pull-right {
margin-right: 10px;
}
.pika-single { .pika-single {
position: absolute !important; /* inline JS styles */ position: absolute !important; /* inline JS styles */
} }