mirror of
https://github.com/discourse/discourse.git
synced 2024-11-29 06:53:39 +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}}
|
@hideCategory={{this.hideCategory}}
|
||||||
@topics={{this.topics}}
|
@topics={{this.topics}}
|
||||||
@expandExcerpts={{this.expandExcerpts}}
|
@expandExcerpts={{this.expandExcerpts}}
|
||||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
@bulkSelectHelper={{this.bulkSelectHelper}}
|
||||||
@bulkSelectAction={{this.bulkSelectAction}}
|
|
||||||
@canBulkSelect={{this.canBulkSelect}}
|
@canBulkSelect={{this.canBulkSelect}}
|
||||||
@selected={{this.selected}}
|
|
||||||
@tagsForUser={{this.tagsForUser}}
|
@tagsForUser={{this.tagsForUser}}
|
||||||
@toggleBulkSelect={{this.toggleBulkSelect}}
|
|
||||||
@updateAutoAddTopicsToBulkSelect={{this.updateAutoAddTopicsToBulkSelect}}
|
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#unless this.loadingMore}}
|
{{#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}}
|
{{/unless}}
|
||||||
|
|
||||||
<div class="navigation-controls">
|
<div class="navigation-controls">
|
||||||
{{#if (and this.notCategoriesRoute this.site.mobileView this.canBulk)}}
|
{{#if (and this.notCategoriesRoute this.site.mobileView @canBulkSelect)}}
|
||||||
<BulkSelectToggle @parentController="discovery/topics" />
|
<BulkSelectToggle @bulkSelectHelper={{@bulkSelectHelper}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.showCategoryAdmin}}
|
{{#if this.showCategoryAdmin}}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { getOwner } from "@ember/application";
|
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
|
import { action } from "@ember/object";
|
||||||
import { dependentKeyCompat } from "@ember/object/compat";
|
import { dependentKeyCompat } from "@ember/object/compat";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
|
@ -142,10 +142,23 @@ export default Component.extend({
|
||||||
return filterType !== "categories";
|
return filterType !== "categories";
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed()
|
@action
|
||||||
canBulk() {
|
async changeTagNotificationLevel(notificationLevel) {
|
||||||
const controller = getOwner(this).lookup("controller:discovery/topics");
|
const response = await this.tagNotification.update({
|
||||||
return controller.canBulkSelect;
|
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: {
|
actions: {
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
import { readOnly } from "@ember/object/computed";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
hide: false,
|
hide: false,
|
||||||
|
|
||||||
|
banner: readOnly("site.banner"),
|
||||||
|
|
||||||
@discourseComputed("banner.html")
|
@discourseComputed("banner.html")
|
||||||
content(bannerHtml) {
|
content(bannerHtml) {
|
||||||
const newDiv = document.createElement("div");
|
const newDiv = document.createElement("div");
|
||||||
|
@ -15,7 +18,7 @@ export default Component.extend({
|
||||||
return newDiv.innerHTML;
|
return newDiv.innerHTML;
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed("user.dismissed_banner_key", "banner.key", "hide")
|
@discourseComputed("currentUser.dismissed_banner_key", "banner.key", "hide")
|
||||||
visible(dismissedBannerKey, bannerKey, hide) {
|
visible(dismissedBannerKey, bannerKey, hide) {
|
||||||
dismissedBannerKey =
|
dismissedBannerKey =
|
||||||
dismissedBannerKey || this.keyValueStore.get("dismissed_banner_key");
|
dismissedBannerKey || this.keyValueStore.get("dismissed_banner_key");
|
||||||
|
@ -32,8 +35,8 @@ export default Component.extend({
|
||||||
|
|
||||||
@action
|
@action
|
||||||
dismiss() {
|
dismiss() {
|
||||||
if (this.user) {
|
if (this.currentUser) {
|
||||||
this.user.dismissBanner(this.get("banner.key"));
|
this.currentUser.dismissBanner(this.get("banner.key"));
|
||||||
} else {
|
} else {
|
||||||
this.set("hide", true);
|
this.set("hide", true);
|
||||||
this.keyValueStore.set({
|
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 { inject as service } from "@ember/service";
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import LoadMore from "discourse/mixins/load-more";
|
import LoadMore from "discourse/mixins/load-more";
|
||||||
import UrlRefresh from "discourse/mixins/url-refresh";
|
|
||||||
import { observes, on } from "discourse-common/utils/decorators";
|
import { observes, on } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default Component.extend(UrlRefresh, LoadMore, {
|
export default Component.extend(LoadMore, {
|
||||||
classNames: ["contents"],
|
classNames: ["contents"],
|
||||||
eyelineSelector: ".topic-list-item",
|
eyelineSelector: ".topic-list-item",
|
||||||
documentTitle: service(),
|
documentTitle: service(),
|
||||||
|
@ -40,17 +39,13 @@ export default Component.extend(UrlRefresh, LoadMore, {
|
||||||
if (
|
if (
|
||||||
newTopics &&
|
newTopics &&
|
||||||
newTopics.length &&
|
newTopics.length &&
|
||||||
this.autoAddTopicsToBulkSelect &&
|
this.bulkSelectHelper?.bulkSelectEnabled
|
||||||
this.bulkSelectEnabled
|
|
||||||
) {
|
) {
|
||||||
this.addTopicsToBulkSelect(newTopics);
|
this.bulkSelectHelper.addTopics(newTopics);
|
||||||
}
|
}
|
||||||
if (moreTopicsUrl && $(window).height() >= $(document).height()) {
|
if (moreTopicsUrl && $(window).height() >= $(document).height()) {
|
||||||
this.send("loadMore");
|
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
|
<Input
|
||||||
class="topic-query-filter__filter-term"
|
class="topic-query-filter__filter-term"
|
||||||
@value={{this.newQueryString}}
|
@value={{this.newQueryString}}
|
||||||
@enter={{action
|
@enter={{action @updateTopicsListQueryParams this.newQueryString}}
|
||||||
this.discoveryFilter.updateTopicsListQueryParams
|
|
||||||
this.newQueryString
|
|
||||||
}}
|
|
||||||
@type="text"
|
@type="text"
|
||||||
id="queryStringInput"
|
id="queryStringInput"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
|
@ -1,15 +1,14 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import Controller, { inject as controller } from "@ember/controller";
|
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
import { resettableTracked } from "discourse/lib/tracked-tools";
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default class NavigationFilterController extends Controller {
|
export default class DiscoveryFilterNavigation extends Component {
|
||||||
@controller("discovery/filter") discoveryFilter;
|
|
||||||
|
|
||||||
@tracked copyIcon = "link";
|
@tracked copyIcon = "link";
|
||||||
@tracked copyClass = "btn-default";
|
@tracked copyClass = "btn-default";
|
||||||
@tracked newQueryString = "";
|
@resettableTracked newQueryString = this.args.queryString;
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
updateQueryString(string) {
|
updateQueryString(string) {
|
||||||
|
@ -19,7 +18,7 @@ export default class NavigationFilterController extends Controller {
|
||||||
@action
|
@action
|
||||||
clearInput() {
|
clearInput() {
|
||||||
this.newQueryString = "";
|
this.newQueryString = "";
|
||||||
this.discoveryFilter.updateTopicsListQueryParams(this.newQueryString);
|
this.args.updateTopicsListQueryParams(this.newQueryString);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
|
@ -1,11 +1,11 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<DiscourseBanner @user={{this.currentUser}} @banner={{this.site.banner}} />
|
<DiscourseBanner />
|
||||||
{{#unless this.viewingCategoriesList}}
|
{{#if @category}}
|
||||||
<CategoryReadOnlyBanner
|
<CategoryReadOnlyBanner
|
||||||
@category={{this.category}}
|
@category={{@category}}
|
||||||
@readOnly={{not this.navigationCategory.enableCreateTopicButton}}
|
@readOnly={{@createTopicDisabled}}
|
||||||
/>
|
/>
|
||||||
{{/unless}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
|
@ -18,28 +18,22 @@
|
||||||
@connectorTagName="div"
|
@connectorTagName="div"
|
||||||
/>
|
/>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{{outlet "navigation-bar"}}
|
{{yield to="navigation"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConditionalLoadingSpinner @condition={{this.showLoadingSpinner}} />
|
|
||||||
|
|
||||||
{{#if this.loading}}
|
|
||||||
{{hide-application-footer}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<PluginOutlet @name="discovery-above" @connectorTagName="div" />
|
<PluginOutlet @name="discovery-above" @connectorTagName="div" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="container list-container {{if this.showLoadingSpinner 'hidden'}}">
|
<div class="container list-container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="full-width">
|
<div class="full-width">
|
||||||
<div id="header-list-area">
|
<div id="header-list-area">
|
||||||
{{outlet "header-list-container"}}
|
{{yield to="header"}}
|
||||||
<PluginOutlet
|
<PluginOutlet
|
||||||
@name="header-list-container-bottom"
|
@name="header-list-container-bottom"
|
||||||
@outletArgs={{hash category=this.category}}
|
@outletArgs={{hash category=@category}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,9 +45,9 @@
|
||||||
<PluginOutlet
|
<PluginOutlet
|
||||||
@name="discovery-list-container-top"
|
@name="discovery-list-container-top"
|
||||||
@connectorTagName="span"
|
@connectorTagName="span"
|
||||||
@outletArgs={{hash category=this.category listLoading=this.loading}}
|
@outletArgs={{hash category=@category}}
|
||||||
/>
|
/>
|
||||||
{{outlet "list-container"}}
|
{{yield to="list"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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}}
|
{{hide-application-footer}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
@ -8,39 +8,36 @@
|
||||||
|
|
||||||
<TopicDismissButtons
|
<TopicDismissButtons
|
||||||
@position="top"
|
@position="top"
|
||||||
@selectedTopics={{this.selected}}
|
@selectedTopics={{@bulkSelectHelper.selected}}
|
||||||
@model={{this.model}}
|
@model={{@model}}
|
||||||
@showResetNew={{this.showResetNew}}
|
@showResetNew={{@showResetNew}}
|
||||||
@showDismissRead={{this.showDismissRead}}
|
@showDismissRead={{@showDismissRead}}
|
||||||
@resetNew={{action "resetNew"}}
|
@resetNew={{this.resetNew}}
|
||||||
|
@dismissRead={{this.dismissRead}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{{#if this.model.sharedDrafts}}
|
{{#if @model.sharedDrafts}}
|
||||||
<TopicList
|
<TopicList
|
||||||
@class="shared-drafts"
|
@class="shared-drafts"
|
||||||
@listTitle="shared_drafts.title"
|
@listTitle="shared_drafts.title"
|
||||||
@top={{this.top}}
|
@top={{this.top}}
|
||||||
@hideCategory="true"
|
@hideCategory="true"
|
||||||
@category={{this.category}}
|
@category={{@category}}
|
||||||
@topics={{this.model.sharedDrafts}}
|
@topics={{@model.sharedDrafts}}
|
||||||
@discoveryList={{true}}
|
@discoveryList={{true}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<DiscoveryTopicsList
|
<DiscoveryTopicsList
|
||||||
@model={{this.model}}
|
@model={{@model}}
|
||||||
@refresh={{action "refresh"}}
|
|
||||||
@loadingComplete={{action "loadingComplete"}}
|
|
||||||
@incomingCount={{this.topicTrackingState.incomingCount}}
|
@incomingCount={{this.topicTrackingState.incomingCount}}
|
||||||
@autoAddTopicsToBulkSelect={{this.autoAddTopicsToBulkSelect}}
|
@bulkSelectHelper={{@bulkSelectHelper}}
|
||||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
|
||||||
@addTopicsToBulkSelect={{action "addTopicsToBulkSelect"}}
|
|
||||||
>
|
>
|
||||||
{{#if this.top}}
|
{{#if this.top}}
|
||||||
<div class="top-lists">
|
<div class="top-lists">
|
||||||
<PeriodChooser
|
<PeriodChooser
|
||||||
@period={{this.period}}
|
@period={{@period}}
|
||||||
@action={{action "changePeriod"}}
|
@action={{@changePeriod}}
|
||||||
@fullDay={{false}}
|
@fullDay={{false}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -65,10 +62,10 @@
|
||||||
|
|
||||||
{{#if this.renderNewListHeaderControls}}
|
{{#if this.renderNewListHeaderControls}}
|
||||||
<NewListHeaderControlsWrapper
|
<NewListHeaderControlsWrapper
|
||||||
@current={{this.model.params.subset}}
|
@current={{@model.params.subset}}
|
||||||
@newRepliesCount={{this.newRepliesCount}}
|
@newRepliesCount={{this.newRepliesCount}}
|
||||||
@newTopicsCount={{this.newTopicsCount}}
|
@newTopicsCount={{this.newTopicsCount}}
|
||||||
@changeNewListSubset={{route-action "changeNewListSubset"}}
|
@changeNewListSubset={{@changeNewListSubset}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
@ -76,7 +73,7 @@
|
||||||
<PluginOutlet
|
<PluginOutlet
|
||||||
@name="before-topic-list"
|
@name="before-topic-list"
|
||||||
@connectorTagName="div"
|
@connectorTagName="div"
|
||||||
@outletArgs={{hash category=this.category}}
|
@outletArgs={{hash category=@category tag=@tag}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
@ -86,27 +83,21 @@
|
||||||
@top={{this.top}}
|
@top={{this.top}}
|
||||||
@showTopicPostBadges={{this.showTopicPostBadges}}
|
@showTopicPostBadges={{this.showTopicPostBadges}}
|
||||||
@showPosters={{true}}
|
@showPosters={{true}}
|
||||||
@canBulkSelect={{this.canBulkSelect}}
|
@canBulkSelect={{@canBulkSelect}}
|
||||||
@changeSort={{route-action "changeSort"}}
|
@bulkSelectHelper={{@bulkSelectHelper}}
|
||||||
@toggleBulkSelect={{action "toggleBulkSelect"}}
|
@changeSort={{@changeSort}}
|
||||||
@updateAutoAddTopicsToBulkSelect={{action
|
@hideCategory={{@model.hideCategory}}
|
||||||
"updateAutoAddTopicsToBulkSelect"
|
|
||||||
}}
|
|
||||||
@hideCategory={{this.model.hideCategory}}
|
|
||||||
@order={{this.order}}
|
@order={{this.order}}
|
||||||
@ascending={{this.ascending}}
|
@ascending={{this.ascending}}
|
||||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
|
||||||
@bulkSelectAction={{action "refresh"}}
|
|
||||||
@selected={{this.selected}}
|
|
||||||
@expandGloballyPinned={{this.expandGloballyPinned}}
|
@expandGloballyPinned={{this.expandGloballyPinned}}
|
||||||
@expandAllPinned={{this.expandAllPinned}}
|
@expandAllPinned={{this.expandAllPinned}}
|
||||||
@category={{this.category}}
|
@category={{@category}}
|
||||||
@topics={{this.model.topics}}
|
@topics={{@model.topics}}
|
||||||
@discoveryList={{true}}
|
@discoveryList={{true}}
|
||||||
@focusLastVisitedTopic={{true}}
|
@focusLastVisitedTopic={{true}}
|
||||||
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
|
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
|
||||||
@newListSubset={{this.model.params.subset}}
|
@newListSubset={{@model.params.subset}}
|
||||||
@changeNewListSubset={{route-action "changeNewListSubset"}}
|
@changeNewListSubset={{@changeNewListSubset}}
|
||||||
@newRepliesCount={{this.newRepliesCount}}
|
@newRepliesCount={{this.newRepliesCount}}
|
||||||
@newTopicsCount={{this.newTopicsCount}}
|
@newTopicsCount={{this.newTopicsCount}}
|
||||||
/>
|
/>
|
||||||
|
@ -115,33 +106,38 @@
|
||||||
<PluginOutlet
|
<PluginOutlet
|
||||||
@name="after-topic-list"
|
@name="after-topic-list"
|
||||||
@connectorTagName="div"
|
@connectorTagName="div"
|
||||||
@outletArgs={{hash category=this.category}}
|
@outletArgs={{hash category=@category tag=@tag}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</DiscoveryTopicsList>
|
</DiscoveryTopicsList>
|
||||||
|
|
||||||
<footer class="topic-list-bottom">
|
<footer class="topic-list-bottom">
|
||||||
<ConditionalLoadingSpinner @condition={{this.model.loadingMore}} />
|
<ConditionalLoadingSpinner @condition={{@model.loadingMore}} />
|
||||||
{{#if this.allLoaded}}
|
{{#if this.allLoaded}}
|
||||||
<TopicDismissButtons
|
<TopicDismissButtons
|
||||||
@position="bottom"
|
@position="bottom"
|
||||||
@selectedTopics={{this.selected}}
|
@selectedTopics={{@bulkSelectHelper.selected}}
|
||||||
@model={{this.model}}
|
@model={{@model}}
|
||||||
@showResetNew={{this.showResetNew}}
|
@showResetNew={{@showResetNew}}
|
||||||
@showDismissRead={{this.showDismissRead}}
|
@showDismissRead={{@showDismissRead}}
|
||||||
@resetNew={{action "resetNew"}}
|
@resetNew={{this.resetNew}}
|
||||||
|
@dismissRead={{this.dismissRead}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FooterMessage
|
<FooterMessage
|
||||||
@education={{this.footerEducation}}
|
@education={{this.footerEducation}}
|
||||||
@message={{this.footerMessage}}
|
@message={{this.footerMessage}}
|
||||||
>
|
>
|
||||||
{{#if this.latest}}
|
{{#if @tag}}
|
||||||
{{#if this.category.canCreateTopic}}
|
{{html-safe
|
||||||
|
(i18n "topic.browse_all_tags_or_latest" basePath=(base-path))
|
||||||
|
}}
|
||||||
|
{{else if this.latest}}
|
||||||
|
{{#if @category.canCreateTopic}}
|
||||||
<DiscourseLinkedText
|
<DiscourseLinkedText
|
||||||
@action={{fn
|
@action={{fn
|
||||||
this.composer.openNewTopic
|
this.composer.openNewTopic
|
||||||
(hash category=this.category preferDraft=true)
|
(hash category=@category preferDraft=true)
|
||||||
}}
|
}}
|
||||||
@text="topic.suggest_create_topic"
|
@text="topic.suggest_create_topic"
|
||||||
/>
|
/>
|
||||||
|
@ -152,10 +148,7 @@
|
||||||
"topic.browse_all_categories_latest_or_top" basePath=(base-path)
|
"topic.browse_all_categories_latest_or_top" basePath=(base-path)
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
<TopPeriodButtons
|
<TopPeriodButtons @period={{@period}} @action={{@changePeriod}} />
|
||||||
@period={{this.period}}
|
|
||||||
@action={{action "changePeriod"}}
|
|
||||||
/>
|
|
||||||
{{else}}
|
{{else}}
|
||||||
{{html-safe
|
{{html-safe
|
||||||
(i18n "topic.browse_all_categories_latest" basePath=(base-path))
|
(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>
|
<:footer>
|
||||||
<DButton
|
<DButton
|
||||||
class="btn-primary"
|
class="btn-primary"
|
||||||
@action={{route-action "dismissReadTopics" this.dismissTopics}}
|
@action={{fn @model.dismissRead this.dismissTopics}}
|
||||||
@icon="check"
|
@icon="check"
|
||||||
id="dismiss-read-confirm"
|
id="dismiss-read-confirm"
|
||||||
@label="topics.bulk.dismiss"
|
@label="topics.bulk.dismiss"
|
||||||
|
|
|
@ -146,7 +146,7 @@
|
||||||
id="edit-synonyms"
|
id="edit-synonyms"
|
||||||
class="btn-default"
|
class="btn-default"
|
||||||
/>
|
/>
|
||||||
{{#if this.deleteAction}}
|
{{#if this.canAdminTag}}
|
||||||
<DButton
|
<DButton
|
||||||
@action={{action "deleteTag"}}
|
@action={{action "deleteTag"}}
|
||||||
@icon="far-trash-alt"
|
@icon="far-trash-alt"
|
||||||
|
|
|
@ -140,8 +140,35 @@ export default Component.extend({
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
deleteTag() {
|
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() {
|
addSynonyms() {
|
||||||
|
|
|
@ -72,6 +72,7 @@ export default Component.extend({
|
||||||
model: {
|
model: {
|
||||||
title: dismissTitle,
|
title: dismissTitle,
|
||||||
count: this.selectedTopics.length,
|
count: this.selectedTopics.length,
|
||||||
|
dismissRead: this.dismissRead,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Component from "@ember/component";
|
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 { on } from "@ember/object/evented";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import LoadMore from "discourse/mixins/load-more";
|
import LoadMore from "discourse/mixins/load-more";
|
||||||
|
@ -8,13 +9,19 @@ import TopicBulkActions from "./modal/topic-bulk-actions";
|
||||||
|
|
||||||
export default Component.extend(LoadMore, {
|
export default Component.extend(LoadMore, {
|
||||||
modal: service(),
|
modal: service(),
|
||||||
|
router: service(),
|
||||||
|
|
||||||
tagName: "table",
|
tagName: "table",
|
||||||
classNames: ["topic-list"],
|
classNames: ["topic-list"],
|
||||||
classNameBindings: ["bulkSelectEnabled:sticky-header"],
|
classNameBindings: ["bulkSelectEnabled:sticky-header"],
|
||||||
showTopicPostBadges: true,
|
showTopicPostBadges: true,
|
||||||
listTitle: "topic.title",
|
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
|
// Overwrite this to perform client side filtering of topics, if desired
|
||||||
filteredTopics: alias("topics"),
|
filteredTopics: alias("topics"),
|
||||||
|
@ -26,9 +33,17 @@ export default Component.extend(LoadMore, {
|
||||||
this.refreshLastVisited();
|
this.refreshLastVisited();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@discourseComputed("bulkSelectEnabled")
|
get selected() {
|
||||||
toggleInTitle(bulkSelectEnabled) {
|
return this.bulkSelectHelper?.selected;
|
||||||
return !bulkSelectEnabled && this.canBulkSelect;
|
},
|
||||||
|
|
||||||
|
@dependentKeyCompat // for the classNameBindings
|
||||||
|
get bulkSelectEnabled() {
|
||||||
|
return this.bulkSelectHelper?.bulkSelectEnabled;
|
||||||
|
},
|
||||||
|
|
||||||
|
get toggleInTitle() {
|
||||||
|
return !this.bulkSelectHelper?.bulkSelectEnabled && this.canBulkSelect;
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed
|
@discourseComputed
|
||||||
|
@ -138,10 +153,6 @@ export default Component.extend(LoadMore, {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateAutoAddTopicsToBulkSelect(newVal) {
|
|
||||||
this.set("autoAddTopicsToBulkSelect", newVal);
|
|
||||||
},
|
|
||||||
|
|
||||||
click(e) {
|
click(e) {
|
||||||
const onClick = (sel, callback) => {
|
const onClick = (sel, callback) => {
|
||||||
let target = e.target.closest(sel);
|
let target = e.target.closest(sel);
|
||||||
|
@ -152,19 +163,19 @@ export default Component.extend(LoadMore, {
|
||||||
};
|
};
|
||||||
|
|
||||||
onClick("button.bulk-select", () => {
|
onClick("button.bulk-select", () => {
|
||||||
this.toggleBulkSelect();
|
this.bulkSelectHelper.toggleBulkSelect();
|
||||||
this.rerender();
|
this.rerender();
|
||||||
});
|
});
|
||||||
|
|
||||||
onClick("button.bulk-select-all", () => {
|
onClick("button.bulk-select-all", () => {
|
||||||
this.updateAutoAddTopicsToBulkSelect(true);
|
this.bulkSelectHelper.autoAddTopicsToBulkSelect = true;
|
||||||
document
|
document
|
||||||
.querySelectorAll("input.bulk-select:not(:checked)")
|
.querySelectorAll("input.bulk-select:not(:checked)")
|
||||||
.forEach((el) => el.click());
|
.forEach((el) => el.click());
|
||||||
});
|
});
|
||||||
|
|
||||||
onClick("button.bulk-clear-all", () => {
|
onClick("button.bulk-clear-all", () => {
|
||||||
this.updateAutoAddTopicsToBulkSelect(false);
|
this.bulkSelectHelper.autoAddTopicsToBulkSelect = false;
|
||||||
document
|
document
|
||||||
.querySelectorAll("input.bulk-select:checked")
|
.querySelectorAll("input.bulk-select:checked")
|
||||||
.forEach((el) => el.click());
|
.forEach((el) => el.click());
|
||||||
|
@ -178,9 +189,9 @@ export default Component.extend(LoadMore, {
|
||||||
onClick("button.bulk-select-actions", () => {
|
onClick("button.bulk-select-actions", () => {
|
||||||
this.modal.show(TopicBulkActions, {
|
this.modal.show(TopicBulkActions, {
|
||||||
model: {
|
model: {
|
||||||
topics: this.selected,
|
topics: this.bulkSelectHelper.selected,
|
||||||
category: this.category,
|
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 { action } from "@ember/object";
|
||||||
import { reads } from "@ember/object/computed";
|
import { reads } from "@ember/object/computed";
|
||||||
import { dasherize } from "@ember/string";
|
import { inject as service } from "@ember/service";
|
||||||
import DiscoveryController from "discourse/controllers/discovery";
|
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
const subcategoryStyleComponentNames = {
|
export default class CategoriesController extends Controller {
|
||||||
rows: "categories_only",
|
@service router;
|
||||||
rows_with_featured_topics: "categories_with_featured_topics",
|
@service composer;
|
||||||
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;
|
|
||||||
|
|
||||||
@reads("currentUser.staff") canEdit;
|
@reads("currentUser.staff") canEdit;
|
||||||
|
|
||||||
|
@ -30,30 +15,6 @@ export default class CategoriesController extends DiscoveryController {
|
||||||
return this.router.currentRouteName === "discovery.categories";
|
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
|
@action
|
||||||
showInserted(event) {
|
showInserted(event) {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
|
@ -63,6 +24,13 @@ export default class CategoriesController extends DiscoveryController {
|
||||||
tracker.resetTracking();
|
tracker.resetTracking();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
createTopic() {
|
||||||
|
this.composer.openNewTopic({
|
||||||
|
preferDraft: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
refresh() {
|
refresh() {
|
||||||
this.send("triggerRefresh");
|
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 DiscoveryListController, {
|
||||||
import TagShowController from "discourse/controllers/tag-show";
|
queryParams,
|
||||||
|
} from "discourse/controllers/discovery/list";
|
||||||
|
|
||||||
export default class TagsIntersectionController extends TagShowController {
|
export default class TagsIntersectionController extends DiscoveryListController {
|
||||||
queryParams = [...Object.keys(queryParams), { categoryParam: "category" }];
|
queryParams = [...Object.keys(queryParams), { categoryParam: "category" }];
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ export function resetCustomUserNavMessagesDropdownRows() {
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
@service router;
|
@service router;
|
||||||
@controller user;
|
@controller user;
|
||||||
|
@controller userTopicsList;
|
||||||
|
|
||||||
@tracked group;
|
@tracked group;
|
||||||
@tracked tagId;
|
@tracked tagId;
|
||||||
|
@ -38,6 +39,10 @@ export default class extends Controller {
|
||||||
@readOnly("router.currentRoute.parent.name") currentParentRouteName;
|
@readOnly("router.currentRoute.parent.name") currentParentRouteName;
|
||||||
@readOnly("site.can_tag_pms") pmTaggingEnabled;
|
@readOnly("site.can_tag_pms") pmTaggingEnabled;
|
||||||
|
|
||||||
|
get bulkSelectHelper() {
|
||||||
|
this.userTopicsList.bulkSelectHelper;
|
||||||
|
}
|
||||||
|
|
||||||
get messagesDropdownValue() {
|
get messagesDropdownValue() {
|
||||||
let value;
|
let value;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { dasherize } from "@ember/string";
|
import { dasherize } from "@ember/string";
|
||||||
import DiscoverySortableController from "discourse/controllers/discovery-sortable";
|
|
||||||
import Site from "discourse/models/site";
|
import Site from "discourse/models/site";
|
||||||
import buildCategoryRoute from "discourse/routes/build-category-route";
|
import buildCategoryRoute from "discourse/routes/build-category-route";
|
||||||
import buildTopicRoute from "discourse/routes/build-topic-route";
|
import buildTopicRoute from "discourse/routes/build-topic-route";
|
||||||
|
@ -9,19 +8,6 @@ export default {
|
||||||
after: "inject-discourse-objects",
|
after: "inject-discourse-objects",
|
||||||
|
|
||||||
initialize(app) {
|
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(
|
app.register(
|
||||||
"route:discovery.category",
|
"route:discovery.category",
|
||||||
buildCategoryRoute({ filter: "default" })
|
buildCategoryRoute({ filter: "default" })
|
||||||
|
@ -38,18 +24,6 @@ export default {
|
||||||
const site = Site.current();
|
const site = Site.current();
|
||||||
site.get("filters").forEach((filter) => {
|
site.get("filters").forEach((filter) => {
|
||||||
const filterDasherized = dasherize(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(
|
app.register(
|
||||||
`route:discovery.${filterDasherized}`,
|
`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 { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||||
import { NotificationLevels } from "discourse/lib/notification-levels";
|
import { NotificationLevels } from "discourse/lib/notification-levels";
|
||||||
import Topic from "discourse/models/topic";
|
import Topic from "discourse/models/topic";
|
||||||
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default class BulkSelectHelper {
|
export default class BulkSelectHelper {
|
||||||
@service router;
|
@service router;
|
||||||
|
@ -28,6 +29,7 @@ export default class BulkSelectHelper {
|
||||||
this.selected.concat(topics);
|
this.selected.concat(topics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
toggleBulkSelect() {
|
toggleBulkSelect() {
|
||||||
this.bulkSelectEnabled = !this.bulkSelectEnabled;
|
this.bulkSelectEnabled = !this.bulkSelectEnabled;
|
||||||
this.clear();
|
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 { 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 { addTopicTitleDecorator } from "discourse/components/topic-title";
|
||||||
import { addUserMenuProfileTabItem } from "discourse/components/user-menu/profile-tab-content";
|
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 { registerFullPageSearchType } from "discourse/controllers/full-page-search";
|
||||||
import { registerCustomPostMessageCallback as registerCustomPostMessageCallback1 } from "discourse/controllers/topic";
|
import { registerCustomPostMessageCallback as registerCustomPostMessageCallback1 } from "discourse/controllers/topic";
|
||||||
import { registerCustomUserNavMessagesDropdownRow } from "discourse/controllers/user-private-messages";
|
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 (oldPath === path) {
|
||||||
// If navigating to the same path send an app event.
|
// If navigating to the same path send an app event.
|
||||||
// Views can watch it and tell their controllers to refresh
|
// 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
|
// TODO: Extract into rules we can inject into the URL handler
|
||||||
|
@ -400,7 +400,7 @@ const DiscourseURL = EmberObject.extend({
|
||||||
(path === "/" || path === "/" + homepage) &&
|
(path === "/" || path === "/" + homepage) &&
|
||||||
(oldPath === "/" || oldPath === "/" + homepage)
|
(oldPath === "/" || oldPath === "/" + homepage)
|
||||||
) {
|
) {
|
||||||
this.appEvents.trigger("url:refresh");
|
this.routerService.refresh();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,6 +421,10 @@ const DiscourseURL = EmberObject.extend({
|
||||||
return this.container.lookup("router:main");
|
return this.container.lookup("router:main");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get routerService() {
|
||||||
|
return this.container.lookup("service:router");
|
||||||
|
},
|
||||||
|
|
||||||
get appEvents() {
|
get appEvents() {
|
||||||
return this.container.lookup("service:app-events");
|
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 ArrayProxy from "@ember/array/proxy";
|
||||||
|
import EmberObject from "@ember/object";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { number } from "discourse/lib/formatter";
|
import { number } from "discourse/lib/formatter";
|
||||||
import PreloadStore from "discourse/lib/preload-store";
|
import PreloadStore from "discourse/lib/preload-store";
|
||||||
|
@ -103,7 +104,7 @@ CategoryList.reopenClass({
|
||||||
return ajax(
|
return ajax(
|
||||||
`/categories.json?parent_category_id=${category.get("id")}`
|
`/categories.json?parent_category_id=${category.get("id")}`
|
||||||
).then((result) => {
|
).then((result) => {
|
||||||
return CategoryList.create({
|
return EmberObject.create({
|
||||||
categories: this.categoriesFrom(store, result),
|
categories: this.categoriesFrom(store, result),
|
||||||
parentCategory: category,
|
parentCategory: category,
|
||||||
});
|
});
|
||||||
|
|
|
@ -791,7 +791,7 @@ const Composer = RestModel.extend({
|
||||||
composerTotalOpened: opts.composerTime,
|
composerTotalOpened: opts.composerTime,
|
||||||
typingTime: opts.typingTime,
|
typingTime: opts.typingTime,
|
||||||
whisper: opts.whisper,
|
whisper: opts.whisper,
|
||||||
tags: opts.tags,
|
tags: opts.tags || [],
|
||||||
noBump: opts.noBump,
|
noBump: opts.noBump,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { all, Promise } from "rsvp";
|
import { queryParams, resetParams } from "discourse/controllers/discovery/list";
|
||||||
import {
|
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||||
changeNewListSubset,
|
|
||||||
changeSort,
|
|
||||||
queryParams,
|
|
||||||
resetParams,
|
|
||||||
} from "discourse/controllers/discovery-sortable";
|
|
||||||
import PreloadStore from "discourse/lib/preload-store";
|
import PreloadStore from "discourse/lib/preload-store";
|
||||||
|
import { setTopicList } from "discourse/lib/topic-list-tracker";
|
||||||
import Category from "discourse/models/category";
|
import Category from "discourse/models/category";
|
||||||
import CategoryList from "discourse/models/category-list";
|
import CategoryList from "discourse/models/category-list";
|
||||||
import TopicList from "discourse/models/topic-list";
|
import TopicList from "discourse/models/topic-list";
|
||||||
|
@ -18,64 +14,62 @@ import {
|
||||||
import DiscourseRoute from "discourse/routes/discourse";
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
|
|
||||||
|
@disableImplicitInjections
|
||||||
class AbstractCategoryRoute extends DiscourseRoute {
|
class AbstractCategoryRoute extends DiscourseRoute {
|
||||||
@service composer;
|
@service composer;
|
||||||
@service router;
|
@service router;
|
||||||
|
@service store;
|
||||||
|
@service topicTrackingState;
|
||||||
|
@service("search") searchService;
|
||||||
|
|
||||||
queryParams = queryParams;
|
queryParams = queryParams;
|
||||||
|
|
||||||
model(modelParams) {
|
templateName = "discovery/list";
|
||||||
|
controllerName = "discovery/list";
|
||||||
|
|
||||||
|
async model(params, transition) {
|
||||||
const category = Category.findBySlugPathWithID(
|
const category = Category.findBySlugPathWithID(
|
||||||
modelParams.category_slug_path_with_id
|
params.category_slug_path_with_id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!category) {
|
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");
|
this.router.replaceWith("/404");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { category, modelParams } = model;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.routeConfig?.no_subcategories === undefined &&
|
this.routeConfig?.no_subcategories === undefined &&
|
||||||
category.default_list_filter === "none" &&
|
category.default_list_filter === "none" &&
|
||||||
this.routeConfig?.filter === "default" &&
|
this.routeConfig?.filter === "default" &&
|
||||||
modelParams
|
params
|
||||||
) {
|
) {
|
||||||
// TODO: avoid throwing away preload data by redirecting on the server
|
// TODO: avoid throwing away preload data by redirecting on the server
|
||||||
PreloadStore.getAndRemove("topic_list");
|
PreloadStore.getAndRemove("topic_list");
|
||||||
this.router.replaceWith(
|
this.router.replaceWith(
|
||||||
"discovery.categoryNone",
|
"discovery.categoryNone",
|
||||||
modelParams.category_slug_path_with_id
|
params.category_slug_path_with_id
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setupNavigation(category);
|
const subcategoryListPromise = this._createSubcategoryList(category);
|
||||||
return all([
|
const topicListPromise = this._retrieveTopicList(
|
||||||
this._createSubcategoryList(category),
|
category,
|
||||||
this._retrieveTopicList(category, transition, modelParams),
|
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) {
|
filter(category) {
|
||||||
|
@ -84,32 +78,13 @@ class AbstractCategoryRoute extends DiscourseRoute {
|
||||||
: this.routeConfig?.filter;
|
: this.routeConfig?.filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupNavigation(category) {
|
async _createSubcategoryList(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;
|
|
||||||
|
|
||||||
if (category.isParent && category.show_subcategory_list) {
|
if (category.isParent && category.show_subcategory_list) {
|
||||||
return CategoryList.listForParent(this.store, category).then(
|
return CategoryList.listForParent(this.store, category);
|
||||||
(list) => (this._categoryList = list)
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're not loading a subcategory list just resolve
|
async _retrieveTopicList(category, transition, modelParams) {
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
_retrieveTopicList(category, transition, modelParams) {
|
|
||||||
const findOpts = filterQueryParams(modelParams, this.routeConfig);
|
const findOpts = filterQueryParams(modelParams, this.routeConfig);
|
||||||
const extras = { cached: this.isPoppedState(transition) };
|
const extras = { cached: this.isPoppedState(transition) };
|
||||||
|
|
||||||
|
@ -119,17 +94,16 @@ class AbstractCategoryRoute extends DiscourseRoute {
|
||||||
}
|
}
|
||||||
listFilter += `/l/${this.filter(category)}`;
|
listFilter += `/l/${this.filter(category)}`;
|
||||||
|
|
||||||
return findTopicList(
|
const topicList = await findTopicList(
|
||||||
this.store,
|
this.store,
|
||||||
this.topicTrackingState,
|
this.topicTrackingState,
|
||||||
listFilter,
|
listFilter,
|
||||||
findOpts,
|
findOpts,
|
||||||
extras
|
extras
|
||||||
).then((list) => {
|
);
|
||||||
TopicList.hideUniformCategory(list, category);
|
TopicList.hideUniformCategory(topicList, category);
|
||||||
this.set("topics", list);
|
|
||||||
return list;
|
return topicList;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
titleToken() {
|
titleToken() {
|
||||||
|
@ -153,52 +127,20 @@ class AbstractCategoryRoute extends DiscourseRoute {
|
||||||
}
|
}
|
||||||
|
|
||||||
setupController(controller, model) {
|
setupController(controller, model) {
|
||||||
const topics = this.topics,
|
super.setupController(...arguments);
|
||||||
category = model.category;
|
controller.bulkSelectHelper.clear();
|
||||||
|
this.searchService.searchContext = model.category.get("searchContext");
|
||||||
|
setTopicList(model.list);
|
||||||
|
|
||||||
let topicOpts = {
|
const p = model.category.params;
|
||||||
model: topics,
|
if (p?.order !== undefined) {
|
||||||
category,
|
controller.order = p.order;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
if (p.ascending !== undefined) {
|
if (p?.ascending !== undefined) {
|
||||||
topicOpts.ascending = p.ascending;
|
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() {
|
deactivate() {
|
||||||
super.deactivate(...arguments);
|
super.deactivate(...arguments);
|
||||||
|
|
||||||
|
@ -216,16 +158,6 @@ class AbstractCategoryRoute extends DiscourseRoute {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
changeSort(sortBy) {
|
|
||||||
changeSort.call(this, sortBy);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
changeNewListSubset(subset) {
|
|
||||||
changeNewListSubset.call(this, subset);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
resetParams(skipParams = []) {
|
resetParams(skipParams = []) {
|
||||||
resetParams.call(this, skipParams);
|
resetParams.call(this, skipParams);
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { isEmpty } from "@ember/utils";
|
import { isEmpty } from "@ember/utils";
|
||||||
import {
|
import { queryParams, resetParams } from "discourse/controllers/discovery/list";
|
||||||
changeNewListSubset,
|
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||||
changeSort,
|
import { setTopicList } from "discourse/lib/topic-list-tracker";
|
||||||
queryParams,
|
|
||||||
resetParams,
|
|
||||||
} from "discourse/controllers/discovery-sortable";
|
|
||||||
import { defaultHomepage } from "discourse/lib/utilities";
|
import { defaultHomepage } from "discourse/lib/utilities";
|
||||||
import Session from "discourse/models/session";
|
import Session from "discourse/models/session";
|
||||||
import Site from "discourse/models/site";
|
import Site from "discourse/models/site";
|
||||||
import User from "discourse/models/user";
|
|
||||||
import DiscourseRoute from "discourse/routes/discourse";
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
import { deepEqual } from "discourse-common/lib/object";
|
import { deepEqual } from "discourse-common/lib/object";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
|
@ -96,31 +92,36 @@ export async function findTopicList(
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@disableImplicitInjections
|
||||||
class AbstractTopicRoute extends DiscourseRoute {
|
class AbstractTopicRoute extends DiscourseRoute {
|
||||||
@service screenTrack;
|
@service screenTrack;
|
||||||
|
@service store;
|
||||||
|
@service topicTrackingState;
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
queryParams = queryParams;
|
queryParams = queryParams;
|
||||||
|
templateName = "discovery/list";
|
||||||
|
controllerName = "discovery/list";
|
||||||
|
|
||||||
beforeModel() {
|
async model(data, transition) {
|
||||||
this.controllerFor("navigation/default").set(
|
|
||||||
"filterType",
|
|
||||||
this.routeConfig.filter.split("/")[0]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
model(data, transition) {
|
|
||||||
// attempt to stop early cause we need this to be called before .sync
|
// attempt to stop early cause we need this to be called before .sync
|
||||||
this.screenTrack.stop();
|
this.screenTrack.stop();
|
||||||
|
|
||||||
const findOpts = filterQueryParams(data),
|
const findOpts = filterQueryParams(data),
|
||||||
findExtras = { cached: this.isPoppedState(transition) };
|
findExtras = { cached: this.isPoppedState(transition) };
|
||||||
|
|
||||||
return findTopicList(
|
const topicListPromise = findTopicList(
|
||||||
this.store,
|
this.store,
|
||||||
this.topicTrackingState,
|
this.topicTrackingState,
|
||||||
this.routeConfig.filter,
|
this.routeConfig.filter,
|
||||||
findOpts,
|
findOpts,
|
||||||
findExtras
|
findExtras
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
list: await topicListPromise,
|
||||||
|
filterType: this.routeConfig.filter.split("/")[0],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
titleToken() {
|
titleToken() {
|
||||||
|
@ -135,40 +136,9 @@ class AbstractTopicRoute extends DiscourseRoute {
|
||||||
}
|
}
|
||||||
|
|
||||||
setupController(controller, model) {
|
setupController(controller, model) {
|
||||||
const topicOpts = {
|
super.setupController(...arguments);
|
||||||
model,
|
controller.bulkSelectHelper.clear();
|
||||||
category: null,
|
setTopicList(model.list);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -178,10 +148,10 @@ class AbstractTopicRoute extends DiscourseRoute {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
willTransition() {
|
willTransition() {
|
||||||
if (this.routeConfig.filter === "top") {
|
if (this.routeConfig.filter === "top" && this.currentUser) {
|
||||||
User.currentProp("user_option.should_be_redirected_to_top", false);
|
this.currentUser.set("user_option.should_be_redirected_to_top", false);
|
||||||
if (User.currentProp("user_option.redirected_to_top")) {
|
if (this.currentUser.user_option?.redirected_to_top) {
|
||||||
User.currentProp("user_option.redirected_to_top.reason", null);
|
this.currentUser.set("user_option.redirected_to_top.reason", null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.willTransition(...arguments);
|
return super.willTransition(...arguments);
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { inject as service } from "@ember/service";
|
||||||
import { hash } from "rsvp";
|
import { hash } from "rsvp";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import PreloadStore from "discourse/lib/preload-store";
|
import PreloadStore from "discourse/lib/preload-store";
|
||||||
import showModal from "discourse/lib/show-modal";
|
|
||||||
import { defaultHomepage } from "discourse/lib/utilities";
|
import { defaultHomepage } from "discourse/lib/utilities";
|
||||||
import CategoryList from "discourse/models/category-list";
|
import CategoryList from "discourse/models/category-list";
|
||||||
import TopicList from "discourse/models/topic-list";
|
import TopicList from "discourse/models/topic-list";
|
||||||
|
@ -14,10 +13,8 @@ export default class DiscoveryCategoriesRoute extends DiscourseRoute {
|
||||||
@service router;
|
@service router;
|
||||||
@service session;
|
@service session;
|
||||||
|
|
||||||
renderTemplate() {
|
templateName = "discovery/categories";
|
||||||
this.render("navigation/categories", { outlet: "navigation-bar" });
|
controllerName = "discovery/categories";
|
||||||
this.render("discovery/categories", { outlet: "list-container" });
|
|
||||||
}
|
|
||||||
|
|
||||||
findCategories() {
|
findCategories() {
|
||||||
let style =
|
let style =
|
||||||
|
@ -131,27 +128,16 @@ export default class DiscoveryCategoriesRoute extends DiscourseRoute {
|
||||||
return I18n.t("filters.categories.title");
|
return I18n.t("filters.categories.title");
|
||||||
}
|
}
|
||||||
|
|
||||||
setupController(controller, model) {
|
setupController(controller) {
|
||||||
controller.set("model", model);
|
controller.setProperties({
|
||||||
|
discovery: this.controllerFor("discovery"),
|
||||||
this.controllerFor("navigation/categories").setProperties({
|
|
||||||
showCategoryAdmin: model.get("can_create_category"),
|
|
||||||
canCreateTopic: model.get("can_create_topic"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
super.setupController(...arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
triggerRefresh() {
|
triggerRefresh() {
|
||||||
this.refresh();
|
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 DiscourseRoute from "discourse/routes/discourse";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
|
|
||||||
|
@ -18,29 +17,4 @@ export default class DiscoveryFilterRoute extends DiscourseRoute {
|
||||||
const filterText = I18n.t("filters.filter.title");
|
const filterText = I18n.t("filters.filter.title");
|
||||||
return I18n.t("filters.with_topics", { filter: filterText });
|
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 { action } from "@ember/object";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { resetCachedTopicList } from "discourse/lib/cached-topic-list";
|
import { resetCachedTopicList } from "discourse/lib/cached-topic-list";
|
||||||
import { setTopicList } from "discourse/lib/topic-list-tracker";
|
|
||||||
import User from "discourse/models/user";
|
import User from "discourse/models/user";
|
||||||
import DiscourseRoute from "discourse/routes/discourse";
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The parent route for all discovery routes.
|
The parent route for all discovery routes.
|
||||||
Handles the logic for showing the loading spinners.
|
|
||||||
**/
|
**/
|
||||||
export default class DiscoveryRoute extends DiscourseRoute {
|
export default class DiscoveryRoute extends DiscourseRoute {
|
||||||
@service router;
|
@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
|
// clear a pinned topic
|
||||||
@action
|
@action
|
||||||
clearPin(topic) {
|
clearPin(topic) {
|
||||||
topic.clearPin();
|
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() {
|
refresh() {
|
||||||
resetCachedTopicList(this.session);
|
resetCachedTopicList(this.session);
|
||||||
super.refresh();
|
super.refresh();
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import {
|
import { queryParams, resetParams } from "discourse/controllers/discovery/list";
|
||||||
queryParams,
|
import { filterTypeForMode } from "discourse/lib/filter-mode";
|
||||||
resetParams,
|
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||||
} from "discourse/controllers/discovery-sortable";
|
|
||||||
import PreloadStore from "discourse/lib/preload-store";
|
import PreloadStore from "discourse/lib/preload-store";
|
||||||
import showModal from "discourse/lib/show-modal";
|
|
||||||
import { setTopicList } from "discourse/lib/topic-list-tracker";
|
import { setTopicList } from "discourse/lib/topic-list-tracker";
|
||||||
import { escapeExpression } from "discourse/lib/utilities";
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
import Category from "discourse/models/category";
|
import Category from "discourse/models/category";
|
||||||
import Composer from "discourse/models/composer";
|
|
||||||
import PermissionType from "discourse/models/permission-type";
|
import PermissionType from "discourse/models/permission-type";
|
||||||
import {
|
import {
|
||||||
filterQueryParams,
|
filterQueryParams,
|
||||||
findTopicList,
|
findTopicList,
|
||||||
} from "discourse/routes/build-topic-route";
|
} from "discourse/routes/build-topic-route";
|
||||||
import DiscourseRoute from "discourse/routes/discourse";
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
import { makeArray } from "discourse-common/lib/helpers";
|
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
|
|
||||||
const NONE = "none";
|
const NONE = "none";
|
||||||
const ALL = "all";
|
const ALL = "all";
|
||||||
|
|
||||||
|
@disableImplicitInjections
|
||||||
export default class TagShowRoute extends DiscourseRoute {
|
export default class TagShowRoute extends DiscourseRoute {
|
||||||
@service composer;
|
@service composer;
|
||||||
@service router;
|
@service router;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
|
@service store;
|
||||||
|
@service topicTrackingState;
|
||||||
|
@service("search") searchService;
|
||||||
|
|
||||||
queryParams = queryParams;
|
queryParams = queryParams;
|
||||||
controllerName = "tag.show";
|
controllerName = "discovery/list";
|
||||||
templateName = "tag.show";
|
templateName = "discovery/list";
|
||||||
routeConfig = {};
|
routeConfig = {};
|
||||||
|
|
||||||
get navMode() {
|
get navMode() {
|
||||||
|
@ -40,14 +40,6 @@ export default class TagShowRoute extends DiscourseRoute {
|
||||||
return this.routeConfig.noSubcategories;
|
return this.routeConfig.noSubcategories;
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeModel() {
|
|
||||||
const controller = this.controllerFor(this.controllerName);
|
|
||||||
controller.setProperties({
|
|
||||||
loading: true,
|
|
||||||
showInfo: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async model(params, transition) {
|
async model(params, transition) {
|
||||||
const tag = this.store.createRecord("tag", {
|
const tag = this.store.createRecord("tag", {
|
||||||
id: escapeExpression(params.tag_id),
|
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;
|
let tagNotification;
|
||||||
if (tag && tag.id !== NONE && this.currentUser && !additionalTags) {
|
if (tag && tag.id !== NONE && this.currentUser && !additionalTags) {
|
||||||
|
@ -139,8 +131,6 @@ export default class TagShowRoute extends DiscourseRoute {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setTopicList(list);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tag,
|
tag,
|
||||||
category,
|
category,
|
||||||
|
@ -151,20 +141,14 @@ export default class TagShowRoute extends DiscourseRoute {
|
||||||
canCreateTopic: list.can_create_topic,
|
canCreateTopic: list.can_create_topic,
|
||||||
canCreateTopicOnCategory: category?.permission === PermissionType.FULL,
|
canCreateTopicOnCategory: category?.permission === PermissionType.FULL,
|
||||||
canCreateTopicOnTag: !tag.staff || this.currentUser?.staff,
|
canCreateTopicOnTag: !tag.staff || this.currentUser?.staff,
|
||||||
|
noSubcategories: this.noSubcategories,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setupController(controller, model) {
|
setupController(controller, model) {
|
||||||
const noSubcategories = this.noSubcategories;
|
super.setupController(...arguments);
|
||||||
|
controller.bulkSelectHelper.clear();
|
||||||
controller.setProperties({
|
setTopicList(model.list);
|
||||||
model: model.tag,
|
|
||||||
...model,
|
|
||||||
period: model.list.for_period,
|
|
||||||
navMode: this.navMode,
|
|
||||||
noSubcategories,
|
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (model.category || model.additionalTags) {
|
if (model.category || model.additionalTags) {
|
||||||
const tagIntersectionSearchContext = {
|
const tagIntersectionSearchContext = {
|
||||||
|
@ -220,68 +204,10 @@ export default class TagShowRoute extends DiscourseRoute {
|
||||||
this.searchService.searchContext = null;
|
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
|
@action
|
||||||
resetParams(skipParams = []) {
|
resetParams(skipParams = []) {
|
||||||
resetParams.call(this, 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 = {}) {
|
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
|
// be handled by the intersection logic. Defining tags-intersection as something separate avoids
|
||||||
// that confusion.
|
// that confusion.
|
||||||
export default class extends buildTagRoute() {
|
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"
|
@name="above-discovery-categories"
|
||||||
@connectorTagName="div"
|
@connectorTagName="div"
|
||||||
@outletArgs={{hash
|
@outletArgs={{hash
|
||||||
|
@ -6,11 +16,15 @@
|
||||||
categoryPageStyle=this.categoryPageStyle
|
categoryPageStyle=this.categoryPageStyle
|
||||||
topics=this.model.topics
|
topics=this.model.topics
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DiscoveryCategories @refresh={{action "refresh"}}>
|
{{body-class "categories-list"}}
|
||||||
|
|
||||||
|
<div class="contents">
|
||||||
{{#if (and this.topicTrackingState.hasIncoming this.isCategoriesRoute)}}
|
{{#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
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
class="alert alert-info clickable"
|
class="alert alert-info clickable"
|
||||||
|
@ -25,24 +39,14 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if
|
<Discovery::CategoriesDisplay
|
||||||
(eq this.categoryPageStyle "categories-and-latest-topics-created-date")
|
|
||||||
}}
|
|
||||||
<CategoriesAndLatestTopics
|
|
||||||
@categories={{this.model.categories}}
|
@categories={{this.model.categories}}
|
||||||
@topics={{this.model.topics}}
|
@topics={{this.model.topics}}
|
||||||
|
@parentCategory={{this.model.parentCategory}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
</div>
|
||||||
{{component
|
|
||||||
this.categoryPageStyle
|
|
||||||
categories=this.model.categories
|
|
||||||
topics=this.model.topics
|
|
||||||
}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
</DiscoveryCategories>
|
<PluginOutlet
|
||||||
|
|
||||||
<PluginOutlet
|
|
||||||
@name="below-discovery-categories"
|
@name="below-discovery-categories"
|
||||||
@connectorTagName="div"
|
@connectorTagName="div"
|
||||||
@outletArgs={{hash
|
@outletArgs={{hash
|
||||||
|
@ -50,4 +54,6 @@
|
||||||
categoryPageStyle=this.categoryPageStyle
|
categoryPageStyle=this.categoryPageStyle
|
||||||
topics=this.model.topics
|
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">
|
<div class="container">
|
||||||
<DiscourseBanner @user={{this.currentUser}} @banner={{this.site.banner}} />
|
<DiscourseBanner />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container tags-index">
|
<div class="container tags-index">
|
||||||
|
|
|
@ -19,8 +19,6 @@
|
||||||
<AddTopicStatusClasses @topic={{this.model}} />
|
<AddTopicStatusClasses @topic={{this.model}} />
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<DiscourseBanner
|
<DiscourseBanner
|
||||||
@user={{this.currentUser}}
|
|
||||||
@banner={{this.site.banner}}
|
|
||||||
@overlay={{this.hasScrolled}}
|
@overlay={{this.hasScrolled}}
|
||||||
@hide={{this.model.errorLoading}}
|
@hide={{this.model.errorLoading}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -15,11 +15,15 @@
|
||||||
>
|
>
|
||||||
<TopicDismissButtons
|
<TopicDismissButtons
|
||||||
@position="top"
|
@position="top"
|
||||||
@selectedTopics={{this.selected}}
|
@selectedTopics={{this.bulkSelectHelper.selected}}
|
||||||
@model={{this.model}}
|
@model={{this.model}}
|
||||||
@showResetNew={{this.showResetNew}}
|
@showResetNew={{this.showResetNew}}
|
||||||
@showDismissRead={{this.showDismissRead}}
|
@showDismissRead={{this.showDismissRead}}
|
||||||
@resetNew={{action "resetNew"}}
|
@resetNew={{action "resetNew"}}
|
||||||
|
@dismissRead={{if
|
||||||
|
this.showDismissRead
|
||||||
|
(route-action "dismissReadTopics")
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{{#if (gt this.incomingCount 0)}}
|
{{#if (gt this.incomingCount 0)}}
|
||||||
|
@ -43,24 +47,22 @@
|
||||||
@topicList={{this.model}}
|
@topicList={{this.model}}
|
||||||
@hideCategory={{this.hideCategory}}
|
@hideCategory={{this.hideCategory}}
|
||||||
@showPosters={{this.showPosters}}
|
@showPosters={{this.showPosters}}
|
||||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
|
||||||
@bulkSelectAction={{action "refresh"}}
|
|
||||||
@selected={{this.selected}}
|
|
||||||
@tagsForUser={{this.tagsForUser}}
|
@tagsForUser={{this.tagsForUser}}
|
||||||
@canBulkSelect={{this.canBulkSelect}}
|
@canBulkSelect={{this.canBulkSelect}}
|
||||||
@toggleBulkSelect={{action "toggleBulkSelect"}}
|
@bulkSelectHelper={{this.bulkSelectHelper}}
|
||||||
@updateAutoAddTopicsToBulkSelect={{action
|
|
||||||
"updateAutoAddTopicsToBulkSelect"
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TopicDismissButtons
|
<TopicDismissButtons
|
||||||
@position="bottom"
|
@position="bottom"
|
||||||
@selectedTopics={{this.selected}}
|
@selectedTopics={{this.bulkSelectHelper.selected}}
|
||||||
@model={{this.model}}
|
@model={{this.model}}
|
||||||
@showResetNew={{this.showResetNew}}
|
@showResetNew={{this.showResetNew}}
|
||||||
@showDismissRead={{this.showDismissRead}}
|
@showDismissRead={{this.showDismissRead}}
|
||||||
@resetNew={{action "resetNew"}}
|
@resetNew={{action "resetNew"}}
|
||||||
|
@dismissRead={{if
|
||||||
|
this.showDismissRead
|
||||||
|
(route-action "dismissReadTopics")
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConditionalLoadingSpinner @condition={{this.model.loadingMore}} />
|
<ConditionalLoadingSpinner @condition={{this.model.loadingMore}} />
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
<div class="navigation-controls">
|
<div class="navigation-controls">
|
||||||
{{#if this.site.mobileView}}
|
{{#if this.site.mobileView}}
|
||||||
{{#if this.currentUser.admin}}
|
{{#if this.currentUser.admin}}
|
||||||
<BulkSelectToggle @parentController="user-topics-list" />
|
<BulkSelectToggle @bulkSelectHelper={{this.bulkSelectHelper}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,6 @@ globalThis.deprecationWorkflow.config = {
|
||||||
// We're using RAISE_ON_DEPRECATION in environment.js instead of
|
// We're using RAISE_ON_DEPRECATION in environment.js instead of
|
||||||
// `throwOnUnhandled` here since it is easier to toggle.
|
// `throwOnUnhandled` here since it is easier to toggle.
|
||||||
workflow: [
|
workflow: [
|
||||||
{ handler: "silence", matchId: "route-render-template" },
|
|
||||||
{ handler: "silence", matchId: "route-disconnect-outlet" },
|
|
||||||
{
|
{
|
||||||
handler: "silence",
|
handler: "silence",
|
||||||
matchId: "ember-this-fallback.this-property-fallback",
|
matchId: "ember-this-fallback.this-property-fallback",
|
||||||
|
|
|
@ -543,7 +543,7 @@ acceptance("Tag info", function (needs) {
|
||||||
await visit("/tag/planters");
|
await visit("/tag/planters");
|
||||||
await click("#create-topic");
|
await click("#create-topic");
|
||||||
let composer = this.owner.lookup("service:composer");
|
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 { click, currentURL, settled, visit } from "@ember/test-helpers";
|
||||||
import { skip, test } from "qunit";
|
import { skip, test } from "qunit";
|
||||||
import sinon from "sinon";
|
|
||||||
import { configureEyeline } from "discourse/lib/eyeline";
|
import { configureEyeline } from "discourse/lib/eyeline";
|
||||||
import DiscourseURL from "discourse/lib/url";
|
|
||||||
import { ScrollingDOMMethods } from "discourse/mixins/scrolling";
|
import { ScrollingDOMMethods } from "discourse/mixins/scrolling";
|
||||||
import discoveryFixtures from "discourse/tests/fixtures/discovery-fixtures";
|
import discoveryFixtures from "discourse/tests/fixtures/discovery-fixtures";
|
||||||
import {
|
import {
|
||||||
|
@ -128,19 +126,13 @@ acceptance("Topic Discovery", function (needs) {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Using period chooser when query params are present", async function (assert) {
|
test("Using period chooser when query params are present", async function (assert) {
|
||||||
await visit("/top?f=foo&d=bar");
|
await visit("/top?status=closed");
|
||||||
|
|
||||||
sinon.stub(DiscourseURL, "routeTo");
|
|
||||||
|
|
||||||
const periodChooser = selectKit(".period-chooser");
|
const periodChooser = selectKit(".period-chooser");
|
||||||
|
|
||||||
await periodChooser.expand();
|
await periodChooser.expand();
|
||||||
await periodChooser.selectRowByValue("yearly");
|
await periodChooser.selectRowByValue("yearly");
|
||||||
|
|
||||||
assert.ok(
|
assert.strictEqual(currentURL(), "/top?period=yearly&status=closed");
|
||||||
DiscourseURL.routeTo.calledWith("/top?f=foo&d=bar&period=yearly"),
|
|
||||||
"it keeps the query params"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("switching between tabs", async function (assert) {
|
test("switching between tabs", async function (assert) {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { click, currentURL, visit } from "@ember/test-helpers";
|
import { click, currentURL, visit } from "@ember/test-helpers";
|
||||||
import { test } from "qunit";
|
import { test } from "qunit";
|
||||||
import sinon from "sinon";
|
|
||||||
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
|
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
|
||||||
acceptance("Category 404", function (needs) {
|
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) {
|
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 visit("/t/internationalization-localization/280");
|
||||||
|
|
||||||
await click('[data-for-test="category-404"]');
|
await click('[data-for-test="category-404"]');
|
||||||
assert.strictEqual(currentURL(), "/404");
|
assert.strictEqual(currentURL(), "/404");
|
||||||
sinon.assert.calledOnce(stub);
|
|
||||||
|
|
||||||
// See that we can navigate away
|
// See that we can navigate away
|
||||||
await click("#site-logo");
|
await click("#site-logo");
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { getOwner } from "@ember/application";
|
||||||
import { click, render } from "@ember/test-helpers";
|
import { click, render } from "@ember/test-helpers";
|
||||||
import { hbs } from "ember-cli-htmlbars";
|
import { hbs } from "ember-cli-htmlbars";
|
||||||
import { module, test } from "qunit";
|
import { module, test } from "qunit";
|
||||||
|
import BulkSelectHelper from "discourse/lib/bulk-select-helper";
|
||||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||||
|
|
||||||
module("Integration | Component | topic-list", function (hooks) {
|
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: 24234 }),
|
||||||
store.createRecord("topic", { id: 24235 }),
|
store.createRecord("topic", { id: 24235 }),
|
||||||
],
|
],
|
||||||
selected: [],
|
bulkSelectHelper: new BulkSelectHelper(this),
|
||||||
bulkSelectEnabled: false,
|
|
||||||
autoAddTopicsToBulkSelect: false,
|
|
||||||
|
|
||||||
toggleBulkSelect() {
|
|
||||||
this.toggleProperty("bulkSelectEnabled");
|
|
||||||
},
|
|
||||||
|
|
||||||
updateAutoAddTopicsToBulkSelect(newVal) {
|
|
||||||
this.set("autoAddTopicsToBulkSelect", newVal);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await render(hbs`
|
await render(hbs`
|
||||||
<TopicList
|
<TopicList
|
||||||
@canBulkSelect={{true}}
|
@canBulkSelect={{true}}
|
||||||
@toggleBulkSelect={{this.toggleBulkSelect}}
|
@bulkSelectHelper={{this.bulkSelectHelper}}
|
||||||
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
|
||||||
@autoAddTopicsToBulkSelect={{this.autoAddTopicsToBulkSelect}}
|
|
||||||
@updateAutoAddTopicsToBulkSelect={{this.updateAutoAddTopicsToBulkSelect}}
|
|
||||||
@topics={{this.topics}}
|
@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");
|
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");
|
await click("button.bulk-select-all");
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
this.selected.length,
|
this.bulkSelectHelper.selected.length,
|
||||||
2,
|
2,
|
||||||
"clicking Select All selects all loaded topics"
|
"clicking Select All selects all loaded topics"
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.true(
|
||||||
this.autoAddTopicsToBulkSelect,
|
this.bulkSelectHelper.autoAddTopicsToBulkSelect,
|
||||||
"clicking Select All turns on the autoAddTopicsToBulkSelect flag"
|
"clicking Select All turns on the autoAddTopicsToBulkSelect flag"
|
||||||
);
|
);
|
||||||
|
|
||||||
await click("button.bulk-clear-all");
|
await click("button.bulk-clear-all");
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
this.selected.length,
|
this.bulkSelectHelper.selected.length,
|
||||||
0,
|
0,
|
||||||
"clicking Clear All deselects all topics"
|
"clicking Clear All deselects all topics"
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.false(
|
||||||
!this.autoAddTopicsToBulkSelect,
|
this.bulkSelectHelper.autoAddTopicsToBulkSelect,
|
||||||
"clicking Clear All turns off the autoAddTopicsToBulkSelect flag"
|
"clicking Clear All turns off the autoAddTopicsToBulkSelect flag"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,7 +38,7 @@ class TopicQuery
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.public_valid_options
|
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[
|
@public_valid_options ||= %i[
|
||||||
page
|
page
|
||||||
before
|
before
|
||||||
|
|
Loading…
Reference in New Issue
Block a user