mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
DEV: Refactor discovery routes to remove use of 'named outlets' (#22622)
The motivation of this PR is to remove our dependence on Ember's 'named outlets', which are removed in Ember 4+. At a high-level, the changes can be summarized as: - The top-level `discovery` route is totally emptied of all logic. The HTML structure of the template is moved into the `<Discovery::Layout />` component for use by child routes. - `AbstractTopicRoute` and `AbstractCategoryRoute` routes now both lean on the `DiscoverySortableController` and associated template. This controller is where most of the logic from the old top-level `discovery` controller has ended up. - All navigation controllers/templates have been replaced with components. `navigation/categories`, `navigation/category` and `navigation/default` were very similar, and so they've all been combined into `<Navigation::Default>`. `navigation/filter` gets its own component. - The `discovery/topics` controller/template have been moved into a new `<Discovery::Topics>` component. Various other parts of the app have been tweaked to support these changes, but I've tried to keep that to a minimum. Anything from `<TopicList>` down is untouched, which should hopefully mean that a large proportion of topic-list-customizing themes are unaffected. For more information, see https://meta.discourse.org/t/282816
This commit is contained in:
parent
fe769994d1
commit
82d6d691ee
|
@ -5,13 +5,9 @@
|
|||
@hideCategory={{this.hideCategory}}
|
||||
@topics={{this.topics}}
|
||||
@expandExcerpts={{this.expandExcerpts}}
|
||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
||||
@bulkSelectAction={{this.bulkSelectAction}}
|
||||
@bulkSelectHelper={{this.bulkSelectHelper}}
|
||||
@canBulkSelect={{this.canBulkSelect}}
|
||||
@selected={{this.selected}}
|
||||
@tagsForUser={{this.tagsForUser}}
|
||||
@toggleBulkSelect={{this.toggleBulkSelect}}
|
||||
@updateAutoAddTopicsToBulkSelect={{this.updateAutoAddTopicsToBulkSelect}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#unless this.loadingMore}}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import DButton from "discourse/components/d-button";
|
||||
|
||||
const BulkSelectToggle = <template>
|
||||
<DButton
|
||||
class="bulk-select"
|
||||
@action={{@bulkSelectHelper.toggleBulkSelect}}
|
||||
@icon="list"
|
||||
/>
|
||||
</template>;
|
||||
|
||||
export default BulkSelectToggle;
|
|
@ -1 +0,0 @@
|
|||
<DButton class="bulk-select" @action={{this.toggleBulkSelect}} @icon="list" />
|
|
@ -1,15 +0,0 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { getOwner } from "@ember/application";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
export default class BulkSelectToggle extends Component {
|
||||
@action
|
||||
toggleBulkSelect() {
|
||||
const controller = getOwner(this).lookup(
|
||||
`controller:${this.args.parentController}`
|
||||
);
|
||||
const helper = controller.bulkSelectHelper;
|
||||
helper.clear();
|
||||
helper.bulkSelectEnabled = !helper.bulkSelectEnabled;
|
||||
}
|
||||
}
|
|
@ -17,8 +17,8 @@
|
|||
{{/unless}}
|
||||
|
||||
<div class="navigation-controls">
|
||||
{{#if (and this.notCategoriesRoute this.site.mobileView this.canBulk)}}
|
||||
<BulkSelectToggle @parentController="discovery/topics" />
|
||||
{{#if (and this.notCategoriesRoute this.site.mobileView @canBulkSelect)}}
|
||||
<BulkSelectToggle @bulkSelectHelper={{@bulkSelectHelper}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showCategoryAdmin}}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { getOwner } from "@ember/application";
|
||||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { dependentKeyCompat } from "@ember/object/compat";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
|
@ -142,10 +142,23 @@ export default Component.extend({
|
|||
return filterType !== "categories";
|
||||
},
|
||||
|
||||
@discourseComputed()
|
||||
canBulk() {
|
||||
const controller = getOwner(this).lookup("controller:discovery/topics");
|
||||
return controller.canBulkSelect;
|
||||
@action
|
||||
async changeTagNotificationLevel(notificationLevel) {
|
||||
const response = await this.tagNotification.update({
|
||||
notification_level: notificationLevel,
|
||||
});
|
||||
|
||||
const payload = response.responseJson;
|
||||
|
||||
this.tagNotification.set("notification_level", notificationLevel);
|
||||
|
||||
this.currentUser.setProperties({
|
||||
watched_tags: payload.watched_tags,
|
||||
watching_first_post_tags: payload.watching_first_post_tags,
|
||||
tracked_tags: payload.tracked_tags,
|
||||
muted_tags: payload.muted_tags,
|
||||
regular_tags: payload.regular_tags,
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { readOnly } from "@ember/object/computed";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
export default Component.extend({
|
||||
hide: false,
|
||||
|
||||
banner: readOnly("site.banner"),
|
||||
|
||||
@discourseComputed("banner.html")
|
||||
content(bannerHtml) {
|
||||
const newDiv = document.createElement("div");
|
||||
|
@ -15,7 +18,7 @@ export default Component.extend({
|
|||
return newDiv.innerHTML;
|
||||
},
|
||||
|
||||
@discourseComputed("user.dismissed_banner_key", "banner.key", "hide")
|
||||
@discourseComputed("currentUser.dismissed_banner_key", "banner.key", "hide")
|
||||
visible(dismissedBannerKey, bannerKey, hide) {
|
||||
dismissedBannerKey =
|
||||
dismissedBannerKey || this.keyValueStore.get("dismissed_banner_key");
|
||||
|
@ -32,8 +35,8 @@ export default Component.extend({
|
|||
|
||||
@action
|
||||
dismiss() {
|
||||
if (this.user) {
|
||||
this.user.dismissBanner(this.get("banner.key"));
|
||||
if (this.currentUser) {
|
||||
this.currentUser.dismissBanner(this.get("banner.key"));
|
||||
} else {
|
||||
this.set("hide", true);
|
||||
this.keyValueStore.set({
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import UrlRefresh from "discourse/mixins/url-refresh";
|
||||
|
||||
const CATEGORIES_LIST_BODY_CLASS = "categories-list";
|
||||
|
||||
export default Component.extend(UrlRefresh, {
|
||||
classNames: ["contents"],
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
document.body.classList.add(CATEGORIES_LIST_BODY_CLASS);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
document.body.classList.remove(CATEGORIES_LIST_BODY_CLASS);
|
||||
},
|
||||
});
|
|
@ -2,10 +2,9 @@ import Component from "@ember/component";
|
|||
import { inject as service } from "@ember/service";
|
||||
import $ from "jquery";
|
||||
import LoadMore from "discourse/mixins/load-more";
|
||||
import UrlRefresh from "discourse/mixins/url-refresh";
|
||||
import { observes, on } from "discourse-common/utils/decorators";
|
||||
|
||||
export default Component.extend(UrlRefresh, LoadMore, {
|
||||
export default Component.extend(LoadMore, {
|
||||
classNames: ["contents"],
|
||||
eyelineSelector: ".topic-list-item",
|
||||
documentTitle: service(),
|
||||
|
@ -40,17 +39,13 @@ export default Component.extend(UrlRefresh, LoadMore, {
|
|||
if (
|
||||
newTopics &&
|
||||
newTopics.length &&
|
||||
this.autoAddTopicsToBulkSelect &&
|
||||
this.bulkSelectEnabled
|
||||
this.bulkSelectHelper?.bulkSelectEnabled
|
||||
) {
|
||||
this.addTopicsToBulkSelect(newTopics);
|
||||
this.bulkSelectHelper.addTopics(newTopics);
|
||||
}
|
||||
if (moreTopicsUrl && $(window).height() >= $(document).height()) {
|
||||
this.send("loadMore");
|
||||
}
|
||||
if (this.loadingComplete) {
|
||||
this.loadingComplete();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import CategoriesAndLatestTopics from "discourse/components/categories-and-latest-topics";
|
||||
import CategoriesBoxes from "discourse/components/categories-boxes";
|
||||
import CategoriesBoxesWithTopics from "discourse/components/categories-boxes-with-topics";
|
||||
import CategoriesOnly from "discourse/components/categories-only";
|
||||
import CategoriesWithFeaturedTopics from "discourse/components/categories-with-featured-topics";
|
||||
import SubcategoriesWithFeaturedTopics from "discourse/components/subcategories-with-featured-topics";
|
||||
|
||||
const mobileCompatibleViews = [
|
||||
"categories_with_featured_topics",
|
||||
"subcategories_with_featured_topics",
|
||||
];
|
||||
|
||||
const subcategoryComponents = {
|
||||
boxes_with_featured_topics: CategoriesBoxesWithTopics,
|
||||
boxes: CategoriesBoxes,
|
||||
rows_with_featured_topics: CategoriesWithFeaturedTopics,
|
||||
rows: CategoriesOnly,
|
||||
};
|
||||
|
||||
const globalComponents = {
|
||||
categories_and_latest_topics_created_date: CategoriesAndLatestTopics,
|
||||
categories_and_latest_topics: CategoriesAndLatestTopics,
|
||||
categories_boxes_with_topics: CategoriesBoxesWithTopics,
|
||||
categories_boxes: CategoriesBoxes,
|
||||
categories_only: CategoriesOnly,
|
||||
categories_with_featured_topics: CategoriesWithFeaturedTopics,
|
||||
subcategories_with_featured_topics: SubcategoriesWithFeaturedTopics,
|
||||
};
|
||||
|
||||
export default class CategoriesDisplay extends Component {
|
||||
@service siteSettings;
|
||||
@service site;
|
||||
|
||||
get #componentForSubcategories() {
|
||||
const parentCategory = this.args.parentCategory;
|
||||
const style = parentCategory.subcategory_list_style;
|
||||
const component = subcategoryComponents[style];
|
||||
|
||||
if (!component) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Unknown subcategory list style: " + style);
|
||||
return CategoriesOnly;
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
get #globalComponent() {
|
||||
let style = this.siteSettings.desktop_category_page_style;
|
||||
if (this.site.mobileView && !mobileCompatibleViews.includes(style)) {
|
||||
style = mobileCompatibleViews[0];
|
||||
}
|
||||
const component = globalComponents[style];
|
||||
|
||||
if (!component) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Unknown category list style: " + style);
|
||||
return CategoriesOnly;
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
get categoriesComponent() {
|
||||
if (this.args.parentCategory) {
|
||||
return this.#componentForSubcategories;
|
||||
} else {
|
||||
return this.#globalComponent;
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<this.categoriesComponent @categories={{@categories}} @topics={{@topics}} />
|
||||
</template>
|
||||
}
|
|
@ -7,10 +7,7 @@
|
|||
<Input
|
||||
class="topic-query-filter__filter-term"
|
||||
@value={{this.newQueryString}}
|
||||
@enter={{action
|
||||
this.discoveryFilter.updateTopicsListQueryParams
|
||||
this.newQueryString
|
||||
}}
|
||||
@enter={{action @updateTopicsListQueryParams this.newQueryString}}
|
||||
@type="text"
|
||||
id="queryStringInput"
|
||||
autocomplete="off"
|
|
@ -1,15 +1,14 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { resettableTracked } from "discourse/lib/tracked-tools";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
export default class NavigationFilterController extends Controller {
|
||||
@controller("discovery/filter") discoveryFilter;
|
||||
|
||||
export default class DiscoveryFilterNavigation extends Component {
|
||||
@tracked copyIcon = "link";
|
||||
@tracked copyClass = "btn-default";
|
||||
@tracked newQueryString = "";
|
||||
@resettableTracked newQueryString = this.args.queryString;
|
||||
|
||||
@bind
|
||||
updateQueryString(string) {
|
||||
|
@ -19,7 +18,7 @@ export default class NavigationFilterController extends Controller {
|
|||
@action
|
||||
clearInput() {
|
||||
this.newQueryString = "";
|
||||
this.discoveryFilter.updateTopicsListQueryParams(this.newQueryString);
|
||||
this.args.updateTopicsListQueryParams(this.newQueryString);
|
||||
}
|
||||
|
||||
@action
|
|
@ -1,11 +1,11 @@
|
|||
<div class="container">
|
||||
<DiscourseBanner @user={{this.currentUser}} @banner={{this.site.banner}} />
|
||||
{{#unless this.viewingCategoriesList}}
|
||||
<DiscourseBanner />
|
||||
{{#if @category}}
|
||||
<CategoryReadOnlyBanner
|
||||
@category={{this.category}}
|
||||
@readOnly={{not this.navigationCategory.enableCreateTopicButton}}
|
||||
@category={{@category}}
|
||||
@readOnly={{@createTopicDisabled}}
|
||||
/>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<span>
|
||||
|
@ -18,28 +18,22 @@
|
|||
@connectorTagName="div"
|
||||
/>
|
||||
<div class="container">
|
||||
{{outlet "navigation-bar"}}
|
||||
{{yield to="navigation"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{this.showLoadingSpinner}} />
|
||||
|
||||
{{#if this.loading}}
|
||||
{{hide-application-footer}}
|
||||
{{/if}}
|
||||
|
||||
<span>
|
||||
<PluginOutlet @name="discovery-above" @connectorTagName="div" />
|
||||
</span>
|
||||
|
||||
<div class="container list-container {{if this.showLoadingSpinner 'hidden'}}">
|
||||
<div class="container list-container">
|
||||
<div class="row">
|
||||
<div class="full-width">
|
||||
<div id="header-list-area">
|
||||
{{outlet "header-list-container"}}
|
||||
{{yield to="header"}}
|
||||
<PluginOutlet
|
||||
@name="header-list-container-bottom"
|
||||
@outletArgs={{hash category=this.category}}
|
||||
@outletArgs={{hash category=@category}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -51,9 +45,9 @@
|
|||
<PluginOutlet
|
||||
@name="discovery-list-container-top"
|
||||
@connectorTagName="span"
|
||||
@outletArgs={{hash category=this.category listLoading=this.loading}}
|
||||
@outletArgs={{hash category=@category}}
|
||||
/>
|
||||
{{outlet "list-container"}}
|
||||
{{yield to="list"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,72 @@
|
|||
<AddCategoryTagClasses @category={{@category}} @tags={{array @tag.id}} />
|
||||
|
||||
{{#if @category}}
|
||||
<PluginOutlet
|
||||
@name="above-category-heading"
|
||||
@outletArgs={{hash category=@category}}
|
||||
/>
|
||||
|
||||
<section class="category-heading">
|
||||
{{#if @category.uploaded_logo.url}}
|
||||
<CategoryLogo @category={{@category}} />
|
||||
{{#if @category.description}}
|
||||
<p>{{dir-span @category.description htmlSafe="true"}}</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<span>
|
||||
<PluginOutlet
|
||||
@name="category-heading"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash category=@category}}
|
||||
/>
|
||||
</span>
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
{{body-class this.bodyClass}}
|
||||
|
||||
<section
|
||||
class={{concat-class
|
||||
"navigation-container"
|
||||
(if @category "category-navigation")
|
||||
}}
|
||||
>
|
||||
<DNavigation
|
||||
@category={{@category}}
|
||||
@tag={{@tag}}
|
||||
@additionalTags={{@additionalTags}}
|
||||
@filterMode={{this.filterMode}}
|
||||
@noSubcategories={{@noSubcategories}}
|
||||
@canCreateTopic={{this.canCreateTopic}}
|
||||
@canCreateTopicOnTag={{@canCreateTopicOnTag}}
|
||||
@createTopic={{@createTopic}}
|
||||
@createTopicDisabled={{@createTopicDisabled}}
|
||||
@hasDraft={{this.currentUser.has_topic_draft}}
|
||||
@editCategory={{this.editCategory}}
|
||||
@showCategoryAdmin={{@showCategoryAdmin}}
|
||||
@createCategory={{this.createCategory}}
|
||||
@reorderCategories={{this.reorderCategories}}
|
||||
@canBulkSelect={{@canBulkSelect}}
|
||||
@bulkSelectHelper={{@bulkSelectHelper}}
|
||||
@skipCategoriesNavItem={{this.skipCategoriesNavItem}}
|
||||
@toggleInfo={{@toggleTagInfo}}
|
||||
@tagNotification={{@tagNotification}}
|
||||
/>
|
||||
|
||||
{{#if @category}}
|
||||
<PluginOutlet
|
||||
@name="category-navigation"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash category=@category tag=@tag}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if @tag}}
|
||||
<PluginOutlet
|
||||
@name="tag-navigation"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash category=@category tag=@tag}}
|
||||
/>
|
||||
{{/if}}
|
||||
</section>
|
|
@ -0,0 +1,61 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { calculateFilterMode } from "discourse/lib/filter-mode";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { TRACKED_QUERY_PARAM_VALUE } from "discourse/lib/topic-list-tracked-filter";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import Category from "discourse/models/category";
|
||||
|
||||
export default class DiscoveryNavigation extends Component {
|
||||
@service router;
|
||||
@service currentUser;
|
||||
|
||||
get filterMode() {
|
||||
return calculateFilterMode({
|
||||
category: this.args.category,
|
||||
filterType: this.args.filterType,
|
||||
noSubcategories: this.args.noSubcategories,
|
||||
});
|
||||
}
|
||||
|
||||
get skipCategoriesNavItem() {
|
||||
return this.router.currentRoute.queryParams.f === TRACKED_QUERY_PARAM_VALUE;
|
||||
}
|
||||
|
||||
get canCreateTopic() {
|
||||
return this.currentUser?.can_create_topic;
|
||||
}
|
||||
|
||||
get bodyClass() {
|
||||
if (this.args.tag) {
|
||||
return [
|
||||
"tags-page",
|
||||
this.args.additionalTags ? "tags-intersection" : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
} else if (this.filterMode === "categories") {
|
||||
return "navigation-categories";
|
||||
} else if (this.args.category) {
|
||||
return "navigation-category";
|
||||
} else {
|
||||
return "navigation-topics";
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
editCategory() {
|
||||
DiscourseURL.routeTo(`/c/${Category.slugFor(this.args.category)}/edit`);
|
||||
}
|
||||
|
||||
@action
|
||||
createCategory() {
|
||||
this.router.transitionTo("newCategory");
|
||||
}
|
||||
|
||||
@action
|
||||
reorderCategories() {
|
||||
showModal("reorder-categories");
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
{{#if (or this.loading this.model.canLoadMore)}}
|
||||
{{#if @model.canLoadMore}}
|
||||
{{hide-application-footer}}
|
||||
{{/if}}
|
||||
|
||||
|
@ -8,39 +8,36 @@
|
|||
|
||||
<TopicDismissButtons
|
||||
@position="top"
|
||||
@selectedTopics={{this.selected}}
|
||||
@model={{this.model}}
|
||||
@showResetNew={{this.showResetNew}}
|
||||
@showDismissRead={{this.showDismissRead}}
|
||||
@resetNew={{action "resetNew"}}
|
||||
@selectedTopics={{@bulkSelectHelper.selected}}
|
||||
@model={{@model}}
|
||||
@showResetNew={{@showResetNew}}
|
||||
@showDismissRead={{@showDismissRead}}
|
||||
@resetNew={{this.resetNew}}
|
||||
@dismissRead={{this.dismissRead}}
|
||||
/>
|
||||
|
||||
{{#if this.model.sharedDrafts}}
|
||||
{{#if @model.sharedDrafts}}
|
||||
<TopicList
|
||||
@class="shared-drafts"
|
||||
@listTitle="shared_drafts.title"
|
||||
@top={{this.top}}
|
||||
@hideCategory="true"
|
||||
@category={{this.category}}
|
||||
@topics={{this.model.sharedDrafts}}
|
||||
@category={{@category}}
|
||||
@topics={{@model.sharedDrafts}}
|
||||
@discoveryList={{true}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<DiscoveryTopicsList
|
||||
@model={{this.model}}
|
||||
@refresh={{action "refresh"}}
|
||||
@loadingComplete={{action "loadingComplete"}}
|
||||
@model={{@model}}
|
||||
@incomingCount={{this.topicTrackingState.incomingCount}}
|
||||
@autoAddTopicsToBulkSelect={{this.autoAddTopicsToBulkSelect}}
|
||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
||||
@addTopicsToBulkSelect={{action "addTopicsToBulkSelect"}}
|
||||
@bulkSelectHelper={{@bulkSelectHelper}}
|
||||
>
|
||||
{{#if this.top}}
|
||||
<div class="top-lists">
|
||||
<PeriodChooser
|
||||
@period={{this.period}}
|
||||
@action={{action "changePeriod"}}
|
||||
@period={{@period}}
|
||||
@action={{@changePeriod}}
|
||||
@fullDay={{false}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -65,10 +62,10 @@
|
|||
|
||||
{{#if this.renderNewListHeaderControls}}
|
||||
<NewListHeaderControlsWrapper
|
||||
@current={{this.model.params.subset}}
|
||||
@current={{@model.params.subset}}
|
||||
@newRepliesCount={{this.newRepliesCount}}
|
||||
@newTopicsCount={{this.newTopicsCount}}
|
||||
@changeNewListSubset={{route-action "changeNewListSubset"}}
|
||||
@changeNewListSubset={{@changeNewListSubset}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
|
@ -76,7 +73,7 @@
|
|||
<PluginOutlet
|
||||
@name="before-topic-list"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash category=this.category}}
|
||||
@outletArgs={{hash category=@category tag=@tag}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
|
@ -86,27 +83,21 @@
|
|||
@top={{this.top}}
|
||||
@showTopicPostBadges={{this.showTopicPostBadges}}
|
||||
@showPosters={{true}}
|
||||
@canBulkSelect={{this.canBulkSelect}}
|
||||
@changeSort={{route-action "changeSort"}}
|
||||
@toggleBulkSelect={{action "toggleBulkSelect"}}
|
||||
@updateAutoAddTopicsToBulkSelect={{action
|
||||
"updateAutoAddTopicsToBulkSelect"
|
||||
}}
|
||||
@hideCategory={{this.model.hideCategory}}
|
||||
@canBulkSelect={{@canBulkSelect}}
|
||||
@bulkSelectHelper={{@bulkSelectHelper}}
|
||||
@changeSort={{@changeSort}}
|
||||
@hideCategory={{@model.hideCategory}}
|
||||
@order={{this.order}}
|
||||
@ascending={{this.ascending}}
|
||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
||||
@bulkSelectAction={{action "refresh"}}
|
||||
@selected={{this.selected}}
|
||||
@expandGloballyPinned={{this.expandGloballyPinned}}
|
||||
@expandAllPinned={{this.expandAllPinned}}
|
||||
@category={{this.category}}
|
||||
@topics={{this.model.topics}}
|
||||
@category={{@category}}
|
||||
@topics={{@model.topics}}
|
||||
@discoveryList={{true}}
|
||||
@focusLastVisitedTopic={{true}}
|
||||
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
|
||||
@newListSubset={{this.model.params.subset}}
|
||||
@changeNewListSubset={{route-action "changeNewListSubset"}}
|
||||
@newListSubset={{@model.params.subset}}
|
||||
@changeNewListSubset={{@changeNewListSubset}}
|
||||
@newRepliesCount={{this.newRepliesCount}}
|
||||
@newTopicsCount={{this.newTopicsCount}}
|
||||
/>
|
||||
|
@ -115,33 +106,38 @@
|
|||
<PluginOutlet
|
||||
@name="after-topic-list"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash category=this.category}}
|
||||
@outletArgs={{hash category=@category tag=@tag}}
|
||||
/>
|
||||
</span>
|
||||
</DiscoveryTopicsList>
|
||||
|
||||
<footer class="topic-list-bottom">
|
||||
<ConditionalLoadingSpinner @condition={{this.model.loadingMore}} />
|
||||
<ConditionalLoadingSpinner @condition={{@model.loadingMore}} />
|
||||
{{#if this.allLoaded}}
|
||||
<TopicDismissButtons
|
||||
@position="bottom"
|
||||
@selectedTopics={{this.selected}}
|
||||
@model={{this.model}}
|
||||
@showResetNew={{this.showResetNew}}
|
||||
@showDismissRead={{this.showDismissRead}}
|
||||
@resetNew={{action "resetNew"}}
|
||||
@selectedTopics={{@bulkSelectHelper.selected}}
|
||||
@model={{@model}}
|
||||
@showResetNew={{@showResetNew}}
|
||||
@showDismissRead={{@showDismissRead}}
|
||||
@resetNew={{this.resetNew}}
|
||||
@dismissRead={{this.dismissRead}}
|
||||
/>
|
||||
|
||||
<FooterMessage
|
||||
@education={{this.footerEducation}}
|
||||
@message={{this.footerMessage}}
|
||||
>
|
||||
{{#if this.latest}}
|
||||
{{#if this.category.canCreateTopic}}
|
||||
{{#if @tag}}
|
||||
{{html-safe
|
||||
(i18n "topic.browse_all_tags_or_latest" basePath=(base-path))
|
||||
}}
|
||||
{{else if this.latest}}
|
||||
{{#if @category.canCreateTopic}}
|
||||
<DiscourseLinkedText
|
||||
@action={{fn
|
||||
this.composer.openNewTopic
|
||||
(hash category=this.category preferDraft=true)
|
||||
(hash category=@category preferDraft=true)
|
||||
}}
|
||||
@text="topic.suggest_create_topic"
|
||||
/>
|
||||
|
@ -152,10 +148,7 @@
|
|||
"topic.browse_all_categories_latest_or_top" basePath=(base-path)
|
||||
)
|
||||
}}
|
||||
<TopPeriodButtons
|
||||
@period={{this.period}}
|
||||
@action={{action "changePeriod"}}
|
||||
/>
|
||||
<TopPeriodButtons @period={{@period}} @action={{@changePeriod}} />
|
||||
{{else}}
|
||||
{{html-safe
|
||||
(i18n "topic.browse_all_categories_latest" basePath=(base-path))
|
|
@ -0,0 +1,223 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DismissNew from "discourse/components/modal/dismiss-new";
|
||||
import { filterTypeForMode } from "discourse/lib/filter-mode";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import Topic from "discourse/models/topic";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class DiscoveryTopics extends Component {
|
||||
@service router;
|
||||
@service composer;
|
||||
@service modal;
|
||||
@service currentUser;
|
||||
@service topicTrackingState;
|
||||
@service site;
|
||||
|
||||
get redirectedReason() {
|
||||
return this.currentUser?.user_option.redirected_to_top?.reason;
|
||||
}
|
||||
|
||||
get order() {
|
||||
return this.args.model.get("params.order");
|
||||
}
|
||||
|
||||
get ascending() {
|
||||
return this.args.model.get("params.ascending");
|
||||
}
|
||||
|
||||
get hasTopics() {
|
||||
return this.args.model.get("topics.length") > 0;
|
||||
}
|
||||
|
||||
get allLoaded() {
|
||||
return !this.args.model.get("more_topics_url");
|
||||
}
|
||||
|
||||
get latest() {
|
||||
return filterTypeForMode(this.args.model.filter) === "latest";
|
||||
}
|
||||
|
||||
get top() {
|
||||
return filterTypeForMode(this.args.model.filter) === "top";
|
||||
}
|
||||
|
||||
get new() {
|
||||
return filterTypeForMode(this.args.model.filter) === "new";
|
||||
}
|
||||
|
||||
async callResetNew(
|
||||
dismissPosts = false,
|
||||
dismissTopics = false,
|
||||
untrack = false
|
||||
) {
|
||||
const tracked =
|
||||
(this.router.currentRoute.queryParams["f"] ||
|
||||
this.router.currentRoute.queryParams["filter"]) === "tracked";
|
||||
|
||||
let topicIds = this.args.bulkSelectHelper.selected.map((topic) => topic.id);
|
||||
const result = await Topic.resetNew(
|
||||
this.args.category,
|
||||
!this.args.noSubcategories,
|
||||
{
|
||||
tracked,
|
||||
tag: this.args.tag,
|
||||
topicIds,
|
||||
dismissPosts,
|
||||
dismissTopics,
|
||||
untrack,
|
||||
}
|
||||
);
|
||||
|
||||
if (result.topic_ids) {
|
||||
this.topicTrackingState.removeTopics(result.topic_ids);
|
||||
}
|
||||
this.router.refresh();
|
||||
}
|
||||
|
||||
@action
|
||||
resetNew() {
|
||||
if (!this.currentUser.new_new_view_enabled) {
|
||||
return this.callResetNew();
|
||||
}
|
||||
|
||||
this.modal.show(DismissNew, {
|
||||
model: {
|
||||
selectedTopics: this.args.bulkSelectHelper.selected,
|
||||
subset: this.args.model.listParams?.subset,
|
||||
dismissCallback: ({ dismissPosts, dismissTopics, untrack }) => {
|
||||
this.callResetNew(dismissPosts, dismissTopics, untrack);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Show newly inserted topics
|
||||
@action
|
||||
showInserted(event) {
|
||||
event?.preventDefault();
|
||||
const tracker = this.topicTrackingState;
|
||||
|
||||
// Move inserted into topics
|
||||
this.args.model.loadBefore(tracker.get("newIncoming"), true);
|
||||
tracker.resetTracking();
|
||||
}
|
||||
|
||||
get showTopicsAndRepliesToggle() {
|
||||
return this.new && this.currentUser?.new_new_view_enabled;
|
||||
}
|
||||
|
||||
get newRepliesCount() {
|
||||
this.topicTrackingState.get("messageCount"); // Autotrack this
|
||||
|
||||
if (this.currentUser?.new_new_view_enabled) {
|
||||
return this.topicTrackingState.countUnread({
|
||||
categoryId: this.args.category?.id,
|
||||
noSubcategories: this.args.noSubcategories,
|
||||
tagId: this.args.tag?.id,
|
||||
});
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
get newTopicsCount() {
|
||||
this.topicTrackingState.get("messageCount"); // Autotrack this
|
||||
|
||||
if (this.currentUser?.new_new_view_enabled) {
|
||||
return this.topicTrackingState.countNew({
|
||||
categoryId: this.args.category?.id,
|
||||
noSubcategories: this.args.noSubcategories,
|
||||
tagId: this.args.tag?.id,
|
||||
});
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
get showTopicPostBadges() {
|
||||
return !this.new || this.currentUser?.new_new_view_enabled;
|
||||
}
|
||||
|
||||
get footerMessage() {
|
||||
const topicsLength = this.args.model.get("topics.length");
|
||||
if (!this.allLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { category, tag } = this.args;
|
||||
if (category) {
|
||||
return I18n.t("topics.bottom.category", {
|
||||
category: category.get("name"),
|
||||
});
|
||||
} else if (tag) {
|
||||
return I18n.t("topics.bottom.tag", {
|
||||
tag: tag.id,
|
||||
});
|
||||
} else {
|
||||
const split = (this.args.model.get("filter") || "").split("/");
|
||||
if (topicsLength === 0) {
|
||||
return I18n.t("topics.none." + split[0], {
|
||||
category: split[1],
|
||||
});
|
||||
} else {
|
||||
return I18n.t("topics.bottom." + split[0], {
|
||||
category: split[1],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get footerEducation() {
|
||||
const topicsLength = this.args.model.get("topics.length");
|
||||
|
||||
if (!this.allLoaded || topicsLength > 0 || !this.currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const segments = (this.args.model.get("filter") || "").split("/");
|
||||
|
||||
let tab = segments[segments.length - 1];
|
||||
|
||||
if (tab !== "new" && tab !== "unread") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab === "new" && this.currentUser.new_new_view_enabled) {
|
||||
tab = "new_new";
|
||||
}
|
||||
|
||||
return I18n.t("topics.none.educate." + tab, {
|
||||
userPrefsUrl: userPath(
|
||||
`${this.currentUser.get("username_lower")}/preferences/tracking`
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
get renderNewListHeaderControls() {
|
||||
return (
|
||||
this.site.mobileView &&
|
||||
this.showTopicsAndRepliesToggle &&
|
||||
!this.args.bulkSelectEnabled
|
||||
);
|
||||
}
|
||||
|
||||
get expandAllGloballyPinned() {
|
||||
return !this.expandAllPinned;
|
||||
}
|
||||
|
||||
get expandAllPinned() {
|
||||
return this.args.tag || this.args.category;
|
||||
}
|
||||
|
||||
@action
|
||||
dismissRead(dismissTopics) {
|
||||
const operationType = dismissTopics ? "topics" : "posts";
|
||||
this.args.bulkSelectHelper.dismissRead(operationType, {
|
||||
categoryId: this.args.category?.id,
|
||||
tagName: this.args.tag?.id,
|
||||
includeSubcategories: this.args.noSubcategories,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@
|
|||
<:footer>
|
||||
<DButton
|
||||
class="btn-primary"
|
||||
@action={{route-action "dismissReadTopics" this.dismissTopics}}
|
||||
@action={{fn @model.dismissRead this.dismissTopics}}
|
||||
@icon="check"
|
||||
id="dismiss-read-confirm"
|
||||
@label="topics.bulk.dismiss"
|
||||
|
|
|
@ -146,7 +146,7 @@
|
|||
id="edit-synonyms"
|
||||
class="btn-default"
|
||||
/>
|
||||
{{#if this.deleteAction}}
|
||||
{{#if this.canAdminTag}}
|
||||
<DButton
|
||||
@action={{action "deleteTag"}}
|
||||
@icon="far-trash-alt"
|
||||
|
|
|
@ -140,8 +140,35 @@ export default Component.extend({
|
|||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
@action
|
||||
deleteTag() {
|
||||
this.deleteAction(this.tagInfo);
|
||||
const numTopics =
|
||||
this.get("list.topic_list.tags.firstObject.topic_count") || 0;
|
||||
|
||||
let confirmText =
|
||||
numTopics === 0
|
||||
? I18n.t("tagging.delete_confirm_no_topics")
|
||||
: I18n.t("tagging.delete_confirm", { count: numTopics });
|
||||
|
||||
if (this.tagInfo.synonyms.length > 0) {
|
||||
confirmText +=
|
||||
" " +
|
||||
I18n.t("tagging.delete_confirm_synonyms", {
|
||||
count: this.tagInfo.synonyms.length,
|
||||
});
|
||||
}
|
||||
|
||||
this.dialog.deleteConfirm({
|
||||
message: confirmText,
|
||||
didConfirm: async () => {
|
||||
try {
|
||||
await this.tag.destroyRecord();
|
||||
this.router.transitionTo("tags.index");
|
||||
} catch {
|
||||
this.dialog.alert(I18n.t("generic_error"));
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
addSynonyms() {
|
||||
|
|
|
@ -72,6 +72,7 @@ export default Component.extend({
|
|||
model: {
|
||||
title: dismissTitle,
|
||||
count: this.selectedTopics.length,
|
||||
dismissRead: this.dismissRead,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Component from "@ember/component";
|
||||
import { alias, and } from "@ember/object/computed";
|
||||
import { dependentKeyCompat } from "@ember/object/compat";
|
||||
import { alias } from "@ember/object/computed";
|
||||
import { on } from "@ember/object/evented";
|
||||
import { inject as service } from "@ember/service";
|
||||
import LoadMore from "discourse/mixins/load-more";
|
||||
|
@ -8,13 +9,19 @@ import TopicBulkActions from "./modal/topic-bulk-actions";
|
|||
|
||||
export default Component.extend(LoadMore, {
|
||||
modal: service(),
|
||||
router: service(),
|
||||
|
||||
tagName: "table",
|
||||
classNames: ["topic-list"],
|
||||
classNameBindings: ["bulkSelectEnabled:sticky-header"],
|
||||
showTopicPostBadges: true,
|
||||
listTitle: "topic.title",
|
||||
canDoBulkActions: and("currentUser.canManageTopic", "selected.length"),
|
||||
|
||||
get canDoBulkActions() {
|
||||
return (
|
||||
this.currentUser?.canManageTopic && this.bulkSelectHelper?.selected.length
|
||||
);
|
||||
},
|
||||
|
||||
// Overwrite this to perform client side filtering of topics, if desired
|
||||
filteredTopics: alias("topics"),
|
||||
|
@ -26,9 +33,17 @@ export default Component.extend(LoadMore, {
|
|||
this.refreshLastVisited();
|
||||
}),
|
||||
|
||||
@discourseComputed("bulkSelectEnabled")
|
||||
toggleInTitle(bulkSelectEnabled) {
|
||||
return !bulkSelectEnabled && this.canBulkSelect;
|
||||
get selected() {
|
||||
return this.bulkSelectHelper?.selected;
|
||||
},
|
||||
|
||||
@dependentKeyCompat // for the classNameBindings
|
||||
get bulkSelectEnabled() {
|
||||
return this.bulkSelectHelper?.bulkSelectEnabled;
|
||||
},
|
||||
|
||||
get toggleInTitle() {
|
||||
return !this.bulkSelectHelper?.bulkSelectEnabled && this.canBulkSelect;
|
||||
},
|
||||
|
||||
@discourseComputed
|
||||
|
@ -138,10 +153,6 @@ export default Component.extend(LoadMore, {
|
|||
);
|
||||
},
|
||||
|
||||
updateAutoAddTopicsToBulkSelect(newVal) {
|
||||
this.set("autoAddTopicsToBulkSelect", newVal);
|
||||
},
|
||||
|
||||
click(e) {
|
||||
const onClick = (sel, callback) => {
|
||||
let target = e.target.closest(sel);
|
||||
|
@ -152,19 +163,19 @@ export default Component.extend(LoadMore, {
|
|||
};
|
||||
|
||||
onClick("button.bulk-select", () => {
|
||||
this.toggleBulkSelect();
|
||||
this.bulkSelectHelper.toggleBulkSelect();
|
||||
this.rerender();
|
||||
});
|
||||
|
||||
onClick("button.bulk-select-all", () => {
|
||||
this.updateAutoAddTopicsToBulkSelect(true);
|
||||
this.bulkSelectHelper.autoAddTopicsToBulkSelect = true;
|
||||
document
|
||||
.querySelectorAll("input.bulk-select:not(:checked)")
|
||||
.forEach((el) => el.click());
|
||||
});
|
||||
|
||||
onClick("button.bulk-clear-all", () => {
|
||||
this.updateAutoAddTopicsToBulkSelect(false);
|
||||
this.bulkSelectHelper.autoAddTopicsToBulkSelect = false;
|
||||
document
|
||||
.querySelectorAll("input.bulk-select:checked")
|
||||
.forEach((el) => el.click());
|
||||
|
@ -178,9 +189,9 @@ export default Component.extend(LoadMore, {
|
|||
onClick("button.bulk-select-actions", () => {
|
||||
this.modal.show(TopicBulkActions, {
|
||||
model: {
|
||||
topics: this.selected,
|
||||
topics: this.bulkSelectHelper.selected,
|
||||
category: this.category,
|
||||
refreshClosure: this.bulkSelectAction,
|
||||
refreshClosure: () => this.router.refresh(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
import Controller, { inject as controller } from "@ember/controller";
|
||||
|
||||
// Just add query params here to have them automatically passed to topic list filters.
|
||||
export const queryParams = {
|
||||
order: { replace: true, refreshModel: true },
|
||||
ascending: { replace: true, refreshModel: true, default: false },
|
||||
status: { replace: true, refreshModel: true },
|
||||
state: { replace: true, refreshModel: true },
|
||||
search: { replace: true, refreshModel: true },
|
||||
max_posts: { replace: true, refreshModel: true },
|
||||
min_posts: { replace: true, refreshModel: true },
|
||||
q: { replace: true, refreshModel: true },
|
||||
before: { replace: true, refreshModel: true },
|
||||
bumped_before: { replace: true, refreshModel: true },
|
||||
f: { replace: true, refreshModel: true },
|
||||
subset: { replace: true, refreshModel: true },
|
||||
period: { replace: true, refreshModel: true },
|
||||
topic_ids: { replace: true, refreshModel: true },
|
||||
group_name: { replace: true, refreshModel: true },
|
||||
tags: { replace: true, refreshModel: true },
|
||||
match_all_tags: { replace: true, refreshModel: true },
|
||||
no_subcategories: { replace: true, refreshModel: true },
|
||||
no_tags: { replace: true, refreshModel: true },
|
||||
exclude_tag: { replace: true, refreshModel: true },
|
||||
};
|
||||
|
||||
export function changeSort(sortBy) {
|
||||
let model = this.controllerFor("discovery.topics").model;
|
||||
|
||||
if (sortBy === this.controller.order) {
|
||||
this.controller.toggleProperty("ascending");
|
||||
model.updateSortParams(sortBy, this.controller.ascending);
|
||||
} else {
|
||||
this.controller.setProperties({ order: sortBy, ascending: false });
|
||||
model.updateSortParams(sortBy, false);
|
||||
}
|
||||
}
|
||||
|
||||
export function changeNewListSubset(subset) {
|
||||
this.controller.set("subset", subset);
|
||||
|
||||
let model = this.controllerFor("discovery.topics").model;
|
||||
model.updateNewListSubsetParam(subset);
|
||||
}
|
||||
|
||||
export function resetParams(skipParams = []) {
|
||||
Object.keys(queryParams).forEach((p) => {
|
||||
if (!skipParams.includes(p)) {
|
||||
this.controller.set(p, queryParams[p].default);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function addDiscoveryQueryParam(p, opts) {
|
||||
queryParams[p] = opts;
|
||||
}
|
||||
|
||||
export default class DiscoverySortableController extends Controller {
|
||||
@controller("discovery/topics") discoveryTopics;
|
||||
|
||||
queryParams = Object.keys(queryParams);
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.queryParams.forEach((p) => {
|
||||
this[p] = queryParams[p].default;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { alias, equal } from "@ember/object/computed";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import Category from "discourse/models/category";
|
||||
|
||||
export default class DiscoveryController extends Controller {
|
||||
@service router;
|
||||
|
||||
@controller("navigation/category") navigationCategory;
|
||||
|
||||
@equal("router.currentRouteName", "discovery.categories")
|
||||
viewingCategoriesList;
|
||||
|
||||
@alias("navigationCategory.category") category;
|
||||
@alias("navigationCategory.noSubcategories") noSubcategories;
|
||||
|
||||
loading = false;
|
||||
|
||||
@action
|
||||
loadingBegan() {
|
||||
this.set("loading", true);
|
||||
}
|
||||
|
||||
@action
|
||||
loadingComplete() {
|
||||
this.set("loading", false);
|
||||
}
|
||||
|
||||
showMoreUrl(period) {
|
||||
let url = "",
|
||||
category = this.category;
|
||||
|
||||
if (category) {
|
||||
url = `/c/${Category.slugFor(category)}/${category.id}${
|
||||
this.noSubcategories ? "/none" : ""
|
||||
}/l`;
|
||||
}
|
||||
|
||||
url += "/top";
|
||||
|
||||
const urlSearchParams = new URLSearchParams();
|
||||
|
||||
for (const [key, value] of Object.entries(
|
||||
this.router.currentRoute.queryParams
|
||||
)) {
|
||||
if (typeof value !== "undefined") {
|
||||
urlSearchParams.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
urlSearchParams.set("period", period);
|
||||
|
||||
return `${url}?${urlSearchParams.toString()}`;
|
||||
}
|
||||
|
||||
get showLoadingSpinner() {
|
||||
return (
|
||||
this.get("loading") &&
|
||||
this.siteSettings.page_loading_indicator === "spinner"
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
changePeriod(p) {
|
||||
DiscourseURL.routeTo(this.showMoreUrl(p));
|
||||
}
|
||||
}
|
|
@ -1,27 +1,12 @@
|
|||
import { inject as controller } from "@ember/controller";
|
||||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { reads } from "@ember/object/computed";
|
||||
import { dasherize } from "@ember/string";
|
||||
import DiscoveryController from "discourse/controllers/discovery";
|
||||
import { inject as service } from "@ember/service";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
const subcategoryStyleComponentNames = {
|
||||
rows: "categories_only",
|
||||
rows_with_featured_topics: "categories_with_featured_topics",
|
||||
boxes: "categories_boxes",
|
||||
boxes_with_featured_topics: "categories_boxes_with_topics",
|
||||
};
|
||||
|
||||
const mobileCompatibleViews = [
|
||||
"categories_with_featured_topics",
|
||||
"subcategories_with_featured_topics",
|
||||
];
|
||||
|
||||
export default class CategoriesController extends DiscoveryController {
|
||||
@controller discovery;
|
||||
|
||||
// this makes sure the composer isn't scoping to a specific category
|
||||
category = null;
|
||||
export default class CategoriesController extends Controller {
|
||||
@service router;
|
||||
@service composer;
|
||||
|
||||
@reads("currentUser.staff") canEdit;
|
||||
|
||||
|
@ -30,30 +15,6 @@ export default class CategoriesController extends DiscoveryController {
|
|||
return this.router.currentRouteName === "discovery.categories";
|
||||
}
|
||||
|
||||
@discourseComputed("model.parentCategory")
|
||||
categoryPageStyle(parentCategory) {
|
||||
let style = this.siteSettings.desktop_category_page_style;
|
||||
|
||||
if (this.site.mobileView && !mobileCompatibleViews.includes(style)) {
|
||||
style = mobileCompatibleViews[0];
|
||||
}
|
||||
|
||||
if (parentCategory) {
|
||||
style =
|
||||
subcategoryStyleComponentNames[
|
||||
parentCategory.get("subcategory_list_style")
|
||||
] || style;
|
||||
}
|
||||
|
||||
const componentName =
|
||||
parentCategory &&
|
||||
(style === "categories_and_latest_topics" ||
|
||||
style === "categories_and_latest_topics_created_date")
|
||||
? "categories_only"
|
||||
: style;
|
||||
return dasherize(componentName);
|
||||
}
|
||||
|
||||
@action
|
||||
showInserted(event) {
|
||||
event?.preventDefault();
|
||||
|
@ -63,6 +24,13 @@ export default class CategoriesController extends DiscoveryController {
|
|||
tracker.resetTracking();
|
||||
}
|
||||
|
||||
@action
|
||||
createTopic() {
|
||||
this.composer.openNewTopic({
|
||||
preferDraft: true,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
refresh() {
|
||||
this.send("triggerRefresh");
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import BulkSelectHelper from "discourse/lib/bulk-select-helper";
|
||||
import { filterTypeForMode } from "discourse/lib/filter-mode";
|
||||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
import { defineTrackedProperty } from "discourse/lib/tracked-tools";
|
||||
|
||||
// Just add query params here to have them automatically passed to topic list filters.
|
||||
export const queryParams = {
|
||||
order: { replace: true, refreshModel: true },
|
||||
ascending: { replace: true, refreshModel: true, default: false },
|
||||
status: { replace: true, refreshModel: true },
|
||||
state: { replace: true, refreshModel: true },
|
||||
search: { replace: true, refreshModel: true },
|
||||
max_posts: { replace: true, refreshModel: true },
|
||||
min_posts: { replace: true, refreshModel: true },
|
||||
q: { replace: true, refreshModel: true },
|
||||
before: { replace: true, refreshModel: true },
|
||||
bumped_before: { replace: true, refreshModel: true },
|
||||
f: { replace: true, refreshModel: true },
|
||||
subset: { replace: true, refreshModel: true },
|
||||
period: { replace: true, refreshModel: true },
|
||||
topic_ids: { replace: true, refreshModel: true },
|
||||
group_name: { replace: true, refreshModel: true },
|
||||
tags: { replace: true, refreshModel: true },
|
||||
match_all_tags: { replace: true, refreshModel: true },
|
||||
no_subcategories: { replace: true, refreshModel: true },
|
||||
no_tags: { replace: true, refreshModel: true },
|
||||
exclude_tag: { replace: true, refreshModel: true },
|
||||
};
|
||||
|
||||
export function resetParams(skipParams = []) {
|
||||
for (const [param, value] of Object.entries(queryParams)) {
|
||||
if (!skipParams.includes(param)) {
|
||||
this.controller.set(param, value.default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function addDiscoveryQueryParam(p, opts) {
|
||||
queryParams[p] = opts;
|
||||
}
|
||||
|
||||
@disableImplicitInjections
|
||||
export default class DiscoveryListController extends Controller {
|
||||
@service composer;
|
||||
@service siteSettings;
|
||||
@service site;
|
||||
@service currentUser;
|
||||
|
||||
@tracked model;
|
||||
|
||||
queryParams = Object.keys(queryParams);
|
||||
|
||||
bulkSelectHelper = new BulkSelectHelper(this);
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
for (const [name, info] of Object.entries(queryParams)) {
|
||||
defineTrackedProperty(this, name, info.default);
|
||||
}
|
||||
}
|
||||
|
||||
get canBulkSelect() {
|
||||
return (
|
||||
this.currentUser?.canManageTopic ||
|
||||
this.showDismissRead ||
|
||||
this.showResetNew
|
||||
);
|
||||
}
|
||||
|
||||
get showDismissRead() {
|
||||
return (
|
||||
filterTypeForMode(this.model.list?.filter) === "unread" &&
|
||||
this.model.list.get("topics.length") > 0
|
||||
);
|
||||
}
|
||||
|
||||
get showResetNew() {
|
||||
return (
|
||||
filterTypeForMode(this.model.list?.filter) === "new" &&
|
||||
this.model.list?.get("topics.length") > 0
|
||||
);
|
||||
}
|
||||
|
||||
get createTopicTargetCategory() {
|
||||
const { category } = this.model;
|
||||
if (category?.canCreateTopic) {
|
||||
return category;
|
||||
}
|
||||
|
||||
if (this.siteSettings.default_subcategory_on_read_only_category) {
|
||||
return category?.subcategoryWithCreateTopicPermission;
|
||||
}
|
||||
}
|
||||
|
||||
get createTopicDisabled() {
|
||||
// We are in a category route, but user does not have permission for the category
|
||||
return this.model.category && !this.createTopicTargetCategory;
|
||||
}
|
||||
|
||||
@action
|
||||
createTopic() {
|
||||
this.composer.openNewTopic({
|
||||
category: this.createTopicTargetCategory,
|
||||
tags: [this.model.tag?.id, ...(this.model.additionalTags ?? [])]
|
||||
.filter(Boolean)
|
||||
.reject((t) => ["none", "all"].includes(t))
|
||||
.join(","),
|
||||
preferDraft: true,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
changePeriod(p) {
|
||||
this.period = p;
|
||||
}
|
||||
|
||||
@action
|
||||
changeSort(sortBy) {
|
||||
if (sortBy === this.order) {
|
||||
this.ascending = !this.ascending;
|
||||
this.model.list.updateSortParams(sortBy, this.ascending);
|
||||
} else {
|
||||
this.order = sortBy;
|
||||
this.ascending = false;
|
||||
this.model.list.updateSortParams(sortBy, false);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
changeNewListSubset(subset) {
|
||||
this.subset = subset;
|
||||
this.model.list.updateNewListSubsetParam(subset);
|
||||
}
|
||||
|
||||
@action
|
||||
toggleTagInfo() {
|
||||
this.toggleProperty("showTagInfo");
|
||||
}
|
||||
}
|
|
@ -1,247 +0,0 @@
|
|||
import { inject as controller } from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { alias, empty, equal, gt, or, readOnly } from "@ember/object/computed";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DiscoveryController from "discourse/controllers/discovery";
|
||||
import { routeAction } from "discourse/helpers/route-action";
|
||||
import BulkSelectHelper from "discourse/lib/bulk-select-helper";
|
||||
import { endWith } from "discourse/lib/computed";
|
||||
import { filterTypeForMode } from "discourse/lib/filter-mode";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import DismissTopics from "discourse/mixins/dismiss-topics";
|
||||
import Topic from "discourse/models/topic";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class TopicsController extends DiscoveryController.extend(
|
||||
DismissTopics
|
||||
) {
|
||||
@service router;
|
||||
@service composer;
|
||||
@controller discovery;
|
||||
|
||||
bulkSelectHelper = new BulkSelectHelper(this);
|
||||
|
||||
period = null;
|
||||
expandGloballyPinned = false;
|
||||
expandAllPinned = false;
|
||||
|
||||
@alias("currentUser.id") canStar;
|
||||
@alias("currentUser.user_option.redirected_to_top.reason") redirectedReason;
|
||||
@readOnly("model.params.order") order;
|
||||
@readOnly("model.params.ascending") ascending;
|
||||
@gt("model.topics.length", 0) hasTopics;
|
||||
@empty("model.more_topics_url") allLoaded;
|
||||
@endWith("model.filter", "latest") latest;
|
||||
@endWith("model.filter", "top") top;
|
||||
@equal("period", "yearly") yearly;
|
||||
@equal("period", "quarterly") quarterly;
|
||||
@equal("period", "monthly") monthly;
|
||||
@equal("period", "weekly") weekly;
|
||||
@equal("period", "daily") daily;
|
||||
|
||||
@or("currentUser.canManageTopic", "showDismissRead", "showResetNew")
|
||||
canBulkSelect;
|
||||
|
||||
get bulkSelectEnabled() {
|
||||
return this.bulkSelectHelper.bulkSelectEnabled;
|
||||
}
|
||||
|
||||
get selected() {
|
||||
return this.bulkSelectHelper.selected;
|
||||
}
|
||||
|
||||
@discourseComputed("model.filter", "model.topics.length")
|
||||
showDismissRead(filterMode, topicsLength) {
|
||||
return filterTypeForMode(filterMode) === "unread" && topicsLength > 0;
|
||||
}
|
||||
|
||||
@discourseComputed("model.filter", "model.topics.length")
|
||||
showResetNew(filterMode, topicsLength) {
|
||||
return filterTypeForMode(filterMode) === "new" && topicsLength > 0;
|
||||
}
|
||||
|
||||
callResetNew(dismissPosts = false, dismissTopics = false, untrack = false) {
|
||||
const tracked =
|
||||
(this.router.currentRoute.queryParams["f"] ||
|
||||
this.router.currentRoute.queryParams["filter"]) === "tracked";
|
||||
|
||||
let topicIds = this.selected
|
||||
? this.selected.map((topic) => topic.id)
|
||||
: null;
|
||||
|
||||
Topic.resetNew(this.category, !this.noSubcategories, {
|
||||
tracked,
|
||||
topicIds,
|
||||
dismissPosts,
|
||||
dismissTopics,
|
||||
untrack,
|
||||
}).then((result) => {
|
||||
if (result.topic_ids) {
|
||||
this.topicTrackingState.removeTopics(result.topic_ids);
|
||||
}
|
||||
this.send(
|
||||
"refresh",
|
||||
tracked ? { skipResettingParams: ["filter", "f"] } : {}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Show newly inserted topics
|
||||
@action
|
||||
showInserted(event) {
|
||||
event?.preventDefault();
|
||||
const tracker = this.topicTrackingState;
|
||||
|
||||
// Move inserted into topics
|
||||
this.model.loadBefore(tracker.get("newIncoming"), true);
|
||||
tracker.resetTracking();
|
||||
}
|
||||
|
||||
@action
|
||||
changeSort() {
|
||||
deprecated(
|
||||
"changeSort has been changed from an (action) to a (route-action)",
|
||||
{
|
||||
since: "2.6.0",
|
||||
dropFrom: "2.7.0",
|
||||
id: "discourse.topics.change-sort",
|
||||
}
|
||||
);
|
||||
return routeAction("changeSort", this.router._router, ...arguments)();
|
||||
}
|
||||
|
||||
@action
|
||||
refresh() {
|
||||
this.send("triggerRefresh");
|
||||
}
|
||||
|
||||
afterRefresh(filter, list, listModel = list) {
|
||||
this.setProperties({ model: listModel });
|
||||
this.resetSelected();
|
||||
|
||||
if (this.topicTrackingState) {
|
||||
this.topicTrackingState.sync(list, filter);
|
||||
}
|
||||
|
||||
this.send("loadingComplete");
|
||||
}
|
||||
|
||||
@discourseComputed("model.filter")
|
||||
new(filter) {
|
||||
return filter?.endsWith("new");
|
||||
}
|
||||
|
||||
@discourseComputed("new")
|
||||
showTopicsAndRepliesToggle(isNew) {
|
||||
return isNew && this.currentUser?.new_new_view_enabled;
|
||||
}
|
||||
|
||||
@discourseComputed("topicTrackingState.messageCount")
|
||||
newRepliesCount() {
|
||||
if (this.currentUser?.new_new_view_enabled) {
|
||||
return this.topicTrackingState.countUnread({
|
||||
categoryId: this.category?.id,
|
||||
noSubcategories: this.noSubcategories,
|
||||
});
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@discourseComputed("topicTrackingState.messageCount")
|
||||
newTopicsCount() {
|
||||
if (this.currentUser?.new_new_view_enabled) {
|
||||
return this.topicTrackingState.countNew({
|
||||
categoryId: this.category?.id,
|
||||
noSubcategories: this.noSubcategories,
|
||||
});
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@discourseComputed("new")
|
||||
showTopicPostBadges(isNew) {
|
||||
return !isNew || this.currentUser?.new_new_view_enabled;
|
||||
}
|
||||
|
||||
@discourseComputed("allLoaded", "model.topics.length")
|
||||
footerMessage(allLoaded, topicsLength) {
|
||||
if (!allLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const category = this.category;
|
||||
if (category) {
|
||||
return I18n.t("topics.bottom.category", {
|
||||
category: category.get("name"),
|
||||
});
|
||||
} else {
|
||||
const split = (this.get("model.filter") || "").split("/");
|
||||
if (topicsLength === 0) {
|
||||
return I18n.t("topics.none." + split[0], {
|
||||
category: split[1],
|
||||
});
|
||||
} else {
|
||||
return I18n.t("topics.bottom." + split[0], {
|
||||
category: split[1],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discourseComputed("allLoaded", "model.topics.length")
|
||||
footerEducation(allLoaded, topicsLength) {
|
||||
if (!allLoaded || topicsLength > 0 || !this.currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const segments = (this.get("model.filter") || "").split("/");
|
||||
|
||||
let tab = segments[segments.length - 1];
|
||||
|
||||
if (tab !== "new" && tab !== "unread") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab === "new" && this.currentUser.new_new_view_enabled) {
|
||||
tab = "new_new";
|
||||
}
|
||||
|
||||
return I18n.t("topics.none.educate." + tab, {
|
||||
userPrefsUrl: userPath(
|
||||
`${this.currentUser.get("username_lower")}/preferences/tracking`
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
get renderNewListHeaderControls() {
|
||||
return (
|
||||
this.site.mobileView &&
|
||||
this.get("showTopicsAndRepliesToggle") &&
|
||||
!this.get("bulkSelectEnabled")
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
toggleBulkSelect() {
|
||||
this.bulkSelectHelper.toggleBulkSelect();
|
||||
}
|
||||
|
||||
@action
|
||||
dismissRead(operationType, options) {
|
||||
this.bulkSelectHelper.dismissRead(operationType, options);
|
||||
}
|
||||
|
||||
@action
|
||||
updateAutoAddTopicsToBulkSelect(value) {
|
||||
this.bulkSelectHelper.autoAddTopicsToBulkSelect = value;
|
||||
}
|
||||
|
||||
@action
|
||||
addTopicsToBulkSelect(topics) {
|
||||
this.bulkSelectHelper.addTopics(topics);
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import { inject as controller } from "@ember/controller";
|
||||
import { inject as service } from "@ember/service";
|
||||
import NavigationDefaultController from "discourse/controllers/navigation/default";
|
||||
|
||||
export default class NavigationCategoriesController extends NavigationDefaultController {
|
||||
@service composer;
|
||||
@controller("discovery/categories") discoveryCategories;
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { dependentKeyCompat } from "@ember/object/compat";
|
||||
import { inject as service } from "@ember/service";
|
||||
import NavigationDefaultController from "discourse/controllers/navigation/default";
|
||||
import { calculateFilterMode } from "discourse/lib/filter-mode";
|
||||
|
||||
export default class NavigationCategoryController extends NavigationDefaultController {
|
||||
@service composer;
|
||||
|
||||
@tracked category;
|
||||
@tracked filterType;
|
||||
@tracked noSubcategories;
|
||||
|
||||
@dependentKeyCompat
|
||||
get filterMode() {
|
||||
return calculateFilterMode({
|
||||
category: this.category,
|
||||
filterType: this.filterType,
|
||||
noSubcategories: this.noSubcategories,
|
||||
});
|
||||
}
|
||||
|
||||
get createTopicTargetCategory() {
|
||||
if (this.category?.canCreateTopic) {
|
||||
return this.category;
|
||||
}
|
||||
|
||||
if (this.siteSettings.default_subcategory_on_read_only_category) {
|
||||
return this.category?.subcategoryWithCreateTopicPermission;
|
||||
}
|
||||
}
|
||||
|
||||
get enableCreateTopicButton() {
|
||||
return !!this.createTopicTargetCategory;
|
||||
}
|
||||
|
||||
get canCreateTopic() {
|
||||
return this.currentUser?.can_create_topic;
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import { dependentKeyCompat } from "@ember/object/compat";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { calculateFilterMode } from "discourse/lib/filter-mode";
|
||||
import { TRACKED_QUERY_PARAM_VALUE } from "discourse/lib/topic-list-tracked-filter";
|
||||
|
||||
export default class NavigationDefaultController extends Controller {
|
||||
@service router;
|
||||
@service composer;
|
||||
@controller discovery;
|
||||
|
||||
@tracked category;
|
||||
@tracked filterType;
|
||||
@tracked noSubcategories;
|
||||
|
||||
@dependentKeyCompat
|
||||
get filterMode() {
|
||||
return calculateFilterMode({
|
||||
category: this.category,
|
||||
filterType: this.filterType,
|
||||
noSubcategories: this.noSubcategories,
|
||||
});
|
||||
}
|
||||
|
||||
get skipCategoriesNavItem() {
|
||||
return this.router.currentRoute.queryParams.f === TRACKED_QUERY_PARAM_VALUE;
|
||||
}
|
||||
}
|
|
@ -1,293 +0,0 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { dependentKeyCompat } from "@ember/object/compat";
|
||||
import { or, readOnly } from "@ember/object/computed";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DiscoverySortableController from "discourse/controllers/discovery-sortable";
|
||||
import BulkSelectHelper from "discourse/lib/bulk-select-helper";
|
||||
import { endWith } from "discourse/lib/computed";
|
||||
import { calculateFilterMode } from "discourse/lib/filter-mode";
|
||||
import DismissTopics from "discourse/mixins/dismiss-topics";
|
||||
import NavItem from "discourse/models/nav-item";
|
||||
import Topic from "discourse/models/topic";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class TagShowController extends DiscoverySortableController.extend(
|
||||
DismissTopics
|
||||
) {
|
||||
@service dialog;
|
||||
@service router;
|
||||
@service currentUser;
|
||||
@service siteSettings;
|
||||
|
||||
@tracked category;
|
||||
@tracked filterType;
|
||||
@tracked noSubcategories;
|
||||
|
||||
bulkSelectHelper = new BulkSelectHelper(this);
|
||||
|
||||
tag = null;
|
||||
additionalTags = null;
|
||||
list = null;
|
||||
|
||||
@readOnly("currentUser.staff") canAdminTag;
|
||||
|
||||
navMode = "latest";
|
||||
loading = false;
|
||||
canCreateTopic = false;
|
||||
showInfo = false;
|
||||
|
||||
@endWith("list.filter", "top") top;
|
||||
|
||||
@or("currentUser.canManageTopic", "showDismissRead", "showResetNew")
|
||||
canBulkSelect;
|
||||
|
||||
get bulkSelectEnabled() {
|
||||
return this.bulkSelectHelper.bulkSelectEnabled;
|
||||
}
|
||||
|
||||
get selected() {
|
||||
return this.bulkSelectHelper.selected;
|
||||
}
|
||||
|
||||
@dependentKeyCompat
|
||||
get filterMode() {
|
||||
return calculateFilterMode({
|
||||
category: this.category,
|
||||
filterType: this.filterType,
|
||||
noSubcategories: this.noSubcategories,
|
||||
});
|
||||
}
|
||||
|
||||
@discourseComputed(
|
||||
"canCreateTopic",
|
||||
"category",
|
||||
"canCreateTopicOnCategory",
|
||||
"tag",
|
||||
"canCreateTopicOnTag"
|
||||
)
|
||||
createTopicDisabled(
|
||||
canCreateTopic,
|
||||
category,
|
||||
canCreateTopicOnCategory,
|
||||
tag,
|
||||
canCreateTopicOnTag
|
||||
) {
|
||||
return (
|
||||
!canCreateTopic ||
|
||||
(category && !canCreateTopicOnCategory) ||
|
||||
(tag && !canCreateTopicOnTag)
|
||||
);
|
||||
}
|
||||
|
||||
@discourseComputed("category", "tag.id", "filterType", "noSubcategories")
|
||||
navItems(category, tagId, filterType, noSubcategories) {
|
||||
return NavItem.buildList(category, {
|
||||
tagId,
|
||||
filterType,
|
||||
noSubcategories,
|
||||
siteSettings: this.siteSettings,
|
||||
});
|
||||
}
|
||||
|
||||
@discourseComputed("navMode", "list.topics.length", "loading")
|
||||
footerMessage(navMode, listTopicsLength, loading) {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (listTopicsLength === 0) {
|
||||
return I18n.t(`tagging.topics.none.${navMode}`, {
|
||||
tag: this.tag?.id,
|
||||
});
|
||||
} else {
|
||||
return I18n.t("topics.bottom.tag", {
|
||||
tag: this.tag?.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@discourseComputed("filterType", "list.topics.length")
|
||||
showDismissRead(filterType, topicsLength) {
|
||||
return filterType === "unread" && topicsLength > 0;
|
||||
}
|
||||
|
||||
@discourseComputed("filterType")
|
||||
new(filterType) {
|
||||
return filterType === "new";
|
||||
}
|
||||
|
||||
@discourseComputed("new")
|
||||
showTopicsAndRepliesToggle(isNew) {
|
||||
return isNew && this.currentUser?.new_new_view_enabled;
|
||||
}
|
||||
|
||||
@discourseComputed("topicTrackingState.messageCount")
|
||||
newRepliesCount() {
|
||||
if (this.currentUser?.new_new_view_enabled) {
|
||||
return this.topicTrackingState.countUnread({
|
||||
categoryId: this.category?.id,
|
||||
noSubcategories: this.noSubcategories,
|
||||
tagId: this.tag?.id,
|
||||
});
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@discourseComputed("topicTrackingState.messageCount")
|
||||
newTopicsCount() {
|
||||
if (this.currentUser?.new_new_view_enabled) {
|
||||
return this.topicTrackingState.countNew({
|
||||
categoryId: this.category?.id,
|
||||
noSubcategories: this.noSubcategories,
|
||||
tagId: this.tag?.id,
|
||||
});
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@discourseComputed("new", "list.topics.length")
|
||||
showResetNew(isNew, topicsLength) {
|
||||
return isNew && topicsLength > 0;
|
||||
}
|
||||
|
||||
callResetNew(dismissPosts = false, dismissTopics = false, untrack = false) {
|
||||
const filterTracked =
|
||||
(this.router.currentRoute.queryParams["f"] ||
|
||||
this.router.currentRoute.queryParams["filter"]) === "tracked";
|
||||
|
||||
let topicIds = this.selected ? this.selected.mapBy("id") : null;
|
||||
|
||||
Topic.resetNew(this.category, !this.noSubcategories, {
|
||||
tracked: filterTracked,
|
||||
tag: this.tag,
|
||||
topicIds,
|
||||
dismissPosts,
|
||||
dismissTopics,
|
||||
untrack,
|
||||
}).then((result) => {
|
||||
if (result.topic_ids) {
|
||||
this.topicTrackingState.removeTopics(result.topic_ids);
|
||||
}
|
||||
this.refresh(
|
||||
filterTracked ? { skipResettingParams: ["filter", "f"] } : {}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
showInserted(event) {
|
||||
event?.preventDefault();
|
||||
const tracker = this.topicTrackingState;
|
||||
this.list.loadBefore(tracker.newIncoming, true);
|
||||
tracker.resetTracking();
|
||||
return false;
|
||||
}
|
||||
|
||||
@action
|
||||
changeSort(order) {
|
||||
if (order === this.order) {
|
||||
this.toggleProperty("ascending");
|
||||
} else {
|
||||
this.setProperties({ order, ascending: false });
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
changeNewListSubset(subset) {
|
||||
this.set("subset", subset);
|
||||
}
|
||||
|
||||
@action
|
||||
changePeriod(p) {
|
||||
this.set("period", p);
|
||||
}
|
||||
|
||||
@action
|
||||
toggleInfo() {
|
||||
this.toggleProperty("showInfo");
|
||||
}
|
||||
|
||||
@action
|
||||
refresh() {
|
||||
return this.store
|
||||
.findFiltered("topicList", {
|
||||
filter: this.list?.filter,
|
||||
})
|
||||
.then((list) => {
|
||||
this.set("list", list);
|
||||
this.bulkSelectHelper.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
deleteTag(tagInfo) {
|
||||
const numTopics =
|
||||
this.get("list.topic_list.tags.firstObject.topic_count") || 0;
|
||||
|
||||
let confirmText =
|
||||
numTopics === 0
|
||||
? I18n.t("tagging.delete_confirm_no_topics")
|
||||
: I18n.t("tagging.delete_confirm", { count: numTopics });
|
||||
|
||||
if (tagInfo.synonyms.length > 0) {
|
||||
confirmText +=
|
||||
" " +
|
||||
I18n.t("tagging.delete_confirm_synonyms", {
|
||||
count: tagInfo.synonyms.length,
|
||||
});
|
||||
}
|
||||
|
||||
this.dialog.deleteConfirm({
|
||||
message: confirmText,
|
||||
didConfirm: () => {
|
||||
return this.tag
|
||||
.destroyRecord()
|
||||
.then(() => this.router.transitionTo("tags.index"))
|
||||
.catch(() => this.dialog.alert(I18n.t("generic_error")));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
changeTagNotificationLevel(notificationLevel) {
|
||||
this.tagNotification
|
||||
.update({ notification_level: notificationLevel })
|
||||
.then((response) => {
|
||||
const payload = response.responseJson;
|
||||
|
||||
this.tagNotification.set("notification_level", notificationLevel);
|
||||
|
||||
this.currentUser.setProperties({
|
||||
watched_tags: payload.watched_tags,
|
||||
watching_first_post_tags: payload.watching_first_post_tags,
|
||||
tracked_tags: payload.tracked_tags,
|
||||
muted_tags: payload.muted_tags,
|
||||
regular_tags: payload.regular_tags,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
toggleBulkSelect() {
|
||||
this.bulkSelectHelper.toggleBulkSelect();
|
||||
}
|
||||
|
||||
@action
|
||||
dismissRead(operationType, options) {
|
||||
this.bulkSelectHelper.dismissRead(operationType, options);
|
||||
}
|
||||
|
||||
@action
|
||||
updateAutoAddTopicsToBulkSelect(value) {
|
||||
this.bulkSelectHelper.autoAddTopicsToBulkSelect = value;
|
||||
}
|
||||
|
||||
@action
|
||||
addTopicsToBulkSelect(topics) {
|
||||
this.bulkSelectHelper.addTopics(topics);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { queryParams } from "discourse/controllers/discovery-sortable";
|
||||
import TagShowController from "discourse/controllers/tag-show";
|
||||
import DiscoveryListController, {
|
||||
queryParams,
|
||||
} from "discourse/controllers/discovery/list";
|
||||
|
||||
export default class TagsIntersectionController extends TagShowController {
|
||||
export default class TagsIntersectionController extends DiscoveryListController {
|
||||
queryParams = [...Object.keys(queryParams), { categoryParam: "category" }];
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export function resetCustomUserNavMessagesDropdownRows() {
|
|||
export default class extends Controller {
|
||||
@service router;
|
||||
@controller user;
|
||||
@controller userTopicsList;
|
||||
|
||||
@tracked group;
|
||||
@tracked tagId;
|
||||
|
@ -38,6 +39,10 @@ export default class extends Controller {
|
|||
@readOnly("router.currentRoute.parent.name") currentParentRouteName;
|
||||
@readOnly("site.can_tag_pms") pmTaggingEnabled;
|
||||
|
||||
get bulkSelectHelper() {
|
||||
this.userTopicsList.bulkSelectHelper;
|
||||
}
|
||||
|
||||
get messagesDropdownValue() {
|
||||
let value;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { dasherize } from "@ember/string";
|
||||
import DiscoverySortableController from "discourse/controllers/discovery-sortable";
|
||||
import Site from "discourse/models/site";
|
||||
import buildCategoryRoute from "discourse/routes/build-category-route";
|
||||
import buildTopicRoute from "discourse/routes/build-topic-route";
|
||||
|
@ -9,19 +8,6 @@ export default {
|
|||
after: "inject-discourse-objects",
|
||||
|
||||
initialize(app) {
|
||||
app.register(
|
||||
"controller:discovery.category",
|
||||
DiscoverySortableController.extend()
|
||||
);
|
||||
app.register(
|
||||
"controller:discovery.category-none",
|
||||
DiscoverySortableController.extend()
|
||||
);
|
||||
app.register(
|
||||
"controller:discovery.category-all",
|
||||
DiscoverySortableController.extend()
|
||||
);
|
||||
|
||||
app.register(
|
||||
"route:discovery.category",
|
||||
buildCategoryRoute({ filter: "default" })
|
||||
|
@ -38,18 +24,6 @@ export default {
|
|||
const site = Site.current();
|
||||
site.get("filters").forEach((filter) => {
|
||||
const filterDasherized = dasherize(filter);
|
||||
app.register(
|
||||
`controller:discovery.${filterDasherized}`,
|
||||
DiscoverySortableController.extend()
|
||||
);
|
||||
app.register(
|
||||
`controller:discovery.${filterDasherized}-category`,
|
||||
DiscoverySortableController.extend()
|
||||
);
|
||||
app.register(
|
||||
`controller:discovery.${filterDasherized}-category-none`,
|
||||
DiscoverySortableController.extend()
|
||||
);
|
||||
|
||||
app.register(
|
||||
`route:discovery.${filterDasherized}`,
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import EmberObject from "@ember/object";
|
||||
import { dependentKeyCompat } from "@ember/object/compat";
|
||||
import { inject as service } from "@ember/service";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
|
||||
let reopenedClasses = [];
|
||||
|
||||
function ControllerShim(resolverName, deprecationId) {
|
||||
return class AbstractControllerShim extends EmberObject {
|
||||
static printDeprecation() {
|
||||
deprecated(
|
||||
`${resolverName} no longer exists, and this shim will eventually be removed. To fetch information about the current discovery route, use the discovery service instead.`,
|
||||
{
|
||||
deprecationId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static reopen() {
|
||||
this.printDeprecation();
|
||||
reopenedClasses.push(resolverName);
|
||||
return super.reopen(...arguments);
|
||||
}
|
||||
|
||||
@service discovery;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.constructor.printDeprecation();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class NavigationCategoryControllerShim extends ControllerShim(
|
||||
"controller:navigation/category",
|
||||
"discourse.navigation-category-controller"
|
||||
) {
|
||||
@dependentKeyCompat
|
||||
get category() {
|
||||
this.constructor.printDeprecation();
|
||||
return this.discovery.category;
|
||||
}
|
||||
}
|
||||
|
||||
class DiscoveryTopicsControllerShim extends ControllerShim(
|
||||
"controller:discovery/topics",
|
||||
"discourse.discovery-topics-controller"
|
||||
) {
|
||||
@dependentKeyCompat
|
||||
get model() {
|
||||
this.constructor.printDeprecation();
|
||||
if (this.discovery.onDiscoveryRoute) {
|
||||
return this.discovery.currentTopicList;
|
||||
}
|
||||
}
|
||||
|
||||
@dependentKeyCompat
|
||||
get category() {
|
||||
this.constructor.printDeprecation();
|
||||
if (this.discovery.onDiscoveryRoute) {
|
||||
return this.discovery.category;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TagShowControllerShim extends ControllerShim(
|
||||
"controller:tag-show",
|
||||
"discourse.tag-show-controller"
|
||||
) {
|
||||
@dependentKeyCompat
|
||||
get tag() {
|
||||
this.constructor.printDeprecation();
|
||||
return this.discovery.tag;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize(container) {
|
||||
container.register(
|
||||
"controller:navigation/category",
|
||||
NavigationCategoryControllerShim
|
||||
);
|
||||
|
||||
container.register(
|
||||
"controller:discovery/topics",
|
||||
DiscoveryTopicsControllerShim
|
||||
);
|
||||
|
||||
container.register("controller:tag-show", TagShowControllerShim);
|
||||
|
||||
container.lookup("service:router").on("routeDidChange", (transition) => {
|
||||
const destination = transition.to?.name;
|
||||
if (
|
||||
destination?.startsWith("discovery.") ||
|
||||
destination?.startsWith("tags.show") ||
|
||||
destination === "tag.show"
|
||||
) {
|
||||
// Ensure any reopened shims are initialized in case anything has added observers
|
||||
reopenedClasses.forEach((resolverName) =>
|
||||
container.lookup(resolverName)
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
|
@ -4,6 +4,7 @@ import { inject as service } from "@ember/service";
|
|||
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||
import { NotificationLevels } from "discourse/lib/notification-levels";
|
||||
import Topic from "discourse/models/topic";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
export default class BulkSelectHelper {
|
||||
@service router;
|
||||
|
@ -28,6 +29,7 @@ export default class BulkSelectHelper {
|
|||
this.selected.concat(topics);
|
||||
}
|
||||
|
||||
@bind
|
||||
toggleBulkSelect() {
|
||||
this.bulkSelectEnabled = !this.bulkSelectEnabled;
|
||||
this.clear();
|
||||
|
|
|
@ -21,7 +21,7 @@ import { addSearchSuggestion as addGlimmerSearchSuggestion } from "discourse/com
|
|||
import { REFRESH_COUNTS_APP_EVENT_NAME as REFRESH_USER_SIDEBAR_CATEGORIES_SECTION_COUNTS_APP_EVENT_NAME } from "discourse/components/sidebar/user/categories-section";
|
||||
import { addTopicTitleDecorator } from "discourse/components/topic-title";
|
||||
import { addUserMenuProfileTabItem } from "discourse/components/user-menu/profile-tab-content";
|
||||
import { addDiscoveryQueryParam } from "discourse/controllers/discovery-sortable";
|
||||
import { addDiscoveryQueryParam } from "discourse/controllers/discovery/list";
|
||||
import { registerFullPageSearchType } from "discourse/controllers/full-page-search";
|
||||
import { registerCustomPostMessageCallback as registerCustomPostMessageCallback1 } from "discourse/controllers/topic";
|
||||
import { registerCustomUserNavMessagesDropdownRow } from "discourse/controllers/user-private-messages";
|
||||
|
|
76
app/assets/javascripts/discourse/app/lib/tracked-tools.js
Normal file
76
app/assets/javascripts/discourse/app/lib/tracked-tools.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
|
||||
/**
|
||||
* Define a tracked property on an object without needing to use the @tracked decorator.
|
||||
* Useful when meta-programming the creation of properties on an object.
|
||||
*
|
||||
* This must be run before the property is first accessed, so it normally makes sense for
|
||||
* this to only be called from a constructor.
|
||||
*/
|
||||
export function defineTrackedProperty(target, key, value) {
|
||||
Object.defineProperty(
|
||||
target,
|
||||
key,
|
||||
tracked(target, key, { enumerable: true, value })
|
||||
);
|
||||
}
|
||||
|
||||
class ResettableTrackedState {
|
||||
@tracked currentValue;
|
||||
previousUpstreamValue;
|
||||
}
|
||||
|
||||
function getOrCreateState(map, instance) {
|
||||
let state = map.get(instance);
|
||||
if (!state) {
|
||||
state = new ResettableTrackedState();
|
||||
map.set(instance, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @decorator
|
||||
*
|
||||
* Marks a field as tracked. Its initializer will be re-run whenever upstream state changes.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* class UserRenameForm {
|
||||
* @resettableTracked fullName = this.args.fullName;
|
||||
*
|
||||
* updateName(newName) {
|
||||
* this.fullName = newName;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* `this.fullName` will be updated whenever `updateName()` is called, or there is a change to
|
||||
* `this.args.fullName`.
|
||||
*
|
||||
*/
|
||||
export function resettableTracked(prototype, key, descriptor) {
|
||||
// One WeakMap per-property-per-class. Keys are instances of the class
|
||||
const states = new WeakMap();
|
||||
|
||||
return {
|
||||
get() {
|
||||
const state = getOrCreateState(states, this);
|
||||
|
||||
const upstreamValue = descriptor.initializer?.call(this);
|
||||
|
||||
if (upstreamValue !== state.previousUpstreamValue) {
|
||||
state.currentValue = upstreamValue;
|
||||
state.previousUpstreamValue = upstreamValue;
|
||||
}
|
||||
|
||||
return state.currentValue;
|
||||
},
|
||||
|
||||
set(value) {
|
||||
const state = getOrCreateState(states, this);
|
||||
state.currentValue = value;
|
||||
},
|
||||
};
|
||||
}
|
|
@ -260,7 +260,7 @@ const DiscourseURL = EmberObject.extend({
|
|||
if (oldPath === path) {
|
||||
// If navigating to the same path send an app event.
|
||||
// Views can watch it and tell their controllers to refresh
|
||||
this.appEvents.trigger("url:refresh");
|
||||
this.routerService.refresh();
|
||||
}
|
||||
|
||||
// TODO: Extract into rules we can inject into the URL handler
|
||||
|
@ -400,7 +400,7 @@ const DiscourseURL = EmberObject.extend({
|
|||
(path === "/" || path === "/" + homepage) &&
|
||||
(oldPath === "/" || oldPath === "/" + homepage)
|
||||
) {
|
||||
this.appEvents.trigger("url:refresh");
|
||||
this.routerService.refresh();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -421,6 +421,10 @@ const DiscourseURL = EmberObject.extend({
|
|||
return this.container.lookup("router:main");
|
||||
},
|
||||
|
||||
get routerService() {
|
||||
return this.container.lookup("service:router");
|
||||
},
|
||||
|
||||
get appEvents() {
|
||||
return this.container.lookup("service:app-events");
|
||||
},
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import { action } from "@ember/object";
|
||||
import Mixin from "@ember/object/mixin";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DismissNew from "discourse/components/modal/dismiss-new";
|
||||
|
||||
export default Mixin.create({
|
||||
modal: service(),
|
||||
currentUser: service(),
|
||||
|
||||
@action
|
||||
resetNew() {
|
||||
if (!this.currentUser.new_new_view_enabled) {
|
||||
return this.callResetNew();
|
||||
}
|
||||
|
||||
this.modal.show(DismissNew, {
|
||||
model: {
|
||||
selectedTopics: this.selected,
|
||||
subset: this.model.listParams?.subset,
|
||||
dismissCallback: ({ dismissPosts, dismissTopics, untrack }) => {
|
||||
this.callResetNew(dismissPosts, dismissTopics, untrack);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
|
@ -1,18 +0,0 @@
|
|||
// A Mixin that a view can use to listen for 'url:refresh' when
|
||||
// it is on screen, and will send an action to refresh its data.
|
||||
//
|
||||
// This is useful if you want to get around Ember's default
|
||||
// behavior of not refreshing when navigating to the same place.
|
||||
export default {
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.appEvents.on("url:refresh", this, "refresh");
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.appEvents.off("url:refresh", this, "refresh");
|
||||
},
|
||||
};
|
|
@ -1,4 +1,5 @@
|
|||
import ArrayProxy from "@ember/array/proxy";
|
||||
import EmberObject from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { number } from "discourse/lib/formatter";
|
||||
import PreloadStore from "discourse/lib/preload-store";
|
||||
|
@ -103,7 +104,7 @@ CategoryList.reopenClass({
|
|||
return ajax(
|
||||
`/categories.json?parent_category_id=${category.get("id")}`
|
||||
).then((result) => {
|
||||
return CategoryList.create({
|
||||
return EmberObject.create({
|
||||
categories: this.categoriesFrom(store, result),
|
||||
parentCategory: category,
|
||||
});
|
||||
|
|
|
@ -791,7 +791,7 @@ const Composer = RestModel.extend({
|
|||
composerTotalOpened: opts.composerTime,
|
||||
typingTime: opts.typingTime,
|
||||
whisper: opts.whisper,
|
||||
tags: opts.tags,
|
||||
tags: opts.tags || [],
|
||||
noBump: opts.noBump,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { all, Promise } from "rsvp";
|
||||
import {
|
||||
changeNewListSubset,
|
||||
changeSort,
|
||||
queryParams,
|
||||
resetParams,
|
||||
} from "discourse/controllers/discovery-sortable";
|
||||
import { queryParams, resetParams } from "discourse/controllers/discovery/list";
|
||||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
import PreloadStore from "discourse/lib/preload-store";
|
||||
import { setTopicList } from "discourse/lib/topic-list-tracker";
|
||||
import Category from "discourse/models/category";
|
||||
import CategoryList from "discourse/models/category-list";
|
||||
import TopicList from "discourse/models/topic-list";
|
||||
|
@ -18,64 +14,62 @@ import {
|
|||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
@disableImplicitInjections
|
||||
class AbstractCategoryRoute extends DiscourseRoute {
|
||||
@service composer;
|
||||
@service router;
|
||||
@service store;
|
||||
@service topicTrackingState;
|
||||
@service("search") searchService;
|
||||
|
||||
queryParams = queryParams;
|
||||
|
||||
model(modelParams) {
|
||||
templateName = "discovery/list";
|
||||
controllerName = "discovery/list";
|
||||
|
||||
async model(params, transition) {
|
||||
const category = Category.findBySlugPathWithID(
|
||||
modelParams.category_slug_path_with_id
|
||||
params.category_slug_path_with_id
|
||||
);
|
||||
|
||||
if (!category) {
|
||||
const parts = modelParams.category_slug_path_with_id.split("/");
|
||||
if (parts.length > 0 && parts[parts.length - 1].match(/^\d+$/)) {
|
||||
parts.pop();
|
||||
}
|
||||
|
||||
return Category.reloadBySlugPath(parts.join("/")).then((result) => {
|
||||
const record = this.store.createRecord("category", result.category);
|
||||
record.setupGroupsAndPermissions();
|
||||
this.site.updateCategory(record);
|
||||
return { category: record, modelParams };
|
||||
});
|
||||
}
|
||||
|
||||
if (category) {
|
||||
return { category, modelParams };
|
||||
}
|
||||
}
|
||||
|
||||
afterModel(model, transition) {
|
||||
if (!model) {
|
||||
this.router.replaceWith("/404");
|
||||
return;
|
||||
}
|
||||
|
||||
const { category, modelParams } = model;
|
||||
|
||||
if (
|
||||
this.routeConfig?.no_subcategories === undefined &&
|
||||
category.default_list_filter === "none" &&
|
||||
this.routeConfig?.filter === "default" &&
|
||||
modelParams
|
||||
params
|
||||
) {
|
||||
// TODO: avoid throwing away preload data by redirecting on the server
|
||||
PreloadStore.getAndRemove("topic_list");
|
||||
this.router.replaceWith(
|
||||
"discovery.categoryNone",
|
||||
modelParams.category_slug_path_with_id
|
||||
params.category_slug_path_with_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this._setupNavigation(category);
|
||||
return all([
|
||||
this._createSubcategoryList(category),
|
||||
this._retrieveTopicList(category, transition, modelParams),
|
||||
]);
|
||||
const subcategoryListPromise = this._createSubcategoryList(category);
|
||||
const topicListPromise = this._retrieveTopicList(
|
||||
category,
|
||||
transition,
|
||||
params
|
||||
);
|
||||
|
||||
const noSubcategories = !!this.routeConfig?.no_subcategories;
|
||||
const filterType = this.filter(category).split("/")[0];
|
||||
|
||||
return {
|
||||
category,
|
||||
modelParams: params,
|
||||
subcategoryList: await subcategoryListPromise,
|
||||
list: await topicListPromise,
|
||||
noSubcategories,
|
||||
filterType,
|
||||
};
|
||||
}
|
||||
|
||||
filter(category) {
|
||||
|
@ -84,32 +78,13 @@ class AbstractCategoryRoute extends DiscourseRoute {
|
|||
: this.routeConfig?.filter;
|
||||
}
|
||||
|
||||
_setupNavigation(category) {
|
||||
const noSubcategories =
|
||||
this.routeConfig && !!this.routeConfig.no_subcategories,
|
||||
filterType = this.filter(category).split("/")[0];
|
||||
|
||||
this.controllerFor("navigation/category").setProperties({
|
||||
category,
|
||||
filterType,
|
||||
noSubcategories,
|
||||
});
|
||||
}
|
||||
|
||||
_createSubcategoryList(category) {
|
||||
this._categoryList = null;
|
||||
|
||||
async _createSubcategoryList(category) {
|
||||
if (category.isParent && category.show_subcategory_list) {
|
||||
return CategoryList.listForParent(this.store, category).then(
|
||||
(list) => (this._categoryList = list)
|
||||
);
|
||||
return CategoryList.listForParent(this.store, category);
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not loading a subcategory list just resolve
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
_retrieveTopicList(category, transition, modelParams) {
|
||||
async _retrieveTopicList(category, transition, modelParams) {
|
||||
const findOpts = filterQueryParams(modelParams, this.routeConfig);
|
||||
const extras = { cached: this.isPoppedState(transition) };
|
||||
|
||||
|
@ -119,17 +94,16 @@ class AbstractCategoryRoute extends DiscourseRoute {
|
|||
}
|
||||
listFilter += `/l/${this.filter(category)}`;
|
||||
|
||||
return findTopicList(
|
||||
const topicList = await findTopicList(
|
||||
this.store,
|
||||
this.topicTrackingState,
|
||||
listFilter,
|
||||
findOpts,
|
||||
extras
|
||||
).then((list) => {
|
||||
TopicList.hideUniformCategory(list, category);
|
||||
this.set("topics", list);
|
||||
return list;
|
||||
});
|
||||
);
|
||||
TopicList.hideUniformCategory(topicList, category);
|
||||
|
||||
return topicList;
|
||||
}
|
||||
|
||||
titleToken() {
|
||||
|
@ -153,52 +127,20 @@ class AbstractCategoryRoute extends DiscourseRoute {
|
|||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
const topics = this.topics,
|
||||
category = model.category;
|
||||
super.setupController(...arguments);
|
||||
controller.bulkSelectHelper.clear();
|
||||
this.searchService.searchContext = model.category.get("searchContext");
|
||||
setTopicList(model.list);
|
||||
|
||||
let topicOpts = {
|
||||
model: topics,
|
||||
category,
|
||||
period:
|
||||
topics.get("for_period") ||
|
||||
(model.modelParams && model.modelParams.period),
|
||||
noSubcategories: this.routeConfig && !!this.routeConfig.no_subcategories,
|
||||
expandAllPinned: true,
|
||||
};
|
||||
|
||||
const p = category.get("params");
|
||||
if (p && Object.keys(p).length) {
|
||||
if (p.order !== undefined) {
|
||||
topicOpts.order = p.order;
|
||||
const p = model.category.params;
|
||||
if (p?.order !== undefined) {
|
||||
controller.order = p.order;
|
||||
}
|
||||
if (p.ascending !== undefined) {
|
||||
topicOpts.ascending = p.ascending;
|
||||
if (p?.ascending !== undefined) {
|
||||
controller.ascending = p.ascending;
|
||||
}
|
||||
}
|
||||
|
||||
this.controllerFor("discovery/topics").setProperties(topicOpts);
|
||||
this.controllerFor("discovery/topics").bulkSelectHelper.clear();
|
||||
this.searchService.searchContext = category.get("searchContext");
|
||||
this.set("topics", null);
|
||||
}
|
||||
|
||||
renderTemplate() {
|
||||
this.render("navigation/category", { outlet: "navigation-bar" });
|
||||
|
||||
if (this._categoryList) {
|
||||
this.render("discovery/categories", {
|
||||
outlet: "header-list-container",
|
||||
model: this._categoryList,
|
||||
});
|
||||
} else {
|
||||
this.disconnectOutlet({ outlet: "header-list-container" });
|
||||
}
|
||||
this.render("discovery/topics", {
|
||||
controller: "discovery/topics",
|
||||
outlet: "list-container",
|
||||
});
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
super.deactivate(...arguments);
|
||||
|
||||
|
@ -216,16 +158,6 @@ class AbstractCategoryRoute extends DiscourseRoute {
|
|||
this.refresh();
|
||||
}
|
||||
|
||||
@action
|
||||
changeSort(sortBy) {
|
||||
changeSort.call(this, sortBy);
|
||||
}
|
||||
|
||||
@action
|
||||
changeNewListSubset(subset) {
|
||||
changeNewListSubset.call(this, subset);
|
||||
}
|
||||
|
||||
@action
|
||||
resetParams(skipParams = []) {
|
||||
resetParams.call(this, skipParams);
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import {
|
||||
changeNewListSubset,
|
||||
changeSort,
|
||||
queryParams,
|
||||
resetParams,
|
||||
} from "discourse/controllers/discovery-sortable";
|
||||
import { queryParams, resetParams } from "discourse/controllers/discovery/list";
|
||||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
import { setTopicList } from "discourse/lib/topic-list-tracker";
|
||||
import { defaultHomepage } from "discourse/lib/utilities";
|
||||
import Session from "discourse/models/session";
|
||||
import Site from "discourse/models/site";
|
||||
import User from "discourse/models/user";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import { deepEqual } from "discourse-common/lib/object";
|
||||
import I18n from "discourse-i18n";
|
||||
|
@ -96,31 +92,36 @@ export async function findTopicList(
|
|||
return list;
|
||||
}
|
||||
|
||||
@disableImplicitInjections
|
||||
class AbstractTopicRoute extends DiscourseRoute {
|
||||
@service screenTrack;
|
||||
@service store;
|
||||
@service topicTrackingState;
|
||||
@service currentUser;
|
||||
|
||||
queryParams = queryParams;
|
||||
templateName = "discovery/list";
|
||||
controllerName = "discovery/list";
|
||||
|
||||
beforeModel() {
|
||||
this.controllerFor("navigation/default").set(
|
||||
"filterType",
|
||||
this.routeConfig.filter.split("/")[0]
|
||||
);
|
||||
}
|
||||
|
||||
model(data, transition) {
|
||||
async model(data, transition) {
|
||||
// attempt to stop early cause we need this to be called before .sync
|
||||
this.screenTrack.stop();
|
||||
|
||||
const findOpts = filterQueryParams(data),
|
||||
findExtras = { cached: this.isPoppedState(transition) };
|
||||
|
||||
return findTopicList(
|
||||
const topicListPromise = findTopicList(
|
||||
this.store,
|
||||
this.topicTrackingState,
|
||||
this.routeConfig.filter,
|
||||
findOpts,
|
||||
findExtras
|
||||
);
|
||||
|
||||
return {
|
||||
list: await topicListPromise,
|
||||
filterType: this.routeConfig.filter.split("/")[0],
|
||||
};
|
||||
}
|
||||
|
||||
titleToken() {
|
||||
|
@ -135,40 +136,9 @@ class AbstractTopicRoute extends DiscourseRoute {
|
|||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
const topicOpts = {
|
||||
model,
|
||||
category: null,
|
||||
period: model.get("for_period") || model.get("params.period"),
|
||||
expandAllPinned: false,
|
||||
expandGloballyPinned: true,
|
||||
};
|
||||
|
||||
this.controllerFor("discovery/topics").setProperties(topicOpts);
|
||||
this.controllerFor("discovery/topics").bulkSelectHelper.clear();
|
||||
|
||||
this.controllerFor("navigation/default").set(
|
||||
"canCreateTopic",
|
||||
model.get("can_create_topic")
|
||||
);
|
||||
}
|
||||
|
||||
renderTemplate() {
|
||||
this.render("navigation/default", { outlet: "navigation-bar" });
|
||||
|
||||
this.render("discovery/topics", {
|
||||
controller: "discovery/topics",
|
||||
outlet: "list-container",
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
changeSort(sortBy) {
|
||||
changeSort.call(this, sortBy);
|
||||
}
|
||||
|
||||
@action
|
||||
changeNewListSubset(subset) {
|
||||
changeNewListSubset.call(this, subset);
|
||||
super.setupController(...arguments);
|
||||
controller.bulkSelectHelper.clear();
|
||||
setTopicList(model.list);
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -178,10 +148,10 @@ class AbstractTopicRoute extends DiscourseRoute {
|
|||
|
||||
@action
|
||||
willTransition() {
|
||||
if (this.routeConfig.filter === "top") {
|
||||
User.currentProp("user_option.should_be_redirected_to_top", false);
|
||||
if (User.currentProp("user_option.redirected_to_top")) {
|
||||
User.currentProp("user_option.redirected_to_top.reason", null);
|
||||
if (this.routeConfig.filter === "top" && this.currentUser) {
|
||||
this.currentUser.set("user_option.should_be_redirected_to_top", false);
|
||||
if (this.currentUser.user_option?.redirected_to_top) {
|
||||
this.currentUser.set("user_option.redirected_to_top.reason", null);
|
||||
}
|
||||
}
|
||||
return super.willTransition(...arguments);
|
||||
|
|
|
@ -3,7 +3,6 @@ import { inject as service } from "@ember/service";
|
|||
import { hash } from "rsvp";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import PreloadStore from "discourse/lib/preload-store";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { defaultHomepage } from "discourse/lib/utilities";
|
||||
import CategoryList from "discourse/models/category-list";
|
||||
import TopicList from "discourse/models/topic-list";
|
||||
|
@ -14,10 +13,8 @@ export default class DiscoveryCategoriesRoute extends DiscourseRoute {
|
|||
@service router;
|
||||
@service session;
|
||||
|
||||
renderTemplate() {
|
||||
this.render("navigation/categories", { outlet: "navigation-bar" });
|
||||
this.render("discovery/categories", { outlet: "list-container" });
|
||||
}
|
||||
templateName = "discovery/categories";
|
||||
controllerName = "discovery/categories";
|
||||
|
||||
findCategories() {
|
||||
let style =
|
||||
|
@ -131,27 +128,16 @@ export default class DiscoveryCategoriesRoute extends DiscourseRoute {
|
|||
return I18n.t("filters.categories.title");
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
|
||||
this.controllerFor("navigation/categories").setProperties({
|
||||
showCategoryAdmin: model.get("can_create_category"),
|
||||
canCreateTopic: model.get("can_create_topic"),
|
||||
setupController(controller) {
|
||||
controller.setProperties({
|
||||
discovery: this.controllerFor("discovery"),
|
||||
});
|
||||
|
||||
super.setupController(...arguments);
|
||||
}
|
||||
|
||||
@action
|
||||
triggerRefresh() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
@action
|
||||
createCategory() {
|
||||
this.router.transitionTo("newCategory");
|
||||
}
|
||||
|
||||
@action
|
||||
reorderCategories() {
|
||||
showModal("reorder-categories");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { action } from "@ember/object";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
|
@ -18,29 +17,4 @@ export default class DiscoveryFilterRoute extends DiscourseRoute {
|
|||
const filterText = I18n.t("filters.filter.title");
|
||||
return I18n.t("filters.with_topics", { filter: filterText });
|
||||
}
|
||||
|
||||
setupController(_controller, model) {
|
||||
this.controllerFor("discovery/topics").setProperties({ model });
|
||||
|
||||
this.controllerFor("navigation/filter").setProperties({
|
||||
newQueryString: this.paramsFor("discovery.filter").q,
|
||||
});
|
||||
}
|
||||
|
||||
renderTemplate() {
|
||||
this.render("navigation/filter", { outlet: "navigation-bar" });
|
||||
|
||||
this.render("discovery/topics", {
|
||||
controller: "discovery/topics",
|
||||
outlet: "list-container",
|
||||
});
|
||||
}
|
||||
|
||||
// TODO(tgxworld): The following 2 actions are required by the `discovery/topics` controller which is not necessary for this route.
|
||||
// Figure out a way to remove this.
|
||||
@action
|
||||
changeSort() {}
|
||||
|
||||
@action
|
||||
changeNewListSubset() {}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { resetCachedTopicList } from "discourse/lib/cached-topic-list";
|
||||
import { setTopicList } from "discourse/lib/topic-list-tracker";
|
||||
import User from "discourse/models/user";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
/**
|
||||
The parent route for all discovery routes.
|
||||
Handles the logic for showing the loading spinners.
|
||||
**/
|
||||
export default class DiscoveryRoute extends DiscourseRoute {
|
||||
@service router;
|
||||
|
@ -47,48 +45,12 @@ export default class DiscoveryRoute extends DiscourseRoute {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
loading() {
|
||||
this.controllerFor("discovery").loadingBegan();
|
||||
|
||||
// We don't want loading to bubble
|
||||
return true;
|
||||
}
|
||||
|
||||
@action
|
||||
loadingComplete() {
|
||||
this.controllerFor("discovery").loadingComplete();
|
||||
}
|
||||
|
||||
@action
|
||||
didTransition() {
|
||||
this.send("loadingComplete");
|
||||
|
||||
const model = this.controllerFor("discovery/topics").get("model");
|
||||
setTopicList(model);
|
||||
}
|
||||
|
||||
// clear a pinned topic
|
||||
@action
|
||||
clearPin(topic) {
|
||||
topic.clearPin();
|
||||
}
|
||||
|
||||
@action
|
||||
dismissReadTopics(dismissTopics) {
|
||||
const operationType = dismissTopics ? "topics" : "posts";
|
||||
this.send("dismissRead", operationType);
|
||||
}
|
||||
|
||||
@action
|
||||
dismissRead(operationType) {
|
||||
const controller = this.controllerFor("discovery/topics");
|
||||
controller.send("dismissRead", operationType, {
|
||||
categoryId: controller.get("category.id"),
|
||||
includeSubcategories: !controller.noSubcategories,
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
resetCachedTopicList(this.session);
|
||||
super.refresh();
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import {
|
||||
queryParams,
|
||||
resetParams,
|
||||
} from "discourse/controllers/discovery-sortable";
|
||||
import { queryParams, resetParams } from "discourse/controllers/discovery/list";
|
||||
import { filterTypeForMode } from "discourse/lib/filter-mode";
|
||||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
import PreloadStore from "discourse/lib/preload-store";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { setTopicList } from "discourse/lib/topic-list-tracker";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import Category from "discourse/models/category";
|
||||
import Composer from "discourse/models/composer";
|
||||
import PermissionType from "discourse/models/permission-type";
|
||||
import {
|
||||
filterQueryParams,
|
||||
findTopicList,
|
||||
} from "discourse/routes/build-topic-route";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import { makeArray } from "discourse-common/lib/helpers";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
const NONE = "none";
|
||||
const ALL = "all";
|
||||
|
||||
@disableImplicitInjections
|
||||
export default class TagShowRoute extends DiscourseRoute {
|
||||
@service composer;
|
||||
@service router;
|
||||
@service currentUser;
|
||||
@service store;
|
||||
@service topicTrackingState;
|
||||
@service("search") searchService;
|
||||
|
||||
queryParams = queryParams;
|
||||
controllerName = "tag.show";
|
||||
templateName = "tag.show";
|
||||
controllerName = "discovery/list";
|
||||
templateName = "discovery/list";
|
||||
routeConfig = {};
|
||||
|
||||
get navMode() {
|
||||
|
@ -40,14 +40,6 @@ export default class TagShowRoute extends DiscourseRoute {
|
|||
return this.routeConfig.noSubcategories;
|
||||
}
|
||||
|
||||
beforeModel() {
|
||||
const controller = this.controllerFor(this.controllerName);
|
||||
controller.setProperties({
|
||||
loading: true,
|
||||
showInfo: false,
|
||||
});
|
||||
}
|
||||
|
||||
async model(params, transition) {
|
||||
const tag = this.store.createRecord("tag", {
|
||||
id: escapeExpression(params.tag_id),
|
||||
|
@ -63,7 +55,7 @@ export default class TagShowRoute extends DiscourseRoute {
|
|||
});
|
||||
}
|
||||
|
||||
const filterType = this.navMode.split("/")[0];
|
||||
const filterType = filterTypeForMode(this.navMode);
|
||||
|
||||
let tagNotification;
|
||||
if (tag && tag.id !== NONE && this.currentUser && !additionalTags) {
|
||||
|
@ -139,8 +131,6 @@ export default class TagShowRoute extends DiscourseRoute {
|
|||
});
|
||||
}
|
||||
|
||||
setTopicList(list);
|
||||
|
||||
return {
|
||||
tag,
|
||||
category,
|
||||
|
@ -151,20 +141,14 @@ export default class TagShowRoute extends DiscourseRoute {
|
|||
canCreateTopic: list.can_create_topic,
|
||||
canCreateTopicOnCategory: category?.permission === PermissionType.FULL,
|
||||
canCreateTopicOnTag: !tag.staff || this.currentUser?.staff,
|
||||
noSubcategories: this.noSubcategories,
|
||||
};
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
const noSubcategories = this.noSubcategories;
|
||||
|
||||
controller.setProperties({
|
||||
model: model.tag,
|
||||
...model,
|
||||
period: model.list.for_period,
|
||||
navMode: this.navMode,
|
||||
noSubcategories,
|
||||
loading: false,
|
||||
});
|
||||
super.setupController(...arguments);
|
||||
controller.bulkSelectHelper.clear();
|
||||
setTopicList(model.list);
|
||||
|
||||
if (model.category || model.additionalTags) {
|
||||
const tagIntersectionSearchContext = {
|
||||
|
@ -220,68 +204,10 @@ export default class TagShowRoute extends DiscourseRoute {
|
|||
this.searchService.searchContext = null;
|
||||
}
|
||||
|
||||
@action
|
||||
renameTag(tag) {
|
||||
showModal("rename-tag", { model: tag });
|
||||
}
|
||||
|
||||
@action
|
||||
createTopic() {
|
||||
if (this.currentUser?.has_topic_draft) {
|
||||
this.openTopicDraft();
|
||||
} else {
|
||||
const controller = this.controllerFor(this.controllerName);
|
||||
this.composer
|
||||
.open({
|
||||
categoryId: controller.category?.id,
|
||||
action: Composer.CREATE_TOPIC,
|
||||
draftKey: Composer.NEW_TOPIC_KEY,
|
||||
})
|
||||
.then(() => {
|
||||
// Pre-fill the tags input field
|
||||
if (this.composer.canEditTags && controller.tag?.id) {
|
||||
const composerModel = this.composer.model;
|
||||
composerModel.set("tags", this._controllerTags(controller));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
dismissReadTopics(dismissTopics) {
|
||||
const operationType = dismissTopics ? "topics" : "posts";
|
||||
this.send("dismissRead", operationType);
|
||||
}
|
||||
|
||||
@action
|
||||
dismissRead(operationType) {
|
||||
const controller = this.controllerFor(this.controllerName);
|
||||
let options = {
|
||||
tagName: controller.tag?.id,
|
||||
};
|
||||
const categoryId = controller.category?.id;
|
||||
|
||||
if (categoryId) {
|
||||
options = {
|
||||
...options,
|
||||
categoryId,
|
||||
includeSubcategories: !controller.noSubcategories,
|
||||
};
|
||||
}
|
||||
|
||||
controller.send("dismissRead", operationType, options);
|
||||
}
|
||||
|
||||
@action
|
||||
resetParams(skipParams = []) {
|
||||
resetParams.call(this, skipParams);
|
||||
}
|
||||
|
||||
_controllerTags(controller) {
|
||||
return [controller.get("model.id"), ...makeArray(controller.additionalTags)]
|
||||
.filter(Boolean)
|
||||
.filter((tag) => ![NONE, ALL].includes(tag));
|
||||
}
|
||||
}
|
||||
|
||||
export function buildTagRoute(routeConfig = {}) {
|
||||
|
|
|
@ -6,5 +6,5 @@ import { buildTagRoute } from "discourse/routes/tag-show";
|
|||
// be handled by the intersection logic. Defining tags-intersection as something separate avoids
|
||||
// that confusion.
|
||||
export default class extends buildTagRoute() {
|
||||
controllerName = "tags.intersection";
|
||||
controllerName = "tags-intersection";
|
||||
}
|
||||
|
|
43
app/assets/javascripts/discourse/app/services/discovery.js
Normal file
43
app/assets/javascripts/discourse/app/services/discovery.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import Service, { inject as service } from "@ember/service";
|
||||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
|
||||
/**
|
||||
* The discovery service acts as a 'public API' for our discovery
|
||||
* routes. Themes/plugins can use this service as a stable way
|
||||
* to learn information about the current route.
|
||||
*/
|
||||
@disableImplicitInjections
|
||||
export default class DiscoveryService extends Service {
|
||||
@service router;
|
||||
|
||||
get onDiscoveryRoute() {
|
||||
const { currentRouteName } = this.router;
|
||||
return (
|
||||
currentRouteName?.startsWith("discovery.") ||
|
||||
currentRouteName?.startsWith("tags.show") ||
|
||||
currentRouteName === "tag.show"
|
||||
);
|
||||
}
|
||||
|
||||
get category() {
|
||||
if (this.onDiscoveryRoute) {
|
||||
return this.#routeAttrs.category;
|
||||
}
|
||||
}
|
||||
|
||||
get tag() {
|
||||
if (this.onDiscoveryRoute) {
|
||||
return this.#routeAttrs.tag;
|
||||
}
|
||||
}
|
||||
|
||||
get currentTopicList() {
|
||||
if (this.onDiscoveryRoute) {
|
||||
return this.#routeAttrs.list;
|
||||
}
|
||||
}
|
||||
|
||||
get #routeAttrs() {
|
||||
return this.router.currentRoute.attributes;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,14 @@
|
|||
<PluginOutlet
|
||||
<Discovery::Layout>
|
||||
<:navigation>
|
||||
<Discovery::Navigation
|
||||
@showCategoryAdmin={{this.model.can_create_category}}
|
||||
@canCreateTopic={{this.model.can_create_topic}}
|
||||
@createTopic={{this.createTopic}}
|
||||
@filterType="categories"
|
||||
/>
|
||||
</:navigation>
|
||||
<:list>
|
||||
<PluginOutlet
|
||||
@name="above-discovery-categories"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash
|
||||
|
@ -6,11 +16,15 @@
|
|||
categoryPageStyle=this.categoryPageStyle
|
||||
topics=this.model.topics
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
|
||||
<DiscoveryCategories @refresh={{action "refresh"}}>
|
||||
{{body-class "categories-list"}}
|
||||
|
||||
<div class="contents">
|
||||
{{#if (and this.topicTrackingState.hasIncoming this.isCategoriesRoute)}}
|
||||
<div class="show-more {{if this.hasTopics 'has-topics'}}">
|
||||
<div
|
||||
class={{concat-class "show-more" (if this.hasTopics "has-topics")}}
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
class="alert alert-info clickable"
|
||||
|
@ -25,24 +39,14 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if
|
||||
(eq this.categoryPageStyle "categories-and-latest-topics-created-date")
|
||||
}}
|
||||
<CategoriesAndLatestTopics
|
||||
<Discovery::CategoriesDisplay
|
||||
@categories={{this.model.categories}}
|
||||
@topics={{this.model.topics}}
|
||||
@parentCategory={{this.model.parentCategory}}
|
||||
/>
|
||||
{{else}}
|
||||
{{component
|
||||
this.categoryPageStyle
|
||||
categories=this.model.categories
|
||||
topics=this.model.topics
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
</DiscoveryCategories>
|
||||
|
||||
<PluginOutlet
|
||||
<PluginOutlet
|
||||
@name="below-discovery-categories"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash
|
||||
|
@ -50,4 +54,6 @@
|
|||
categoryPageStyle=this.categoryPageStyle
|
||||
topics=this.model.topics
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
</:list>
|
||||
</Discovery::Layout>
|
|
@ -0,0 +1,18 @@
|
|||
<Discovery::Layout>
|
||||
<:navigation>
|
||||
<Discovery::FilterNavigation
|
||||
@queryString={{this.q}}
|
||||
@updateTopicsListQueryParams={{this.updateTopicsListQueryParams}}
|
||||
/>
|
||||
</:navigation>
|
||||
<:list>
|
||||
<Discovery::Topics
|
||||
@period={{this.period}}
|
||||
@expandAllPinned={{this.expandAllPinned}}
|
||||
@expandAllGloballyPinned={{this.expandAllGloballyPinned}}
|
||||
@model={{this.model}}
|
||||
@canBulkSelect={{this.canBulkSelect}}
|
||||
@bulkSelectHelper={{this.bulkSelectHelper}}
|
||||
/>
|
||||
</:list>
|
||||
</Discovery::Layout>
|
|
@ -0,0 +1,49 @@
|
|||
<Discovery::Layout
|
||||
@category={{this.model.category}}
|
||||
@createTopicDisabled={{this.createTopicDisabled}}
|
||||
>
|
||||
<:navigation>
|
||||
<Discovery::Navigation
|
||||
@category={{this.model.category}}
|
||||
@tag={{this.model.tag}}
|
||||
@additionalTags={{this.model.additionalTags}}
|
||||
@filterType={{this.model.filterType}}
|
||||
@noSubcategories={{this.model.noSubcategories}}
|
||||
@canBulkSelect={{this.canBulkSelect}}
|
||||
@bulkSelectHelper={{this.bulkSelectHelper}}
|
||||
@createTopic={{this.createTopic}}
|
||||
@createTopicDisabled={{this.createTopicDisabled}}
|
||||
@canCreateTopicOnTag={{this.model.canCreateTopicOnTag}}
|
||||
@toggleTagInfo={{this.toggleTagInfo}}
|
||||
@tagNotification={{this.model.tagNotification}}
|
||||
/>
|
||||
</:navigation>
|
||||
|
||||
<:header>
|
||||
{{#if this.model.subcategoryList}}
|
||||
<Discovery::CategoriesDisplay
|
||||
@categories={{this.model.subcategoryList.categories}}
|
||||
@parentCategory={{this.model.subcategoryList.parentCategory}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.showTagInfo}}
|
||||
<TagInfo @tag={{this.model.tag}} @list={{this.model.list}} />
|
||||
{{/if}}
|
||||
</:header>
|
||||
|
||||
<:list>
|
||||
<Discovery::Topics
|
||||
@period={{this.model.list.for_period}}
|
||||
@changePeriod={{this.changePeriod}}
|
||||
@model={{this.model.list}}
|
||||
@canBulkSelect={{this.canBulkSelect}}
|
||||
@bulkSelectHelper={{this.bulkSelectHelper}}
|
||||
@showDismissRead={{this.showDismissRead}}
|
||||
@showResetNew={{this.showResetNew}}
|
||||
@category={{this.model.category}}
|
||||
@tag={{this.model.tag}}
|
||||
@changeSort={{this.changeSort}}
|
||||
@changeNewListSubset={{this.changeNewListSubset}}
|
||||
/>
|
||||
</:list>
|
||||
</Discovery::Layout>
|
|
@ -1,13 +0,0 @@
|
|||
{{body-class "navigation-categories"}}
|
||||
|
||||
<section class="navigation-container">
|
||||
<DNavigation
|
||||
@filterMode="categories"
|
||||
@showCategoryAdmin={{this.showCategoryAdmin}}
|
||||
@createCategory={{route-action "createCategory"}}
|
||||
@reorderCategories={{route-action "reorderCategories"}}
|
||||
@canCreateTopic={{this.canCreateTopic}}
|
||||
@hasDraft={{this.currentUser.has_topic_draft}}
|
||||
@createTopic={{fn this.composer.openNewTopic (hash preferDraft=true)}}
|
||||
/>
|
||||
</section>
|
|
@ -1,45 +0,0 @@
|
|||
<AddCategoryTagClasses @category={{this.category}} />
|
||||
|
||||
<PluginOutlet
|
||||
@name="above-category-heading"
|
||||
@outletArgs={{hash category=this.category}}
|
||||
/>
|
||||
|
||||
<section class="category-heading">
|
||||
{{#if this.category.uploaded_logo.url}}
|
||||
<CategoryLogo @category={{this.category}} />
|
||||
{{#if this.category.description}}
|
||||
<p>{{dir-span this.category.description htmlSafe="true"}}</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<span>
|
||||
<PluginOutlet
|
||||
@name="category-heading"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash category=this.category}}
|
||||
/>
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<section class="navigation-container category-navigation">
|
||||
<DNavigation
|
||||
@category={{this.category}}
|
||||
@filterMode={{this.filterMode}}
|
||||
@noSubcategories={{this.noSubcategories}}
|
||||
@canCreateTopic={{this.canCreateTopic}}
|
||||
@createTopic={{fn
|
||||
this.composer.openNewTopic
|
||||
(hash category=this.createTopicTargetCategory preferDraft=true)
|
||||
}}
|
||||
@createTopicDisabled={{not this.enableCreateTopicButton}}
|
||||
@hasDraft={{this.currentUser.has_topic_draft}}
|
||||
@editCategory={{route-action "editCategory" this.category}}
|
||||
/>
|
||||
|
||||
<PluginOutlet
|
||||
@name="category-navigation"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash category=this.category}}
|
||||
/>
|
||||
</section>
|
|
@ -1,11 +0,0 @@
|
|||
{{body-class "navigation-topics"}}
|
||||
|
||||
<section class="navigation-container">
|
||||
<DNavigation
|
||||
@filterMode={{this.filterMode}}
|
||||
@canCreateTopic={{this.canCreateTopic}}
|
||||
@hasDraft={{this.currentUser.has_topic_draft}}
|
||||
@createTopic={{fn this.composer.openNewTopic (hash preferDraft=true)}}
|
||||
@skipCategoriesNavItem={{this.skipCategoriesNavItem}}
|
||||
/>
|
||||
</section>
|
|
@ -1,189 +0,0 @@
|
|||
{{#if this.list.canLoadMore}}
|
||||
{{hide-application-footer}}
|
||||
{{/if}}
|
||||
|
||||
{{body-class
|
||||
"tags-page"
|
||||
(concat "tag-" this.tag.id)
|
||||
(if this.category.slug (concat "category-" this.category.slug))
|
||||
(if this.additionalTags "tags-intersection")
|
||||
}}
|
||||
|
||||
<div class="container">
|
||||
<DiscourseBanner @user={{this.currentUser}} @banner={{this.site.banner}} />
|
||||
</div>
|
||||
|
||||
<span>
|
||||
<PluginOutlet @name="discovery-list-controls-above" @connectorTagName="div" />
|
||||
</span>
|
||||
|
||||
<div class="list-controls">
|
||||
<PluginOutlet
|
||||
@name="discovery-navigation-bar-above"
|
||||
@connectorTagName="div"
|
||||
/>
|
||||
<div class="container">
|
||||
<section class="navigation-container tag-navigation">
|
||||
<DNavigation
|
||||
@filterMode={{this.filterMode}}
|
||||
@canCreateTopic={{this.canCreateTopic}}
|
||||
@hasDraft={{this.currentUser.has_topic_draft}}
|
||||
@createTopic={{route-action "createTopic"}}
|
||||
@category={{this.category}}
|
||||
@editCategory={{route-action "editCategory" this.category}}
|
||||
@tag={{this.tag}}
|
||||
@noSubcategories={{this.noSubcategories}}
|
||||
@tagNotification={{this.tagNotification}}
|
||||
@additionalTags={{this.additionalTags}}
|
||||
@showInfo={{this.showInfo}}
|
||||
@canCreateTopicOnTag={{this.canCreateTopicOnTag}}
|
||||
@createTopicDisabled={{this.createTopicDisabled}}
|
||||
@changeTagNotificationLevel={{action "changeTagNotificationLevel"}}
|
||||
@toggleInfo={{action "toggleInfo"}}
|
||||
/>
|
||||
|
||||
<PluginOutlet
|
||||
@name="tag-navigation"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash category=this.category tag=this.tag}}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.showInfo}}
|
||||
<TagInfo
|
||||
@tag={{this.tag}}
|
||||
@list={{this.list}}
|
||||
@deleteAction={{action "deleteTag"}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<span>
|
||||
<PluginOutlet
|
||||
@name="discovery-list-container-top"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash category=this.category}}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<TopicDismissButtons
|
||||
@position="top"
|
||||
@selectedTopics={{this.selected}}
|
||||
@model={{this.model}}
|
||||
@showResetNew={{this.showResetNew}}
|
||||
@showDismissRead={{this.showDismissRead}}
|
||||
@resetNew={{action "resetNew"}}
|
||||
/>
|
||||
|
||||
<span>
|
||||
<PluginOutlet @name="discovery-above" @connectorTagName="div" />
|
||||
</span>
|
||||
|
||||
<div class="container list-container">
|
||||
<div class="row">
|
||||
<div class="full-width">
|
||||
<PluginOutlet @name="before-list-area" />
|
||||
<div id="list-area">
|
||||
{{#unless this.loading}}
|
||||
<DiscoveryTopicsList
|
||||
@model={{this.list}}
|
||||
@refresh={{action "refresh"}}
|
||||
@autoAddTopicsToBulkSelect={{this.autoAddTopicsToBulkSelect}}
|
||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
||||
@addTopicsToBulkSelect={{action "addTopicsToBulkSelect"}}
|
||||
>
|
||||
{{#if this.top}}
|
||||
<div class="top-lists">
|
||||
<PeriodChooser
|
||||
@period={{this.period}}
|
||||
@action={{action "changePeriod"}}
|
||||
@fullDay={{false}}
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if this.topicTrackingState.hasIncoming}}
|
||||
<div class="show-more {{if this.hasTopics 'has-topics'}}">
|
||||
<a
|
||||
tabindex="0"
|
||||
href
|
||||
{{on "click" this.showInserted}}
|
||||
class="alert alert-info clickable"
|
||||
>
|
||||
<CountI18n
|
||||
@key="topic_count_"
|
||||
@suffix={{this.topicTrackingState.filter}}
|
||||
@count={{this.topicTrackingState.incomingCount}}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#unless this.bulkSelectEnabled}}
|
||||
{{#if (and this.showTopicsAndRepliesToggle this.site.mobileView)}}
|
||||
<NewListHeaderControlsWrapper
|
||||
@current={{this.subset}}
|
||||
@newRepliesCount={{this.newRepliesCount}}
|
||||
@newTopicsCount={{this.newTopicsCount}}
|
||||
@changeNewListSubset={{action "changeNewListSubset"}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
{{#if this.list.topics}}
|
||||
<TopicList
|
||||
@topics={{this.list.topics}}
|
||||
@canBulkSelect={{this.canBulkSelect}}
|
||||
@toggleBulkSelect={{action "toggleBulkSelect"}}
|
||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
||||
@bulkSelectAction={{action "refresh"}}
|
||||
@updateAutoAddTopicsToBulkSelect={{action
|
||||
"updateAutoAddTopicsToBulkSelect"
|
||||
}}
|
||||
@selected={{this.selected}}
|
||||
@category={{this.category}}
|
||||
@showPosters={{true}}
|
||||
@order={{this.order}}
|
||||
@ascending={{this.ascending}}
|
||||
@changeSort={{action "changeSort"}}
|
||||
@focusLastVisitedTopic={{true}}
|
||||
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
|
||||
@newListSubset={{this.subset}}
|
||||
@changeNewListSubset={{action "changeNewListSubset"}}
|
||||
@newRepliesCount={{this.newRepliesCount}}
|
||||
@newTopicsCount={{this.newTopicsCount}}
|
||||
/>
|
||||
{{/if}}
|
||||
</DiscoveryTopicsList>
|
||||
|
||||
<footer class="topic-list-bottom">
|
||||
<TopicDismissButtons
|
||||
@position="bottom"
|
||||
@selectedTopics={{this.selected}}
|
||||
@model={{this.model}}
|
||||
@showResetNew={{this.showResetNew}}
|
||||
@showDismissRead={{this.showDismissRead}}
|
||||
@resetNew={{action "resetNew"}}
|
||||
/>
|
||||
|
||||
{{#unless this.list.canLoadMore}}
|
||||
<FooterMessage
|
||||
@education={{this.footerEducation}}
|
||||
@message={{this.footerMessage}}
|
||||
>
|
||||
{{html-safe
|
||||
(i18n "topic.browse_all_tags_or_latest" basePath=(base-path))
|
||||
}}
|
||||
</FooterMessage>
|
||||
{{/unless}}
|
||||
</footer>
|
||||
{{/unless}}
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{this.list.loadingMore}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
<PluginOutlet @name="discovery-below" @connectorTagName="div" />
|
||||
</span>
|
|
@ -1,5 +1,5 @@
|
|||
<div class="container">
|
||||
<DiscourseBanner @user={{this.currentUser}} @banner={{this.site.banner}} />
|
||||
<DiscourseBanner />
|
||||
</div>
|
||||
|
||||
<div class="container tags-index">
|
||||
|
|
|
@ -19,8 +19,6 @@
|
|||
<AddTopicStatusClasses @topic={{this.model}} />
|
||||
<div class="container">
|
||||
<DiscourseBanner
|
||||
@user={{this.currentUser}}
|
||||
@banner={{this.site.banner}}
|
||||
@overlay={{this.hasScrolled}}
|
||||
@hide={{this.model.errorLoading}}
|
||||
/>
|
||||
|
|
|
@ -15,11 +15,15 @@
|
|||
>
|
||||
<TopicDismissButtons
|
||||
@position="top"
|
||||
@selectedTopics={{this.selected}}
|
||||
@selectedTopics={{this.bulkSelectHelper.selected}}
|
||||
@model={{this.model}}
|
||||
@showResetNew={{this.showResetNew}}
|
||||
@showDismissRead={{this.showDismissRead}}
|
||||
@resetNew={{action "resetNew"}}
|
||||
@dismissRead={{if
|
||||
this.showDismissRead
|
||||
(route-action "dismissReadTopics")
|
||||
}}
|
||||
/>
|
||||
|
||||
{{#if (gt this.incomingCount 0)}}
|
||||
|
@ -43,24 +47,22 @@
|
|||
@topicList={{this.model}}
|
||||
@hideCategory={{this.hideCategory}}
|
||||
@showPosters={{this.showPosters}}
|
||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
||||
@bulkSelectAction={{action "refresh"}}
|
||||
@selected={{this.selected}}
|
||||
@tagsForUser={{this.tagsForUser}}
|
||||
@canBulkSelect={{this.canBulkSelect}}
|
||||
@toggleBulkSelect={{action "toggleBulkSelect"}}
|
||||
@updateAutoAddTopicsToBulkSelect={{action
|
||||
"updateAutoAddTopicsToBulkSelect"
|
||||
}}
|
||||
@bulkSelectHelper={{this.bulkSelectHelper}}
|
||||
/>
|
||||
|
||||
<TopicDismissButtons
|
||||
@position="bottom"
|
||||
@selectedTopics={{this.selected}}
|
||||
@selectedTopics={{this.bulkSelectHelper.selected}}
|
||||
@model={{this.model}}
|
||||
@showResetNew={{this.showResetNew}}
|
||||
@showDismissRead={{this.showDismissRead}}
|
||||
@resetNew={{action "resetNew"}}
|
||||
@dismissRead={{if
|
||||
this.showDismissRead
|
||||
(route-action "dismissReadTopics")
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{this.model.loadingMore}} />
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<div class="navigation-controls">
|
||||
{{#if this.site.mobileView}}
|
||||
{{#if this.currentUser.admin}}
|
||||
<BulkSelectToggle @parentController="user-topics-list" />
|
||||
<BulkSelectToggle @bulkSelectHelper={{this.bulkSelectHelper}} />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -3,8 +3,6 @@ globalThis.deprecationWorkflow.config = {
|
|||
// We're using RAISE_ON_DEPRECATION in environment.js instead of
|
||||
// `throwOnUnhandled` here since it is easier to toggle.
|
||||
workflow: [
|
||||
{ handler: "silence", matchId: "route-render-template" },
|
||||
{ handler: "silence", matchId: "route-disconnect-outlet" },
|
||||
{
|
||||
handler: "silence",
|
||||
matchId: "ember-this-fallback.this-property-fallback",
|
||||
|
|
|
@ -543,7 +543,7 @@ acceptance("Tag info", function (needs) {
|
|||
await visit("/tag/planters");
|
||||
await click("#create-topic");
|
||||
let composer = this.owner.lookup("service:composer");
|
||||
assert.strictEqual(composer.get("model").tags, undefined);
|
||||
assert.deepEqual(composer.get("model").tags, []);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { click, currentURL, settled, visit } from "@ember/test-helpers";
|
||||
import { skip, test } from "qunit";
|
||||
import sinon from "sinon";
|
||||
import { configureEyeline } from "discourse/lib/eyeline";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { ScrollingDOMMethods } from "discourse/mixins/scrolling";
|
||||
import discoveryFixtures from "discourse/tests/fixtures/discovery-fixtures";
|
||||
import {
|
||||
|
@ -128,19 +126,13 @@ acceptance("Topic Discovery", function (needs) {
|
|||
});
|
||||
|
||||
test("Using period chooser when query params are present", async function (assert) {
|
||||
await visit("/top?f=foo&d=bar");
|
||||
|
||||
sinon.stub(DiscourseURL, "routeTo");
|
||||
await visit("/top?status=closed");
|
||||
|
||||
const periodChooser = selectKit(".period-chooser");
|
||||
|
||||
await periodChooser.expand();
|
||||
await periodChooser.selectRowByValue("yearly");
|
||||
|
||||
assert.ok(
|
||||
DiscourseURL.routeTo.calledWith("/top?f=foo&d=bar&period=yearly"),
|
||||
"it keeps the query params"
|
||||
);
|
||||
assert.strictEqual(currentURL(), "/top?period=yearly&status=closed");
|
||||
});
|
||||
|
||||
test("switching between tabs", async function (assert) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { click, currentURL, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import sinon from "sinon";
|
||||
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
|
||||
|
||||
acceptance("Category 404", function (needs) {
|
||||
|
@ -15,16 +14,10 @@ acceptance("Category 404", function (needs) {
|
|||
});
|
||||
|
||||
test("Navigating to a bad category link does not break the router", async function (assert) {
|
||||
// Don't log the XHR error
|
||||
const stub = sinon
|
||||
.stub(console, "error")
|
||||
.withArgs(sinon.match({ status: 404 }));
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await click('[data-for-test="category-404"]');
|
||||
assert.strictEqual(currentURL(), "/404");
|
||||
sinon.assert.calledOnce(stub);
|
||||
|
||||
// See that we can navigate away
|
||||
await click("#site-logo");
|
||||
|
|
|
@ -2,6 +2,7 @@ import { getOwner } from "@ember/application";
|
|||
import { click, render } from "@ember/test-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { module, test } from "qunit";
|
||||
import BulkSelectHelper from "discourse/lib/bulk-select-helper";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
|
||||
module("Integration | Component | topic-list", function (hooks) {
|
||||
|
@ -14,54 +15,47 @@ module("Integration | Component | topic-list", function (hooks) {
|
|||
store.createRecord("topic", { id: 24234 }),
|
||||
store.createRecord("topic", { id: 24235 }),
|
||||
],
|
||||
selected: [],
|
||||
bulkSelectEnabled: false,
|
||||
autoAddTopicsToBulkSelect: false,
|
||||
|
||||
toggleBulkSelect() {
|
||||
this.toggleProperty("bulkSelectEnabled");
|
||||
},
|
||||
|
||||
updateAutoAddTopicsToBulkSelect(newVal) {
|
||||
this.set("autoAddTopicsToBulkSelect", newVal);
|
||||
},
|
||||
bulkSelectHelper: new BulkSelectHelper(this),
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<TopicList
|
||||
@canBulkSelect={{true}}
|
||||
@toggleBulkSelect={{this.toggleBulkSelect}}
|
||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
||||
@autoAddTopicsToBulkSelect={{this.autoAddTopicsToBulkSelect}}
|
||||
@updateAutoAddTopicsToBulkSelect={{this.updateAutoAddTopicsToBulkSelect}}
|
||||
@bulkSelectHelper={{this.bulkSelectHelper}}
|
||||
@topics={{this.topics}}
|
||||
@selected={{this.selected}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.strictEqual(this.selected.length, 0, "defaults to 0");
|
||||
assert.strictEqual(
|
||||
this.bulkSelectHelper.selected.length,
|
||||
0,
|
||||
"defaults to 0"
|
||||
);
|
||||
await click("button.bulk-select");
|
||||
assert.ok(this.bulkSelectEnabled, "bulk select is enabled");
|
||||
assert.true(
|
||||
this.bulkSelectHelper.bulkSelectEnabled,
|
||||
"bulk select is enabled"
|
||||
);
|
||||
|
||||
await click("button.bulk-select-all");
|
||||
assert.strictEqual(
|
||||
this.selected.length,
|
||||
this.bulkSelectHelper.selected.length,
|
||||
2,
|
||||
"clicking Select All selects all loaded topics"
|
||||
);
|
||||
assert.ok(
|
||||
this.autoAddTopicsToBulkSelect,
|
||||
assert.true(
|
||||
this.bulkSelectHelper.autoAddTopicsToBulkSelect,
|
||||
"clicking Select All turns on the autoAddTopicsToBulkSelect flag"
|
||||
);
|
||||
|
||||
await click("button.bulk-clear-all");
|
||||
assert.strictEqual(
|
||||
this.selected.length,
|
||||
this.bulkSelectHelper.selected.length,
|
||||
0,
|
||||
"clicking Clear All deselects all topics"
|
||||
);
|
||||
assert.ok(
|
||||
!this.autoAddTopicsToBulkSelect,
|
||||
assert.false(
|
||||
this.bulkSelectHelper.autoAddTopicsToBulkSelect,
|
||||
"clicking Clear All turns off the autoAddTopicsToBulkSelect flag"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -38,7 +38,7 @@ class TopicQuery
|
|||
end
|
||||
|
||||
def self.public_valid_options
|
||||
# For these to work in Ember, add them to `controllers/discovery-sortable.js`
|
||||
# For these to work in Ember, add them to `controllers/discovery/list.js`
|
||||
@public_valid_options ||= %i[
|
||||
page
|
||||
before
|
||||
|
|
Loading…
Reference in New Issue
Block a user