DEV: Convert `feature-topic` modal to component-based API (#23277)

Updated styles with some sign off from @jordanvidrine 



<img width="655" alt="Screenshot 2023-08-28 at 9 07 47 AM" src="https://github.com/discourse/discourse/assets/50783505/31b453ef-a787-436f-9fd9-48c9cd3a2e81">
This commit is contained in:
Isaac Janzen 2023-08-28 09:41:14 -05:00 committed by GitHub
parent 81d8c6ba6c
commit 6870f38e87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 428 additions and 401 deletions

View File

@ -0,0 +1,232 @@
<DModal
class="feature-topic"
@title={{i18n "topic.feature_topic.title"}}
@closeModal={{@closeModal}}
>
<:body>
{{#if @model.topic.pinned_at}}
<div class="feature-section">
<div class="desc">
{{#if @model.topic.pinned_globally}}
<p>
<ConditionalLoadingSpinner
@size="small"
@condition={{this.loading}}
>
{{#if this.pinnedGloballyCount}}
{{html-safe
(i18n
"topic.feature_topic.already_pinned_globally"
count=this.pinnedGloballyCount
)
}}
{{else}}
{{html-safe (i18n "topic.feature_topic.not_pinned_globally")}}
{{/if}}
</ConditionalLoadingSpinner>
</p>
<p>{{i18n "topic.feature_topic.global_pin_note"}}</p>
{{else}}
<p>
<ConditionalLoadingSpinner
@size="small"
@condition={{this.loading}}
>
{{html-safe this.alreadyPinnedMessage}}
</ConditionalLoadingSpinner>
</p>
<p>{{i18n "topic.feature_topic.pin_note"}}</p>
{{/if}}
<p>{{html-safe this.unPinMessage}}</p>
<p><DButton
@action={{this.unpin}}
@icon="thumbtack"
@label="topic.feature.unpin"
class="btn-primary"
/></p>
</div>
</div>
{{else}}
<div class="feature-section">
<div class="desc">
<p>
<ConditionalLoadingSpinner
@size="small"
@condition={{this.loading}}
>
{{html-safe this.alreadyPinnedMessage}}
</ConditionalLoadingSpinner>
</p>
<p>
{{i18n "topic.feature_topic.pin_note"}}
</p>
{{#if this.site.isMobileDevice}}
<p>
{{html-safe this.pinMessage}}
</p>
<p class="with-validation">
<FutureDateInput
class="pin-until"
@clearable={{true}}
@input={{@model.topic.pinnedInCategoryUntil}}
@onChangeInput={{action
(mut @model.topic.pinnedInCategoryUntil)
}}
/>
<PopupInputTip
@validation={{this.pinInCategoryValidation}}
@shownAt={{this.pinInCategoryTipShownAt}}
/>
</p>
{{else}}
<p class="with-validation">
{{html-safe this.pinMessage}}
<span>
{{d-icon "far-clock"}}
<FutureDateInput
class="pin-until"
@clearable={{true}}
@input={{@model.topic.pinnedInCategoryUntil}}
@onChangeInput={{action
(mut @model.topic.pinnedInCategoryUntil)
}}
/>
<PopupInputTip
@validation={{this.pinInCategoryValidation}}
@shownAt={{this.pinInCategoryTipShownAt}}
/>
</span>
</p>
{{/if}}
<p>
<DButton
@action={{this.pin}}
@icon="thumbtack"
@label="topic.feature.pin"
class="btn-primary"
/>
</p>
</div>
</div>
{{#if this.canPinGlobally}}
<hr />
<div class="feature-section">
<div class="desc">
<p>
<ConditionalLoadingSpinner
@size="small"
@condition={{this.loading}}
>
{{#if this.pinnedGloballyCount}}
{{html-safe
(i18n
"topic.feature_topic.already_pinned_globally"
count=this.pinnedGloballyCount
)
}}
{{else}}
{{html-safe (i18n "topic.feature_topic.not_pinned_globally")}}
{{/if}}
</ConditionalLoadingSpinner>
</p>
<p>
{{i18n "topic.feature_topic.global_pin_note"}}
</p>
{{#if this.site.isMobileDevice}}
<p>
{{i18n "topic.feature_topic.pin_globally"}}
</p>
<p class="with-validation">
<FutureDateInput
class="pin-until"
@clearable={{true}}
@input={{@model.topic.pinnedGloballyUntil}}
@onChangeInput={{action
(mut @model.topic.pinnedGloballyUntil)
}}
/>
<PopupInputTip
@validation={{this.pinGloballyValidation}}
@shownAt={{this.pinGloballyTipShownAt}}
/>
</p>
{{else}}
<p class="with-validation">
{{i18n "topic.feature_topic.pin_globally"}}
<span>
{{d-icon "far-clock"}}
<FutureDateInput
class="pin-until"
@clearable={{true}}
@input={{@model.topic.pinnedGloballyUntil}}
@onChangeInput={{action
(mut @model.topic.pinnedGloballyUntil)
}}
/>
<PopupInputTip
@validation={{this.pinGloballyValidation}}
@shownAt={{this.pinGloballyTipShownAt}}
/>
</span>
</p>
{{/if}}
<p>
<DButton
@action={{this.pinGlobally}}
@icon="thumbtack"
@label="topic.feature.pin_globally"
class="btn-primary"
/>
</p>
</div>
</div>
{{/if}}
{{/if}}
<hr />
{{#if this.currentUser.staff}}
<div class="feature-section">
<div class="desc">
<p>
<ConditionalLoadingSpinner
@size="small"
@condition={{this.loading}}
>
{{#if this.bannerCount}}
{{html-safe (i18n "topic.feature_topic.banner_exists")}}
{{else}}
{{html-safe (i18n "topic.feature_topic.no_banner_exists")}}
{{/if}}
</ConditionalLoadingSpinner>
</p>
<p>
{{i18n "topic.feature_topic.banner_note"}}
</p>
<p>
{{#if @model.topic.isBanner}}
{{i18n "topic.feature_topic.remove_banner"}}
{{else}}
{{i18n "topic.feature_topic.make_banner"}}
{{/if}}
</p>
<p>
{{#if @model.topic.isBanner}}
<DButton
@action={{this.removeBanner}}
@icon="thumbtack"
@label="topic.feature.remove_banner"
class="btn-primary"
/>
{{else}}
<DButton
@action={{this.makeBanner}}
@icon="thumbtack"
@label="topic.feature.make_banner"
class="btn-primary make-banner"
/>
{{/if}}
</p>
</div>
</div>
{{/if}}
</:body>
</DModal>

View File

@ -0,0 +1,180 @@
import Component from "@glimmer/component";
import I18n from "I18n";
import { inject as service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import EmberObject, { action } from "@ember/object";
import { categoryLinkHTML } from "discourse/helpers/category-link";
import { tracked } from "@glimmer/tracking";
export default class FeatureTopic extends Component {
@service currentUser;
@service dialog;
@tracked loading = true;
@tracked pinnedInCategoryCount = 0;
@tracked pinnedGloballyCount = 0;
@tracked bannerCount = 0;
@tracked pinInCategoryTipShownAt = false;
@tracked pinGloballyTipShownAt = false;
constructor() {
super(...arguments);
this.loadFeatureStats();
}
get categoryLink() {
return categoryLinkHTML(this.args.model.topic.category, {
allowUncategorized: true,
});
}
get unPinMessage() {
let name = "topic.feature_topic.unpin";
if (this.args.model.topic.pinned_globally) {
name += "_globally";
}
if (moment(this.args.model.topic.pinned_until) > moment()) {
name += "_until";
}
const until = moment(this.args.model.topic.pinned_until).format("LL");
return I18n.t(name, { categoryLink: this.categoryLink, until });
}
get canPinGlobally() {
return (
this.currentUser.canManageTopic &&
this.args.model.topic.details.can_pin_unpin_topic
);
}
get pinMessage() {
return I18n.t("topic.feature_topic.pin", {
categoryLink: this.categoryLink,
});
}
get alreadyPinnedMessage() {
const key =
this.pinnedInCategoryCount === 0
? "topic.feature_topic.not_pinned"
: "topic.feature_topic.already_pinned";
return I18n.t(key, {
categoryLink: this.categoryLink,
count: this.pinnedInCategoryCount,
});
}
get pinDisabled() {
return !this._isDateValid(this.parsedPinnedInCategoryUntil);
}
get pinGloballyDisabled() {
return !this._isDateValid(this.parsedPinnedGloballyUntil);
}
get parsedPinnedInCategoryUntil() {
return this._parseDate(this.args.model.topic.pinnedInCategoryUntil);
}
get parsedPinnedGloballyUntil() {
return this._parseDate(this.args.model.topic.pinnedGloballyUntil);
}
get pinInCategoryValidation() {
if (this.pinDisabled) {
return EmberObject.create({
failed: true,
reason: I18n.t("topic.feature_topic.pin_validation"),
});
}
}
get pinGloballyValidation() {
if (this.pinGloballyDisabled) {
return EmberObject.create({
failed: true,
reason: I18n.t("topic.feature_topic.pin_validation"),
});
}
}
_parseDate(date) {
return moment(date, ["YYYY-MM-DD", "YYYY-MM-DD HH:mm"]);
}
_isDateValid(parsedDate) {
return parsedDate.isValid() && parsedDate > moment();
}
@action
async loadFeatureStats() {
try {
this.loading = true;
const result = await ajax("/topics/feature_stats.json", {
data: { category_id: this.args.model.topic.category.id },
});
if (result) {
this.pinnedInCategoryCount = result.pinned_in_category_count;
this.pinnedGloballyCount = result.pinned_globally_count;
this.bannerCount = result.banner_count;
}
} finally {
this.loading = false;
}
}
async _confirmBeforePinningGlobally() {
if (this.pinnedGloballyCount < 4) {
this.args.model.pinGlobally();
this.args.closeModal();
} else {
this.dialog.yesNoConfirm({
message: I18n.t("topic.feature_topic.confirm_pin_globally", {
count: this.pinnedGloballyCount,
}),
didConfirm: () => {
this.args.model.pinGlobally();
this.args.closeModal();
},
});
}
}
@action
pin() {
if (this.pinDisabled) {
this.pinInCategoryTipShownAt = Date.now();
} else {
this.args.model.togglePinned();
this.args.closeModal();
}
}
@action
pinGlobally() {
if (this.pinGloballyDisabled) {
this.pinGloballyTipShownAt = Date.now();
} else {
this._confirmBeforePinningGlobally();
}
}
@action
unpin() {
this.args.model.togglePinned();
this.args.closeModal();
}
@action
makeBanner() {
this.args.model.makeBanner();
this.args.closeModal();
}
@action
removeBanner() {
this.args.model.removeBanner();
this.args.closeModal();
}
}

View File

@ -1,183 +0,0 @@
import Controller, { inject as controller } from "@ember/controller";
import EmberObject from "@ember/object";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { ajax } from "discourse/lib/ajax";
import { categoryLinkHTML } from "discourse/helpers/category-link";
import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service";
export default Controller.extend(ModalFunctionality, {
topicController: controller("topic"),
dialog: service(),
loading: true,
pinnedInCategoryCount: 0,
pinnedGloballyCount: 0,
bannerCount: 0,
reset() {
this.setProperties({
"model.pinnedInCategoryUntil": null,
"model.pinnedGloballyUntil": null,
pinInCategoryTipShownAt: false,
pinGloballyTipShownAt: false,
});
},
@discourseComputed("model.category")
categoryLink(category) {
return categoryLinkHTML(category, { allowUncategorized: true });
},
@discourseComputed(
"categoryLink",
"model.pinned_globally",
"model.pinned_until"
)
unPinMessage(categoryLink, pinnedGlobally, pinnedUntil) {
let name = "topic.feature_topic.unpin";
if (pinnedGlobally) {
name += "_globally";
}
if (moment(pinnedUntil) > moment()) {
name += "_until";
}
const until = moment(pinnedUntil).format("LL");
return I18n.t(name, { categoryLink, until });
},
@discourseComputed("model.details.can_pin_unpin_topic")
canPinGlobally(canPinUnpinTopic) {
return this.currentUser.canManageTopic && canPinUnpinTopic;
},
@discourseComputed("categoryLink")
pinMessage(categoryLink) {
return I18n.t("topic.feature_topic.pin", { categoryLink });
},
@discourseComputed("categoryLink", "pinnedInCategoryCount")
alreadyPinnedMessage(categoryLink, count) {
const key =
count === 0
? "topic.feature_topic.not_pinned"
: "topic.feature_topic.already_pinned";
return I18n.t(key, { categoryLink, count });
},
@discourseComputed("parsedPinnedInCategoryUntil")
pinDisabled(parsedPinnedInCategoryUntil) {
return !this._isDateValid(parsedPinnedInCategoryUntil);
},
@discourseComputed("parsedPinnedGloballyUntil")
pinGloballyDisabled(parsedPinnedGloballyUntil) {
return !this._isDateValid(parsedPinnedGloballyUntil);
},
@discourseComputed("model.pinnedInCategoryUntil")
parsedPinnedInCategoryUntil(pinnedInCategoryUntil) {
return this._parseDate(pinnedInCategoryUntil);
},
@discourseComputed("model.pinnedGloballyUntil")
parsedPinnedGloballyUntil(pinnedGloballyUntil) {
return this._parseDate(pinnedGloballyUntil);
},
@discourseComputed("pinDisabled")
pinInCategoryValidation(pinDisabled) {
if (pinDisabled) {
return EmberObject.create({
failed: true,
reason: I18n.t("topic.feature_topic.pin_validation"),
});
}
},
@discourseComputed("pinGloballyDisabled")
pinGloballyValidation(pinGloballyDisabled) {
if (pinGloballyDisabled) {
return EmberObject.create({
failed: true,
reason: I18n.t("topic.feature_topic.pin_validation"),
});
}
},
_parseDate(date) {
return moment(date, ["YYYY-MM-DD", "YYYY-MM-DD HH:mm"]);
},
_isDateValid(parsedDate) {
return parsedDate.isValid() && parsedDate > moment();
},
onShow() {
this.set("loading", true);
return ajax("/topics/feature_stats.json", {
data: { category_id: this.get("model.category.id") },
})
.then((result) => {
if (result) {
this.setProperties({
pinnedInCategoryCount: result.pinned_in_category_count,
pinnedGloballyCount: result.pinned_globally_count,
bannerCount: result.banner_count,
});
}
})
.finally(() => this.set("loading", false));
},
_forwardAction(name) {
this.topicController.send(name);
this.send("closeModal");
},
_confirmBeforePinningGlobally() {
const count = this.pinnedGloballyCount;
if (count < 4) {
this._forwardAction("pinGlobally");
} else {
this.send("hideModal");
this.dialog.yesNoConfirm({
message: I18n.t("topic.feature_topic.confirm_pin_globally", { count }),
didConfirm: () => this._forwardAction("pinGlobally"),
didCancel: () => this.send("reopenModal"),
});
}
},
actions: {
pin() {
if (this.pinDisabled) {
this.set("pinInCategoryTipShownAt", Date.now());
} else {
this._forwardAction("togglePinned");
}
},
pinGlobally() {
if (this.pinGloballyDisabled) {
this.set("pinGloballyTipShownAt", Date.now());
} else {
this._confirmBeforePinningGlobally();
}
},
unpin() {
this._forwardAction("togglePinned");
},
makeBanner() {
this._forwardAction("makeBanner");
},
removeBanner() {
this._forwardAction("removeBanner");
},
},
});

View File

@ -15,6 +15,7 @@ import PublishPageModal from "discourse/components/modal/publish-page";
import EditSlowModeModal from "discourse/components/modal/edit-slow-mode";
import ChangeTimestampModal from "discourse/components/modal/change-timestamp";
import EditTopicTimerModal from "discourse/components/modal/edit-topic-timer";
import FeatureTopicModal from "discourse/components/modal/feature-topic";
const SCROLL_DELAY = 500;
@ -154,11 +155,22 @@ const TopicRoute = DiscourseRoute.extend({
@action
showFeatureTopic() {
showModal("feature-topic", {
model: this.modelFor("topic"),
title: "topic.feature_topic.title",
const topicController = this.controllerFor("topic");
const model = this.modelFor("topic");
model.setProperties({
pinnedInCategoryUntil: null,
pinnedGloballyUntil: null,
});
this.modal.show(FeatureTopicModal, {
model: {
topic: model,
pinGlobally: () => topicController.send("pinGlobally"),
togglePinned: () => topicController.send("togglePinned"),
makeBanner: () => topicController.send("makeBanner"),
removeBanner: () => topicController.send("removeBanner"),
},
});
this.controllerFor("feature_topic").reset();
},
@action

View File

@ -18,7 +18,6 @@ const KNOWN_LEGACY_MODALS = [
"create-account",
"create-invite-bulk",
"create-invite",
"feature-topic",
"flag",
"grant-badge",
"group-default-notifications",

View File

@ -1,213 +0,0 @@
<DModalBody @class="feature-topic">
{{#if this.model.pinned_at}}
<div class="feature-section">
<div class="desc">
{{#if this.model.pinned_globally}}
<p>
<ConditionalLoadingSpinner
@size="small"
@condition={{this.loading}}
>
{{#if this.pinnedGloballyCount}}
{{html-safe
(i18n
"topic.feature_topic.already_pinned_globally"
count=this.pinnedGloballyCount
)
}}
{{else}}
{{html-safe (i18n "topic.feature_topic.not_pinned_globally")}}
{{/if}}
</ConditionalLoadingSpinner>
</p>
<p>{{i18n "topic.feature_topic.global_pin_note"}}</p>
{{else}}
<p>
<ConditionalLoadingSpinner
@size="small"
@condition={{this.loading}}
>
{{html-safe this.alreadyPinnedMessage}}
</ConditionalLoadingSpinner>
</p>
<p>{{i18n "topic.feature_topic.pin_note"}}</p>
{{/if}}
<p>{{html-safe this.unPinMessage}}</p>
<p><DButton
@action={{action "unpin"}}
@icon="thumbtack"
@label="topic.feature.unpin"
@class="btn-primary"
/></p>
</div>
</div>
{{else}}
<div class="feature-section">
<div class="desc">
<p>
<ConditionalLoadingSpinner @size="small" @condition={{this.loading}}>
{{html-safe this.alreadyPinnedMessage}}
</ConditionalLoadingSpinner>
</p>
<p>
{{i18n "topic.feature_topic.pin_note"}}
</p>
{{#if this.site.isMobileDevice}}
<p>
{{html-safe this.pinMessage}}
</p>
<p class="with-validation">
<FutureDateInput
@class="pin-until"
@clearable={{true}}
@input={{this.model.pinnedInCategoryUntil}}
@onChangeInput={{action (mut this.model.pinnedInCategoryUntil)}}
/>
<PopupInputTip
@validation={{this.pinInCategoryValidation}}
@shownAt={{this.pinInCategoryTipShownAt}}
/>
</p>
{{else}}
<p class="with-validation">
{{html-safe this.pinMessage}}
<span>
{{d-icon "far-clock"}}
<FutureDateInput
@class="pin-until"
@clearable={{true}}
@input={{this.model.pinnedInCategoryUntil}}
@onChangeInput={{action (mut this.model.pinnedInCategoryUntil)}}
/>
<PopupInputTip
@validation={{this.pinInCategoryValidation}}
@shownAt={{this.pinInCategoryTipShownAt}}
/>
</span>
</p>
{{/if}}
<p>
<DButton
@action={{action "pin"}}
@icon="thumbtack"
@label="topic.feature.pin"
@class="btn-primary"
/>
</p>
</div>
</div>
{{#if this.canPinGlobally}}
<div class="feature-section">
<div class="desc">
<p>
<ConditionalLoadingSpinner
@size="small"
@condition={{this.loading}}
>
{{#if this.pinnedGloballyCount}}
{{html-safe
(i18n
"topic.feature_topic.already_pinned_globally"
count=this.pinnedGloballyCount
)
}}
{{else}}
{{html-safe (i18n "topic.feature_topic.not_pinned_globally")}}
{{/if}}
</ConditionalLoadingSpinner>
</p>
<p>
{{i18n "topic.feature_topic.global_pin_note"}}
</p>
{{#if this.site.isMobileDevice}}
<p>
{{i18n "topic.feature_topic.pin_globally"}}
</p>
<p class="with-validation">
<FutureDateInput
@class="pin-until"
@clearable={{true}}
@input={{this.model.pinnedGloballyUntil}}
@onChangeInput={{action (mut this.model.pinnedGloballyUntil)}}
/>
<PopupInputTip
@validation={{this.pinGloballyValidation}}
@shownAt={{this.pinGloballyTipShownAt}}
/>
</p>
{{else}}
<p class="with-validation">
{{i18n "topic.feature_topic.pin_globally"}}
<span>
{{d-icon "far-clock"}}
<FutureDateInput
@class="pin-until"
@clearable={{true}}
@input={{this.model.pinnedGloballyUntil}}
@onChangeInput={{action (mut this.model.pinnedGloballyUntil)}}
/>
<PopupInputTip
@validation={{this.pinGloballyValidation}}
@shownAt={{this.pinGloballyTipShownAt}}
/>
</span>
</p>
{{/if}}
<p>
<DButton
@action={{action "pinGlobally"}}
@icon="thumbtack"
@label="topic.feature.pin_globally"
@class="btn-primary"
/>
</p>
</div>
</div>
{{/if}}
{{/if}}
{{#if this.currentUser.staff}}
<div class="feature-section">
<div class="desc">
<p>
<ConditionalLoadingSpinner @size="small" @condition={{this.loading}}>
{{#if this.bannerCount}}
{{html-safe (i18n "topic.feature_topic.banner_exists")}}
{{else}}
{{html-safe (i18n "topic.feature_topic.no_banner_exists")}}
{{/if}}
</ConditionalLoadingSpinner>
</p>
<p>
{{i18n "topic.feature_topic.banner_note"}}
</p>
<p>
{{#if this.model.isBanner}}
{{i18n "topic.feature_topic.remove_banner"}}
{{else}}
{{i18n "topic.feature_topic.make_banner"}}
{{/if}}
</p>
<p>
{{#if this.model.isBanner}}
<DButton
@action={{action "removeBanner"}}
@icon="thumbtack"
@label="topic.feature.remove_banner"
@class="btn-primary"
/>
{{else}}
<DButton
@action={{action "makeBanner"}}
@icon="thumbtack"
@label="topic.feature.make_banner"
@class="btn-primary make-banner"
/>
{{/if}}
</p>
</div>
</div>
{{/if}}
</DModalBody>
<div class="modal-footer">
<DModalCancel @close={{route-action "closeModal"}} />
</div>