mirror of
https://github.com/discourse/discourse.git
synced 2025-02-07 22:14:59 +08:00
DEV: Implement glimmer topic-list (#26743)
(experimental) The initial implementation of glimmer topic-list and related components. Does not include new APIs and isn't compatible with existing customization. That's gonna come in future PRs. Enabled by adding groups to `experimental_glimmer_topic_list_groups` setting.
This commit is contained in:
parent
eb2df2b7d6
commit
87769a83c4
|
@ -1,18 +1,34 @@
|
||||||
<ConditionalLoadingSpinner @condition={{this.loading}}>
|
<ConditionalLoadingSpinner @condition={{this.loading}}>
|
||||||
{{#if this.topics}}
|
{{#if this.topics}}
|
||||||
<TopicList
|
{{#if this.currentUser.use_glimmer_topic_list}}
|
||||||
@showPosters={{this.showPosters}}
|
<TopicList::List
|
||||||
@hideCategory={{this.hideCategory}}
|
@showPosters={{this.showPosters}}
|
||||||
@topics={{this.topics}}
|
@hideCategory={{this.hideCategory}}
|
||||||
@expandExcerpts={{this.expandExcerpts}}
|
@topics={{this.topics}}
|
||||||
@bulkSelectHelper={{this.bulkSelectHelper}}
|
@expandExcerpts={{this.expandExcerpts}}
|
||||||
@canBulkSelect={{this.canBulkSelect}}
|
@bulkSelectHelper={{this.bulkSelectHelper}}
|
||||||
@tagsForUser={{this.tagsForUser}}
|
@canBulkSelect={{this.canBulkSelect}}
|
||||||
@changeSort={{this.changeSort}}
|
@tagsForUser={{this.tagsForUser}}
|
||||||
@order={{this.order}}
|
@changeSort={{this.changeSort}}
|
||||||
@ascending={{this.ascending}}
|
@order={{this.order}}
|
||||||
@focusLastVisitedTopic={{this.focusLastVisitedTopic}}
|
@ascending={{this.ascending}}
|
||||||
/>
|
@focusLastVisitedTopic={{this.focusLastVisitedTopic}}
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<TopicList
|
||||||
|
@showPosters={{this.showPosters}}
|
||||||
|
@hideCategory={{this.hideCategory}}
|
||||||
|
@topics={{this.topics}}
|
||||||
|
@expandExcerpts={{this.expandExcerpts}}
|
||||||
|
@bulkSelectHelper={{this.bulkSelectHelper}}
|
||||||
|
@canBulkSelect={{this.canBulkSelect}}
|
||||||
|
@tagsForUser={{this.tagsForUser}}
|
||||||
|
@changeSort={{this.changeSort}}
|
||||||
|
@order={{this.order}}
|
||||||
|
@ascending={{this.ascending}}
|
||||||
|
@focusLastVisitedTopic={{this.focusLastVisitedTopic}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#unless this.loadingMore}}
|
{{#unless this.loadingMore}}
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
|
|
|
@ -8,8 +8,13 @@
|
||||||
|
|
||||||
{{#if this.topics}}
|
{{#if this.topics}}
|
||||||
{{#each this.topics as |t|}}
|
{{#each this.topics as |t|}}
|
||||||
<LatestTopicListItem @topic={{t}} />
|
{{#if this.currentUser.use_glimmer_topic_list}}
|
||||||
|
<TopicList::LatestTopicListItem @topic={{t}} />
|
||||||
|
{{else}}
|
||||||
|
<LatestTopicListItem @topic={{t}} />
|
||||||
|
{{/if}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
<div class="more-topics">
|
<div class="more-topics">
|
||||||
{{#if
|
{{#if
|
||||||
(eq
|
(eq
|
||||||
|
|
|
@ -7,15 +7,27 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if @model.sharedDrafts}}
|
{{#if @model.sharedDrafts}}
|
||||||
<TopicList
|
{{#if this.currentUser.use_glimmer_topic_list}}
|
||||||
@listTitle="shared_drafts.title"
|
<TopicList::List
|
||||||
@top={{this.top}}
|
@listTitle="shared_drafts.title"
|
||||||
@hideCategory="true"
|
@top={{this.top}}
|
||||||
@category={{@category}}
|
@hideCategory="true"
|
||||||
@topics={{@model.sharedDrafts}}
|
@category={{@category}}
|
||||||
@discoveryList={{true}}
|
@topics={{@model.sharedDrafts}}
|
||||||
class="shared-drafts"
|
@discoveryList={{true}}
|
||||||
/>
|
class="shared-drafts"
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<TopicList
|
||||||
|
@listTitle="shared_drafts.title"
|
||||||
|
@top={{this.top}}
|
||||||
|
@hideCategory="true"
|
||||||
|
@category={{@category}}
|
||||||
|
@topics={{@model.sharedDrafts}}
|
||||||
|
@discoveryList={{true}}
|
||||||
|
class="shared-drafts"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<DiscoveryTopicsList
|
<DiscoveryTopicsList
|
||||||
|
@ -75,31 +87,59 @@
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{{#if this.hasTopics}}
|
{{#if this.hasTopics}}
|
||||||
<TopicList
|
{{#if this.currentUser.use_glimmer_topic_list}}
|
||||||
@highlightLastVisited={{true}}
|
<TopicList::List
|
||||||
@top={{this.top}}
|
@highlightLastVisited={{true}}
|
||||||
@hot={{this.hot}}
|
@top={{this.top}}
|
||||||
@showTopicPostBadges={{this.showTopicPostBadges}}
|
@hot={{this.hot}}
|
||||||
@showPosters={{true}}
|
@showTopicPostBadges={{this.showTopicPostBadges}}
|
||||||
@canBulkSelect={{@canBulkSelect}}
|
@showPosters={{true}}
|
||||||
@bulkSelectHelper={{@bulkSelectHelper}}
|
@canBulkSelect={{@canBulkSelect}}
|
||||||
@changeSort={{@changeSort}}
|
@bulkSelectHelper={{@bulkSelectHelper}}
|
||||||
@hideCategory={{@model.hideCategory}}
|
@changeSort={{@changeSort}}
|
||||||
@order={{this.order}}
|
@hideCategory={{@model.hideCategory}}
|
||||||
@ascending={{this.ascending}}
|
@order={{this.order}}
|
||||||
@expandGloballyPinned={{this.expandGloballyPinned}}
|
@ascending={{this.ascending}}
|
||||||
@expandAllPinned={{this.expandAllPinned}}
|
@expandGloballyPinned={{this.expandGloballyPinned}}
|
||||||
@category={{@category}}
|
@expandAllPinned={{this.expandAllPinned}}
|
||||||
@topics={{@model.topics}}
|
@category={{@category}}
|
||||||
@discoveryList={{true}}
|
@topics={{@model.topics}}
|
||||||
@focusLastVisitedTopic={{true}}
|
@discoveryList={{true}}
|
||||||
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
|
@focusLastVisitedTopic={{true}}
|
||||||
@newListSubset={{@model.params.subset}}
|
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
|
||||||
@changeNewListSubset={{@changeNewListSubset}}
|
@newListSubset={{@model.params.subset}}
|
||||||
@newRepliesCount={{this.newRepliesCount}}
|
@changeNewListSubset={{@changeNewListSubset}}
|
||||||
@newTopicsCount={{this.newTopicsCount}}
|
@newRepliesCount={{this.newRepliesCount}}
|
||||||
/>
|
@newTopicsCount={{this.newTopicsCount}}
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<TopicList
|
||||||
|
@highlightLastVisited={{true}}
|
||||||
|
@top={{this.top}}
|
||||||
|
@hot={{this.hot}}
|
||||||
|
@showTopicPostBadges={{this.showTopicPostBadges}}
|
||||||
|
@showPosters={{true}}
|
||||||
|
@canBulkSelect={{@canBulkSelect}}
|
||||||
|
@bulkSelectHelper={{@bulkSelectHelper}}
|
||||||
|
@changeSort={{@changeSort}}
|
||||||
|
@hideCategory={{@model.hideCategory}}
|
||||||
|
@order={{this.order}}
|
||||||
|
@ascending={{this.ascending}}
|
||||||
|
@expandGloballyPinned={{this.expandGloballyPinned}}
|
||||||
|
@expandAllPinned={{this.expandAllPinned}}
|
||||||
|
@category={{@category}}
|
||||||
|
@topics={{@model.topics}}
|
||||||
|
@discoveryList={{true}}
|
||||||
|
@focusLastVisitedTopic={{true}}
|
||||||
|
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
|
||||||
|
@newListSubset={{@model.params.subset}}
|
||||||
|
@changeNewListSubset={{@changeNewListSubset}}
|
||||||
|
@newRepliesCount={{this.newRepliesCount}}
|
||||||
|
@newTopicsCount={{this.newTopicsCount}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<PluginOutlet
|
<PluginOutlet
|
||||||
@name="after-topic-list"
|
@name="after-topic-list"
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { on } from "@ember/modifier";
|
import { on } from "@ember/modifier";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import NewListHeaderControls from "discourse/components/topic-list/new-list-header-controls";
|
||||||
import raw from "discourse/helpers/raw";
|
import raw from "discourse/helpers/raw";
|
||||||
|
|
||||||
export default class NewListHeaderControlsWrapper extends Component {
|
export default class NewListHeaderControlsWrapper extends Component {
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
click(e) {
|
click(e) {
|
||||||
const target = e.target;
|
const target = e.target;
|
||||||
|
@ -17,18 +21,30 @@ export default class NewListHeaderControlsWrapper extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
{{#if this.currentUser.use_glimmer_topic_list}}
|
||||||
{{! template-lint-disable no-invalid-interactive }}
|
<div class="topic-replies-toggle-wrapper">
|
||||||
{{on "click" this.click}}
|
<NewListHeaderControls
|
||||||
class="topic-replies-toggle-wrapper"
|
@current={{@current}}
|
||||||
>
|
@newRepliesCount={{@newRepliesCount}}
|
||||||
{{raw
|
@newTopicsCount={{@newTopicsCount}}
|
||||||
"list/new-list-header-controls"
|
@noStaticLabel={{true}}
|
||||||
current=@current
|
@changeNewListSubset={{@changeNewListSubset}}
|
||||||
newRepliesCount=@newRepliesCount
|
/>
|
||||||
newTopicsCount=@newTopicsCount
|
</div>
|
||||||
noStaticLabel=true
|
{{else}}
|
||||||
}}
|
<div
|
||||||
</div>
|
{{! template-lint-disable no-invalid-interactive }}
|
||||||
|
{{on "click" this.click}}
|
||||||
|
class="topic-replies-toggle-wrapper"
|
||||||
|
>
|
||||||
|
{{raw
|
||||||
|
"list/new-list-header-controls"
|
||||||
|
current=@current
|
||||||
|
newRepliesCount=@newRepliesCount
|
||||||
|
newTopicsCount=@newTopicsCount
|
||||||
|
noStaticLabel=true
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,11 @@
|
||||||
{{#if this.showTopics}}
|
{{#if this.showTopics}}
|
||||||
<td class="latest">
|
<td class="latest">
|
||||||
{{#each this.category.featuredTopics as |t|}}
|
{{#each this.category.featuredTopics as |t|}}
|
||||||
<FeaturedTopic @topic={{t}} />
|
{{#if this.currentUser.use_glimmer_topic_list}}
|
||||||
|
<TopicList::FeaturedTopic @topic={{t}} />
|
||||||
|
{{else}}
|
||||||
|
<FeaturedTopic @topic={{t}} />
|
||||||
|
{{/if}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</td>
|
</td>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -10,12 +10,14 @@ import TopicBulkActions from "./modal/topic-bulk-actions";
|
||||||
export default Component.extend(LoadMore, {
|
export default Component.extend(LoadMore, {
|
||||||
modal: service(),
|
modal: service(),
|
||||||
router: service(),
|
router: service(),
|
||||||
|
siteSettings: 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",
|
||||||
|
lastCheckedElementId: null,
|
||||||
|
|
||||||
get canDoBulkActions() {
|
get canDoBulkActions() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
|
|
||||||
|
const ActionList = <template>
|
||||||
|
{{#if @postNumbers}}
|
||||||
|
<div class="post-actions" ...attributes>
|
||||||
|
{{icon @icon}}
|
||||||
|
{{#each @postNumbers as |postNumber|}}
|
||||||
|
<a href="{{@topic.url}}/{{postNumber}}">#{{postNumber}}</a>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default ActionList;
|
|
@ -0,0 +1,37 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||||
|
import coldAgeClass from "discourse/helpers/cold-age-class";
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import element from "discourse/helpers/element";
|
||||||
|
import formatDate from "discourse/helpers/format-date";
|
||||||
|
|
||||||
|
export default class ActivityColumn extends Component {
|
||||||
|
@service siteSettings;
|
||||||
|
|
||||||
|
get wrapperElement() {
|
||||||
|
return element(this.args.tagName ?? "td");
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<this.wrapperElement
|
||||||
|
title={{htmlSafe @topic.bumpedAtTitle}}
|
||||||
|
class={{concatClass
|
||||||
|
"activity"
|
||||||
|
(coldAgeClass @topic.createdAt startDate=@topic.bumpedAt class="")
|
||||||
|
}}
|
||||||
|
...attributes
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={{@topic.lastPostUrl}}
|
||||||
|
class="post-activity"
|
||||||
|
>{{! no whitespace
|
||||||
|
}}<PluginOutlet
|
||||||
|
@name="topic-list-before-relative-date"
|
||||||
|
/>
|
||||||
|
{{~formatDate @topic.bumpedAt format="tiny" noTitle="true"~}}
|
||||||
|
</a>
|
||||||
|
</this.wrapperElement>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import TopicEntrance from "discourse/components/topic-list/topic-entrance";
|
||||||
|
import TopicPostBadges from "discourse/components/topic-post-badges";
|
||||||
|
import TopicStatus from "discourse/components/topic-status";
|
||||||
|
import formatAge from "discourse/helpers/format-age";
|
||||||
|
import { modKeysPressed } from "discourse/lib/utilities";
|
||||||
|
|
||||||
|
const onTimestampClick = function (event) {
|
||||||
|
if (modKeysPressed(event).length) {
|
||||||
|
// Allow opening the link in a new tab/window
|
||||||
|
event.stopPropagation();
|
||||||
|
} else {
|
||||||
|
// Otherwise only display the TopicEntrance component
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const FeaturedTopic = <template>
|
||||||
|
<div data-topic-id={{@topic.id}} class="featured-topic --glimmer">
|
||||||
|
<TopicStatus @topic={{@topic}} />
|
||||||
|
|
||||||
|
<a href={{@topic.lastUnreadUrl}} class="title">{{htmlSafe
|
||||||
|
@topic.fancyTitle
|
||||||
|
}}</a>
|
||||||
|
|
||||||
|
<TopicPostBadges
|
||||||
|
@unreadPosts={{@topic.unread_posts}}
|
||||||
|
@unseen={{@topic.unseen}}
|
||||||
|
@url={{@topic.lastUnreadUrl}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TopicEntrance @topic={{@topic}}>
|
||||||
|
<a
|
||||||
|
{{on "click" onTimestampClick}}
|
||||||
|
href={{@topic.lastPostUrl}}
|
||||||
|
class="last-posted-at"
|
||||||
|
>{{formatAge @topic.last_posted_at}}</a>
|
||||||
|
</TopicEntrance>
|
||||||
|
</div>
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default FeaturedTopic;
|
|
@ -0,0 +1,94 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { concat, hash } from "@ember/helper";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||||
|
import PostsCountColumn from "discourse/components/topic-list/posts-count-column";
|
||||||
|
import TopicPostBadges from "discourse/components/topic-post-badges";
|
||||||
|
import TopicStatus from "discourse/components/topic-status";
|
||||||
|
import UserAvatarFlair from "discourse/components/user-avatar-flair";
|
||||||
|
import UserLink from "discourse/components/user-link";
|
||||||
|
import avatar from "discourse/helpers/avatar";
|
||||||
|
import categoryLink from "discourse/helpers/category-link";
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import discourseTags from "discourse/helpers/discourse-tags";
|
||||||
|
import formatDate from "discourse/helpers/format-date";
|
||||||
|
import topicFeaturedLink from "discourse/helpers/topic-featured-link";
|
||||||
|
import topicLink from "discourse/helpers/topic-link";
|
||||||
|
|
||||||
|
export default class LatestTopicListItem extends Component {
|
||||||
|
@service appEvents;
|
||||||
|
|
||||||
|
get tagClassNames() {
|
||||||
|
if (this.args.topic.tags) {
|
||||||
|
return this.args.topic.tags.map((tagName) => `tag-${tagName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-topic-id={{@topic.id}}
|
||||||
|
class={{concatClass
|
||||||
|
"latest-topic-list-item"
|
||||||
|
this.tagClassNames
|
||||||
|
(if @topic.category (concat "category-" @topic.category.fullSlug))
|
||||||
|
(if @topic.liked "liked")
|
||||||
|
(if @topic.archived "archived")
|
||||||
|
(if @topic.bookmarked "bookmarked")
|
||||||
|
(if @topic.pinned "pinned")
|
||||||
|
(if @topic.closed "closed")
|
||||||
|
(if @topic.visited "visited")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PluginOutlet
|
||||||
|
@name="above-latest-topic-list-item"
|
||||||
|
@connectorTagName="div"
|
||||||
|
@outletArgs={{hash topic=@topic}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="topic-poster">
|
||||||
|
<UserLink @user={{@topic.lastPosterUser}}>
|
||||||
|
{{avatar @topic.lastPosterUser imageSize="large"}}
|
||||||
|
</UserLink>
|
||||||
|
<UserAvatarFlair @user={{@topic.lastPosterUser}} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-link">
|
||||||
|
<div class="top-row">
|
||||||
|
<TopicStatus @topic={{@topic}} />
|
||||||
|
|
||||||
|
{{topicLink @topic}}
|
||||||
|
{{~#if @topic.featured_link}}
|
||||||
|
{{topicFeaturedLink @topic}}
|
||||||
|
{{/if~}}
|
||||||
|
<TopicPostBadges
|
||||||
|
@unreadPosts={{@topic.unread_posts}}
|
||||||
|
@unseen={{@topic.unseen}}
|
||||||
|
@url={{@topic.lastUnreadUrl}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bottom-row">
|
||||||
|
{{categoryLink @topic.category~}}
|
||||||
|
{{~discourseTags @topic mode="list"}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="topic-stats">
|
||||||
|
<PluginOutlet
|
||||||
|
@name="above-latest-topic-list-item-post-count"
|
||||||
|
@connectorTagName="div"
|
||||||
|
@outletArgs={{hash topic=@topic}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PostsCountColumn @topic={{@topic}} @tagName="div" />
|
||||||
|
|
||||||
|
<div class="topic-last-activity">
|
||||||
|
<a
|
||||||
|
href={{@topic.lastPostUrl}}
|
||||||
|
title={{@topic.bumpedAtTitle}}
|
||||||
|
>{{formatDate @topic.bumpedAt format="tiny" noTitle="true"}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { fn, hash } from "@ember/helper";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { eq, or } from "truth-helpers";
|
||||||
|
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||||
|
import TopicListHeader from "discourse/components/topic-list/topic-list-header";
|
||||||
|
import TopicListItem from "discourse/components/topic-list/topic-list-item";
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
|
||||||
|
export default class TopicList extends Component {
|
||||||
|
@service currentUser;
|
||||||
|
@service router;
|
||||||
|
@service siteSettings;
|
||||||
|
|
||||||
|
@tracked lastCheckedElementId;
|
||||||
|
|
||||||
|
get selected() {
|
||||||
|
return this.args.bulkSelectHelper?.selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
get bulkSelectEnabled() {
|
||||||
|
return this.args.bulkSelectHelper?.bulkSelectEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canDoBulkActions() {
|
||||||
|
return this.currentUser?.canManageTopic && this.selected?.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get toggleInTitle() {
|
||||||
|
return !this.bulkSelectEnabled && this.args.canBulkSelect;
|
||||||
|
}
|
||||||
|
|
||||||
|
get experimentalTopicBulkActionsEnabled() {
|
||||||
|
return this.currentUser?.use_experimental_topic_bulk_actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sortable() {
|
||||||
|
return !!this.args.changeSort;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showLikes() {
|
||||||
|
return this.args.order === "likes";
|
||||||
|
}
|
||||||
|
|
||||||
|
get showOpLikes() {
|
||||||
|
return this.args.order === "op_likes";
|
||||||
|
}
|
||||||
|
|
||||||
|
get lastVisitedTopic() {
|
||||||
|
const { topics, order, ascending, top, hot } = this.args;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.args.highlightLastVisited ||
|
||||||
|
top ||
|
||||||
|
hot ||
|
||||||
|
ascending ||
|
||||||
|
!topics ||
|
||||||
|
topics.length === 1 ||
|
||||||
|
(order && order !== "activity") ||
|
||||||
|
!this.currentUser?.get("previous_visit_at")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// work backwards
|
||||||
|
// this is more efficient cause we keep appending to list
|
||||||
|
const start = topics.findIndex((topic) => !topic.get("pinned"));
|
||||||
|
let lastVisitedTopic, topic;
|
||||||
|
|
||||||
|
for (let i = topics.length - 1; i >= start; i--) {
|
||||||
|
if (topics[i].get("bumpedAt") > this.currentUser.get("previousVisitAt")) {
|
||||||
|
lastVisitedTopic = topics[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
topic = topics[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lastVisitedTopic || !topic) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// end of list that was scanned
|
||||||
|
if (topic.get("bumpedAt") > this.currentUser.get("previousVisitAt")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastVisitedTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{! template-lint-disable table-groups }}
|
||||||
|
<table
|
||||||
|
class={{concatClass
|
||||||
|
"topic-list"
|
||||||
|
(if this.bulkSelectEnabled "sticky-header")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<thead class="topic-list-header">
|
||||||
|
<TopicListHeader
|
||||||
|
@canBulkSelect={{@canBulkSelect}}
|
||||||
|
@toggleInTitle={{this.toggleInTitle}}
|
||||||
|
@category={{@category}}
|
||||||
|
@hideCategory={{@hideCategory}}
|
||||||
|
@showPosters={{@showPosters}}
|
||||||
|
@showLikes={{this.showLikes}}
|
||||||
|
@showOpLikes={{this.showOpLikes}}
|
||||||
|
@order={{@order}}
|
||||||
|
@changeSort={{@changeSort}}
|
||||||
|
@ascending={{@ascending}}
|
||||||
|
@sortable={{this.sortable}}
|
||||||
|
@listTitle={{or @listTitle "topic.title"}}
|
||||||
|
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
||||||
|
@bulkSelectHelper={{@bulkSelectHelper}}
|
||||||
|
@experimentalTopicBulkActionsEnabled={{this.experimentalTopicBulkActionsEnabled}}
|
||||||
|
@canDoBulkActions={{this.canDoBulkActions}}
|
||||||
|
@showTopicsAndRepliesToggle={{@showTopicsAndRepliesToggle}}
|
||||||
|
@newListSubset={{@newListSubset}}
|
||||||
|
@newRepliesCount={{@newRepliesCount}}
|
||||||
|
@newTopicsCount={{@newTopicsCount}}
|
||||||
|
@changeNewListSubset={{@changeNewListSubset}}
|
||||||
|
/>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<PluginOutlet
|
||||||
|
@name="before-topic-list-body"
|
||||||
|
@outletArgs={{hash
|
||||||
|
topics=@topics
|
||||||
|
selected=this.selected
|
||||||
|
bulkSelectEnabled=this.bulkSelectEnabled
|
||||||
|
lastVisitedTopic=this.lastVisitedTopic
|
||||||
|
discoveryList=@discoveryList
|
||||||
|
hideCategory=@hideCategory
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<tbody class="topic-list-body">
|
||||||
|
{{#each @topics as |topic index|}}
|
||||||
|
<TopicListItem
|
||||||
|
@topic={{topic}}
|
||||||
|
@bulkSelectEnabled={{this.bulkSelectEnabled}}
|
||||||
|
@showTopicPostBadges={{@showTopicPostBadges}}
|
||||||
|
@hideCategory={{@hideCategory}}
|
||||||
|
@showPosters={{@showPosters}}
|
||||||
|
@showLikes={{this.showLikes}}
|
||||||
|
@showOpLikes={{this.showOpLikes}}
|
||||||
|
@expandGloballyPinned={{@expandGloballyPinned}}
|
||||||
|
@expandAllPinned={{@expandAllPinned}}
|
||||||
|
@lastVisitedTopic={{this.lastVisitedTopic}}
|
||||||
|
@selected={{this.selected}}
|
||||||
|
@lastCheckedElementId={{this.lastCheckedElementId}}
|
||||||
|
@updateLastCheckedElementId={{fn (mut this.lastCheckedElementId)}}
|
||||||
|
@tagsForUser={{@tagsForUser}}
|
||||||
|
@focusLastVisitedTopic={{@focusLastVisitedTopic}}
|
||||||
|
@index={{index}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#if (eq topic this.lastVisitedTopic)}}
|
||||||
|
<tr class="topic-list-item-separator">
|
||||||
|
<td class="topic-list-data" colspan="6">
|
||||||
|
<span>
|
||||||
|
{{i18n "topics.new_messages_marker"}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<PluginOutlet
|
||||||
|
@name="after-topic-list-item"
|
||||||
|
@outletArgs={{hash topic=topic index=index}}
|
||||||
|
@connectorTagName="tr"
|
||||||
|
/>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
<PluginOutlet
|
||||||
|
@name="after-topic-list-body"
|
||||||
|
@outletArgs={{hash
|
||||||
|
topics=@topics
|
||||||
|
selected=this.selected
|
||||||
|
bulkSelectEnabled=this.bulkSelectEnabled
|
||||||
|
lastVisitedTopic=this.lastVisitedTopic
|
||||||
|
discoveryList=@discoveryList
|
||||||
|
hideCategory=@hideCategory
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { fn } from "@ember/helper";
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
|
||||||
|
export default class NewListHeaderControls extends Component {
|
||||||
|
get topicsActive() {
|
||||||
|
return this.args.current === "topics";
|
||||||
|
}
|
||||||
|
|
||||||
|
get repliesActive() {
|
||||||
|
return this.args.current === "replies";
|
||||||
|
}
|
||||||
|
|
||||||
|
get allActive() {
|
||||||
|
return !this.topicsActive && !this.repliesActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
get repliesButtonLabel() {
|
||||||
|
if (this.args.newRepliesCount > 0) {
|
||||||
|
return i18n("filters.new.replies_with_count", {
|
||||||
|
count: this.args.newRepliesCount,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return i18n("filters.new.replies");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get topicsButtonLabel() {
|
||||||
|
if (this.args.newTopicsCount > 0) {
|
||||||
|
return i18n("filters.new.topics_with_count", {
|
||||||
|
count: this.args.newTopicsCount,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return i18n("filters.new.topics");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get staticLabel() {
|
||||||
|
if (
|
||||||
|
this.args.noStaticLabel ||
|
||||||
|
(this.args.newTopicsCount > 0 && this.args.newRepliesCount > 0)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.args.newTopicsCount > 0) {
|
||||||
|
return this.topicsButtonLabel;
|
||||||
|
} else {
|
||||||
|
return this.repliesButtonLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{#if this.staticLabel}}
|
||||||
|
<span class="static-label">{{this.staticLabel}}</span>
|
||||||
|
{{else}}
|
||||||
|
<button
|
||||||
|
{{on "click" (fn @changeNewListSubset null)}}
|
||||||
|
class={{concatClass
|
||||||
|
"topics-replies-toggle --all"
|
||||||
|
(if this.allActive "active")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{{i18n "filters.new.all"}}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
{{on "click" (fn @changeNewListSubset "topics")}}
|
||||||
|
class={{concatClass
|
||||||
|
"topics-replies-toggle --topics"
|
||||||
|
(if this.topicsActive "active")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{{this.topicsButtonLabel}}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
{{on "click" (fn @changeNewListSubset "replies")}}
|
||||||
|
class={{concatClass
|
||||||
|
"topics-replies-toggle --replies"
|
||||||
|
(if this.repliesActive "active")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{{this.repliesButtonLabel}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
|
||||||
|
const ParticipantGroups = <template>
|
||||||
|
<div
|
||||||
|
role="list"
|
||||||
|
aria-label={{i18n "topic.participant_groups"}}
|
||||||
|
class="participant-group-wrapper"
|
||||||
|
>
|
||||||
|
{{#each @groups as |group|}}
|
||||||
|
<div class="participant-group">
|
||||||
|
<a
|
||||||
|
href={{group.url}}
|
||||||
|
data-group-card={{group.name}}
|
||||||
|
class="user-group trigger-group-card"
|
||||||
|
>
|
||||||
|
{{icon "users"}}
|
||||||
|
{{group.name}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default ParticipantGroups;
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { and } from "truth-helpers";
|
||||||
|
import PostsCountColumn from "discourse/components/topic-list/posts-count-column";
|
||||||
|
import TopicPostBadges from "discourse/components/topic-post-badges";
|
||||||
|
|
||||||
|
const PostCountOrBadges = <template>
|
||||||
|
{{#if (and @postBadgesEnabled @topic.unread_posts)}}
|
||||||
|
<TopicPostBadges
|
||||||
|
@unreadPosts={{@topic.unread_posts}}
|
||||||
|
@unseen={{@topic.unseen}}
|
||||||
|
@url={{@topic.lastUnreadUrl}}
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<PostsCountColumn @topic={{@topic}} @tagName="div" />
|
||||||
|
{{/if}}
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default PostCountOrBadges;
|
|
@ -0,0 +1,25 @@
|
||||||
|
import avatar from "discourse/helpers/avatar";
|
||||||
|
|
||||||
|
const PostersColumn = <template>
|
||||||
|
<td class="posters topic-list-data">
|
||||||
|
{{#each @posters as |poster|}}
|
||||||
|
{{#if poster.moreCount}}
|
||||||
|
<a class="posters-more-count">{{poster.moreCount}}</a>
|
||||||
|
{{else}}
|
||||||
|
<a
|
||||||
|
href={{poster.user.path}}
|
||||||
|
data-user-card={{poster.user.username}}
|
||||||
|
class={{poster.extraClasses}}
|
||||||
|
>{{avatar
|
||||||
|
poster
|
||||||
|
avatarTemplatePath="user.avatar_template"
|
||||||
|
usernamePath="user.username"
|
||||||
|
namePath="user.name"
|
||||||
|
imageSize="small"
|
||||||
|
}}</a>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</td>
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default PostersColumn;
|
|
@ -0,0 +1,67 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||||
|
import TopicEntrance from "discourse/components/topic-list/topic-entrance";
|
||||||
|
import element from "discourse/helpers/element";
|
||||||
|
import number from "discourse/helpers/number";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
|
|
||||||
|
export default class PostsCountColumn extends Component {
|
||||||
|
@service siteSettings;
|
||||||
|
|
||||||
|
get ratio() {
|
||||||
|
const likes = parseFloat(this.args.topic.like_count);
|
||||||
|
const posts = parseFloat(this.args.topic.posts_count);
|
||||||
|
|
||||||
|
if (posts < 10) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (likes || 0) / posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return I18n.messageFormat("posts_likes_MF", {
|
||||||
|
count: this.args.topic.replyCount,
|
||||||
|
ratio: this.ratioText,
|
||||||
|
}).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
get ratioText() {
|
||||||
|
if (this.ratio > this.siteSettings.topic_post_like_heat_high) {
|
||||||
|
return "high";
|
||||||
|
}
|
||||||
|
if (this.ratio > this.siteSettings.topic_post_like_heat_medium) {
|
||||||
|
return "med";
|
||||||
|
}
|
||||||
|
if (this.ratio > this.siteSettings.topic_post_like_heat_low) {
|
||||||
|
return "low";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
get likesHeat() {
|
||||||
|
if (this.ratioText?.length) {
|
||||||
|
return `heatmap-${this.ratioText}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get wrapperElement() {
|
||||||
|
return element(this.args.tagName ?? "td");
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<this.wrapperElement
|
||||||
|
class="num posts-map posts {{this.likesHeat}} topic-list-data"
|
||||||
|
>
|
||||||
|
<TopicEntrance
|
||||||
|
@topic={{@topic}}
|
||||||
|
@title={{this.title}}
|
||||||
|
@triggerClass="btn-link posts-map badge-posts {{this.likesHeat}}"
|
||||||
|
>
|
||||||
|
<PluginOutlet @name="topic-list-before-reply-count" />
|
||||||
|
{{number @topic.replyCount noTitle="true"}}
|
||||||
|
</TopicEntrance>
|
||||||
|
</this.wrapperElement>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import BulkSelectTopicsDropdown from "discourse/components/bulk-select-topics-dropdown";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
|
||||||
|
const TopicBulkSelectDropdown = <template>
|
||||||
|
<div class="bulk-select-topics-dropdown">
|
||||||
|
<span class="bulk-select-topic-dropdown__count">
|
||||||
|
{{i18n
|
||||||
|
"topics.bulk.selected_count"
|
||||||
|
count=@bulkSelectHelper.selected.length
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<BulkSelectTopicsDropdown @bulkSelectHelper={{@bulkSelectHelper}} />
|
||||||
|
</div>
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default TopicBulkSelectDropdown;
|
|
@ -0,0 +1,101 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { fn } from "@ember/helper";
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import DiscourseURL from "discourse/lib/url";
|
||||||
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
|
import DMenu from "float-kit/components/d-menu";
|
||||||
|
|
||||||
|
function entranceDate(dt, showTime) {
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
if (dt.toDateString() === today.toDateString()) {
|
||||||
|
return moment(dt).format(I18n.t("dates.time"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dt.getYear() === today.getYear()) {
|
||||||
|
// No year
|
||||||
|
return moment(dt).format(
|
||||||
|
showTime
|
||||||
|
? I18n.t("dates.long_date_without_year_with_linebreak")
|
||||||
|
: I18n.t("dates.long_no_year_no_time")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return moment(dt).format(
|
||||||
|
showTime
|
||||||
|
? I18n.t("dates.long_date_with_year_with_linebreak")
|
||||||
|
: I18n.t("dates.long_date_with_year_without_time")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TopicEntrance extends Component {
|
||||||
|
@service historyStore;
|
||||||
|
|
||||||
|
get createdDate() {
|
||||||
|
return new Date(this.args.topic.created_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
get bumpedDate() {
|
||||||
|
return new Date(this.args.topic.bumped_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
get showTime() {
|
||||||
|
return (
|
||||||
|
this.bumpedDate.getTime() - this.createdDate.getTime() <
|
||||||
|
1000 * 60 * 60 * 24 * 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get topDate() {
|
||||||
|
return entranceDate(this.createdDate, this.showTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
get bottomDate() {
|
||||||
|
return entranceDate(this.bumpedDate, this.showTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
jumpTo(destination) {
|
||||||
|
this.historyStore.set("lastTopicIdViewed", this.args.topic.id);
|
||||||
|
DiscourseURL.routeTo(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DMenu
|
||||||
|
@ariaLabel={{@title}}
|
||||||
|
@placement="center"
|
||||||
|
@autofocus={{true}}
|
||||||
|
@triggerClass={{@triggerClass}}
|
||||||
|
>
|
||||||
|
<:trigger>
|
||||||
|
{{yield}}
|
||||||
|
</:trigger>
|
||||||
|
|
||||||
|
<:content>
|
||||||
|
<div id="topic-entrance" class="--glimmer">
|
||||||
|
<button
|
||||||
|
{{on "click" (fn this.jumpTo @topic.url)}}
|
||||||
|
aria-label="topic_entrance.sr_jump_top_button"
|
||||||
|
class="btn btn-default full jump-top"
|
||||||
|
>
|
||||||
|
{{icon "step-backward"}}
|
||||||
|
{{htmlSafe this.topDate}}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
{{on "click" (fn this.jumpTo @topic.lastPostUrl)}}
|
||||||
|
aria-label="topic_entrance.sr_jump_bottom_button"
|
||||||
|
class="btn btn-default full jump-bottom"
|
||||||
|
>
|
||||||
|
{{htmlSafe this.bottomDate}}
|
||||||
|
{{icon "step-forward"}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</:content>
|
||||||
|
</DMenu>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import dirSpan from "discourse/helpers/dir-span";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
|
||||||
|
const TopicExcerpt = <template>
|
||||||
|
{{#if @topic.hasExcerpt}}
|
||||||
|
<a href={{@topic.url}} class="topic-excerpt">
|
||||||
|
{{dirSpan @topic.escapedExcerpt htmlSafe="true"}}
|
||||||
|
|
||||||
|
{{#if @topic.excerptTruncated}}
|
||||||
|
<span class="topic-excerpt-more">{{i18n "read_more"}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default TopicExcerpt;
|
|
@ -0,0 +1,21 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
|
||||||
|
export default class TopicLink extends Component {
|
||||||
|
get url() {
|
||||||
|
return this.args.topic.linked_post_number
|
||||||
|
? this.args.topic.urlForPostNumber(this.args.topic.linked_post_number)
|
||||||
|
: this.args.topic.lastUnreadUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a
|
||||||
|
href={{this.url}}
|
||||||
|
data-topic-id={{@topic.id}}
|
||||||
|
role="heading"
|
||||||
|
aria-level="2"
|
||||||
|
class="title"
|
||||||
|
...attributes
|
||||||
|
>{{htmlSafe @topic.fancyTitle}}</a>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import TopicBulkActions from "discourse/components/modal/topic-bulk-actions";
|
||||||
|
import NewListHeaderControls from "discourse/components/topic-list/new-list-header-controls";
|
||||||
|
import TopicBulkSelectDropdown from "discourse/components/topic-list/topic-bulk-select-dropdown";
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
|
||||||
|
export default class TopicListHeaderColumn extends Component {
|
||||||
|
@service modal;
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
get localizedName() {
|
||||||
|
if (this.args.forceName) {
|
||||||
|
return this.args.forceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.args.name ? i18n(this.args.name) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSorting() {
|
||||||
|
return this.args.sortable && this.args.order === this.args.activeOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
get ariaSort() {
|
||||||
|
if (this.isSorting) {
|
||||||
|
return this.args.ascending ? "ascending" : "descending";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this code probably shouldn't be in all columns
|
||||||
|
@action
|
||||||
|
bulkSelectAll() {
|
||||||
|
this.args.bulkSelectHelper.autoAddTopicsToBulkSelect = true;
|
||||||
|
document
|
||||||
|
.querySelectorAll("input.bulk-select:not(:checked)")
|
||||||
|
.forEach((el) => el.click());
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
bulkClearAll() {
|
||||||
|
this.args.bulkSelectHelper.autoAddTopicsToBulkSelect = false;
|
||||||
|
document
|
||||||
|
.querySelectorAll("input.bulk-select:checked")
|
||||||
|
.forEach((el) => el.click());
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
bulkSelectActions() {
|
||||||
|
this.modal.show(TopicBulkActions, {
|
||||||
|
model: {
|
||||||
|
topics: this.args.bulkSelectHelper.selected,
|
||||||
|
category: this.category,
|
||||||
|
refreshClosure: () => this.router.refresh(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onClick() {
|
||||||
|
this.args.changeSort(this.args.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onKeyDown(event) {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
this.args.changeSort(this.args.order);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<th
|
||||||
|
{{(if @sortable (modifier on "click" this.onClick))}}
|
||||||
|
{{(if @sortable (modifier on "keydown" this.onKeyDown))}}
|
||||||
|
data-sort-order={{@order}}
|
||||||
|
scope="col"
|
||||||
|
tabindex={{if @sortable "0"}}
|
||||||
|
role={{if @sortable "button"}}
|
||||||
|
aria-pressed={{this.isSorting}}
|
||||||
|
aria-sort={{this.ariaSort}}
|
||||||
|
class={{concatClass
|
||||||
|
"topic-list-data"
|
||||||
|
@order
|
||||||
|
(if @sortable "sortable")
|
||||||
|
(if @isSorting "sorting")
|
||||||
|
(if @number "num")
|
||||||
|
}}
|
||||||
|
...attributes
|
||||||
|
>
|
||||||
|
{{#if @canBulkSelect}}
|
||||||
|
{{#if @showBulkToggle}}
|
||||||
|
<button
|
||||||
|
{{on "click" @bulkSelectHelper.toggleBulkSelect}}
|
||||||
|
title={{i18n "topics.bulk.toggle"}}
|
||||||
|
class="btn-flat bulk-select"
|
||||||
|
>
|
||||||
|
{{icon (if @experimentalTopicBulkActionsEnabled "tasks" "list")}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if @bulkSelectEnabled}}
|
||||||
|
<span class="bulk-select-topics">
|
||||||
|
{{#if @canDoBulkActions}}
|
||||||
|
{{#if @experimentalTopicBulkActionsEnabled}}
|
||||||
|
<TopicBulkSelectDropdown
|
||||||
|
@bulkSelectHelper={{@bulkSelectHelper}}
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<button
|
||||||
|
{{on "click" this.bulkSelectActions}}
|
||||||
|
class="btn btn-icon no-text bulk-select-actions"
|
||||||
|
>{{icon "cog"}}​</button>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<button
|
||||||
|
{{on "click" this.bulkSelectAll}}
|
||||||
|
class="btn btn-default bulk-select-all"
|
||||||
|
>{{i18n "topics.bulk.select_all"}}</button>
|
||||||
|
<button
|
||||||
|
{{on "click" this.bulkClearAll}}
|
||||||
|
class="btn btn-default bulk-clear-all"
|
||||||
|
>{{i18n "topics.bulk.clear_all"}}</button>
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#unless @bulkSelectEnabled}}
|
||||||
|
{{#if this.showTopicsAndRepliesToggle}}
|
||||||
|
<NewListHeaderControls
|
||||||
|
@current={{@newListSubset}}
|
||||||
|
@newRepliesCount={{@newRepliesCount}}
|
||||||
|
@newTopicsCount={{@newTopicsCount}}
|
||||||
|
@changeNewListSubset={{@changeNewListSubset}}
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<span>{{this.localizedName}}</span>
|
||||||
|
{{/if}}
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
{{#if this.isSorting}}
|
||||||
|
{{icon (if @ascending "chevron-up" "chevron-down")}}
|
||||||
|
{{/if}}
|
||||||
|
</th>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||||
|
import TopicListHeaderColumn from "discourse/components/topic-list/topic-list-header-column";
|
||||||
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
|
||||||
|
const TopicListHeader = <template>
|
||||||
|
<PluginOutlet @name="topic-list-header-before" />
|
||||||
|
|
||||||
|
{{#if @bulkSelectEnabled}}
|
||||||
|
<th class="bulk-select topic-list-data">
|
||||||
|
{{#if @canBulkSelect}}
|
||||||
|
<button
|
||||||
|
{{on "click" @bulkSelectHelper.toggleBulkSelect}}
|
||||||
|
title={{i18n "topics.bulk.toggle"}}
|
||||||
|
class="btn-flat bulk-select"
|
||||||
|
>
|
||||||
|
{{icon (if @experimentalTopicBulkActionsEnabled "tasks" "list")}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
</th>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<TopicListHeaderColumn
|
||||||
|
@order="default"
|
||||||
|
@category={{@category}}
|
||||||
|
@activeOrder={{@order}}
|
||||||
|
@changeSort={{@changeSort}}
|
||||||
|
@ascending={{@ascending}}
|
||||||
|
@name={{@listTitle}}
|
||||||
|
@bulkSelectEnabled={{@bulkSelectEnabled}}
|
||||||
|
@showBulkToggle={{@toggleInTitle}}
|
||||||
|
@canBulkSelect={{@canBulkSelect}}
|
||||||
|
@canDoBulkActions={{@canDoBulkActions}}
|
||||||
|
@showTopicsAndRepliesToggle={{@showTopicsAndRepliesToggle}}
|
||||||
|
@newListSubset={{@newListSubset}}
|
||||||
|
@newRepliesCount={{@newRepliesCount}}
|
||||||
|
@newTopicsCount={{@newTopicsCount}}
|
||||||
|
@experimentalTopicBulkActionsEnabled={{@experimentalTopicBulkActionsEnabled}}
|
||||||
|
@bulkSelectHelper={{@bulkSelectHelper}}
|
||||||
|
@changeNewListSubset={{@changeNewListSubset}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PluginOutlet @name="topic-list-header-after-main-link" />
|
||||||
|
|
||||||
|
{{#if @showPosters}}
|
||||||
|
<TopicListHeaderColumn
|
||||||
|
@order="posters"
|
||||||
|
@activeOrder={{@order}}
|
||||||
|
@changeSort={{@changeSort}}
|
||||||
|
@ascending={{@ascending}}
|
||||||
|
aria-label={{i18n "category.sort_options.posters"}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<TopicListHeaderColumn
|
||||||
|
@sortable={{@sortable}}
|
||||||
|
@number="true"
|
||||||
|
@order="posts"
|
||||||
|
@activeOrder={{@order}}
|
||||||
|
@changeSort={{@changeSort}}
|
||||||
|
@ascending={{@ascending}}
|
||||||
|
@name="replies"
|
||||||
|
aria-label={{i18n "sr_replies"}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#if @showLikes}}
|
||||||
|
<TopicListHeaderColumn
|
||||||
|
@sortable={{@sortable}}
|
||||||
|
@number="true"
|
||||||
|
@order="likes"
|
||||||
|
@activeOrder={{@order}}
|
||||||
|
@changeSort={{@changeSort}}
|
||||||
|
@ascending={{@ascending}}
|
||||||
|
@name="likes"
|
||||||
|
aria-label={{i18n "sr_likes"}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if @showOpLikes}}
|
||||||
|
<TopicListHeaderColumn
|
||||||
|
@sortable={{@sortable}}
|
||||||
|
@number="true"
|
||||||
|
@order="op_likes"
|
||||||
|
@activeOrder={{@order}}
|
||||||
|
@changeSort={{@changeSort}}
|
||||||
|
@ascending={{@ascending}}
|
||||||
|
@name="likes"
|
||||||
|
aria-label={{i18n "sr_op_likes"}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<TopicListHeaderColumn
|
||||||
|
@sortable={{@sortable}}
|
||||||
|
@number="true"
|
||||||
|
@order="views"
|
||||||
|
@activeOrder={{@order}}
|
||||||
|
@changeSort={{@changeSort}}
|
||||||
|
@ascending={{@ascending}}
|
||||||
|
@name="views"
|
||||||
|
aria-label={{i18n "sr_views"}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TopicListHeaderColumn
|
||||||
|
@sortable={{@sortable}}
|
||||||
|
@number="true"
|
||||||
|
@order="activity"
|
||||||
|
@activeOrder={{@order}}
|
||||||
|
@changeSort={{@changeSort}}
|
||||||
|
@ascending={{@ascending}}
|
||||||
|
@name="activity"
|
||||||
|
aria-label={{i18n "sr_activity"}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PluginOutlet @name="topic-list-header-after" />
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default TopicListHeader;
|
|
@ -0,0 +1,503 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { concat, hash } from "@ember/helper";
|
||||||
|
import { on } from "@ember/modifier";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import { eq, gt } from "truth-helpers";
|
||||||
|
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||||
|
import ActionList from "discourse/components/topic-list/action-list";
|
||||||
|
import ActivityColumn from "discourse/components/topic-list/activity-column";
|
||||||
|
import ParticipantGroups from "discourse/components/topic-list/participant-groups";
|
||||||
|
import PostCountOrBadges from "discourse/components/topic-list/post-count-or-badges";
|
||||||
|
import PostersColumn from "discourse/components/topic-list/posters-column";
|
||||||
|
import PostsCountColumn from "discourse/components/topic-list/posts-count-column";
|
||||||
|
import TopicExcerpt from "discourse/components/topic-list/topic-excerpt";
|
||||||
|
import TopicLink from "discourse/components/topic-list/topic-link";
|
||||||
|
import UnreadIndicator from "discourse/components/topic-list/unread-indicator";
|
||||||
|
import TopicPostBadges from "discourse/components/topic-post-badges";
|
||||||
|
import TopicStatus from "discourse/components/topic-status";
|
||||||
|
import { topicTitleDecorators } from "discourse/components/topic-title";
|
||||||
|
import avatar from "discourse/helpers/avatar";
|
||||||
|
import categoryLink from "discourse/helpers/category-link";
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import discourseTags from "discourse/helpers/discourse-tags";
|
||||||
|
import formatDate from "discourse/helpers/format-date";
|
||||||
|
import number from "discourse/helpers/number";
|
||||||
|
import topicFeaturedLink from "discourse/helpers/topic-featured-link";
|
||||||
|
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||||
|
import DiscourseURL, { groupPath } from "discourse/lib/url";
|
||||||
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
import I18n from "discourse-i18n";
|
||||||
|
|
||||||
|
export default class TopicListItem extends Component {
|
||||||
|
@service appEvents;
|
||||||
|
@service currentUser;
|
||||||
|
@service historyStore;
|
||||||
|
@service messageBus;
|
||||||
|
@service router;
|
||||||
|
@service site;
|
||||||
|
@service siteSettings;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
if (this.includeUnreadIndicator) {
|
||||||
|
this.messageBus.subscribe(this.unreadIndicatorChannel, this.onMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
super.willDestroy(...arguments);
|
||||||
|
|
||||||
|
this.messageBus.unsubscribe(this.unreadIndicatorChannel, this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
onMessage(data) {
|
||||||
|
const nodeClassList = document.querySelector(
|
||||||
|
`.indicator-topic-${data.topic_id}`
|
||||||
|
).classList;
|
||||||
|
|
||||||
|
nodeClassList.toggle("read", !data.show_indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
get unreadIndicatorChannel() {
|
||||||
|
return `/private-messages/unread-indicator/${this.args.topic.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get includeUnreadIndicator() {
|
||||||
|
return typeof this.args.topic.unread_by_group_member !== "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSelected() {
|
||||||
|
return this.args.selected?.includes(this.args.topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
get participantGroups() {
|
||||||
|
if (!this.args.topic.participant_groups) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.args.topic.participant_groups.map((name) => ({
|
||||||
|
name,
|
||||||
|
url: groupPath(name),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
get newDotText() {
|
||||||
|
return this.currentUser?.trust_level > 0
|
||||||
|
? ""
|
||||||
|
: I18n.t("filters.new.lower_title");
|
||||||
|
}
|
||||||
|
|
||||||
|
get tagClassNames() {
|
||||||
|
return this.args.topic.tags?.map((tagName) => `tag-${tagName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
get expandPinned() {
|
||||||
|
if (
|
||||||
|
!this.args.topic.pinned ||
|
||||||
|
(this.site.mobileView && !this.siteSettings.show_pinned_excerpt_mobile) ||
|
||||||
|
(this.site.desktopView && !this.siteSettings.show_pinned_excerpt_desktop)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
(this.args.expandGloballyPinned && this.args.topic.pinned_globally) ||
|
||||||
|
this.args.expandAllPinned
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get shouldFocusLastVisited() {
|
||||||
|
return this.site.desktopView && this.args.focusLastVisitedTopic;
|
||||||
|
}
|
||||||
|
|
||||||
|
get unreadClass() {
|
||||||
|
return this.args.topic.unread_by_group_member ? "" : "read";
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToTopic(topic, href) {
|
||||||
|
this.historyStore.set("lastTopicIdViewed", topic.id);
|
||||||
|
DiscourseURL.routeTo(href || topic.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight(element, isLastViewedTopic) {
|
||||||
|
element.classList.add("highlighted");
|
||||||
|
element.setAttribute("data-islastviewedtopic", isLastViewedTopic);
|
||||||
|
element.addEventListener(
|
||||||
|
"animationend",
|
||||||
|
() => element.classList.remove("highlighted"),
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLastViewedTopic && this.shouldFocusLastVisited) {
|
||||||
|
element.querySelector(".main-link .title")?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
highlightIfNeeded(element) {
|
||||||
|
if (this.args.topic.id === this.historyStore.get("lastTopicIdViewed")) {
|
||||||
|
this.historyStore.delete("lastTopicIdViewed");
|
||||||
|
this.highlight(element, true);
|
||||||
|
} else if (this.args.topic.highlight) {
|
||||||
|
// highlight new topics that have been loaded from the server or the one we just created
|
||||||
|
this.args.topic.set("highlight", false);
|
||||||
|
this.highlight(element, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onTitleFocus(event) {
|
||||||
|
event.target.classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onTitleBlur(event) {
|
||||||
|
event.target.classList.remove("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
applyTitleDecorators(element) {
|
||||||
|
const rawTopicLink = element.querySelector(".raw-topic-link");
|
||||||
|
|
||||||
|
if (rawTopicLink) {
|
||||||
|
topicTitleDecorators?.forEach((cb) =>
|
||||||
|
cb(this.args.topic, rawTopicLink, "topic-list-item-title")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onBulkSelectToggle(e) {
|
||||||
|
if (e.target.checked) {
|
||||||
|
this.args.selected.addObject(this.args.topic);
|
||||||
|
|
||||||
|
if (this.args.lastCheckedElementId && e.shiftKey) {
|
||||||
|
const bulkSelects = Array.from(
|
||||||
|
document.querySelectorAll("input.bulk-select")
|
||||||
|
);
|
||||||
|
const from = bulkSelects.indexOf(e.target);
|
||||||
|
const to = bulkSelects.findIndex(
|
||||||
|
(el) => el.id === this.args.lastCheckedElementId
|
||||||
|
);
|
||||||
|
const start = Math.min(from, to);
|
||||||
|
const end = Math.max(from, to);
|
||||||
|
|
||||||
|
bulkSelects
|
||||||
|
.slice(start, end)
|
||||||
|
.filter((el) => !el.checked)
|
||||||
|
.forEach((checkbox) => checkbox.click());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.args.updateLastCheckedElementId(e.target.id);
|
||||||
|
} else {
|
||||||
|
this.args.selected.removeObject(this.args.topic);
|
||||||
|
this.args.updateLastCheckedElementId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
click(e) {
|
||||||
|
if (
|
||||||
|
e.target.classList.contains("raw-topic-link") ||
|
||||||
|
e.target.classList.contains("post-activity")
|
||||||
|
) {
|
||||||
|
if (wantsNewWindow(e)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
this.navigateToTopic(this.args.topic, e.target.href);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make full row click target on mobile, due to size constraints
|
||||||
|
if (
|
||||||
|
this.site.mobileView &&
|
||||||
|
e.target.matches(
|
||||||
|
".topic-list-data, .main-link, .right, .topic-item-stats, .topic-item-stats__category-tags, .discourse-tags"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (wantsNewWindow(e)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
this.navigateToTopic(this.args.topic, this.args.topic.lastUnreadUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
e.target.classList.contains("d-icon-thumbtack") &&
|
||||||
|
e.target.closest("a.topic-status")
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.args.topic.togglePinnedForUser();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
keyDown(e) {
|
||||||
|
if (e.key === "Enter" && e.target.classList.contains("post-activity")) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.navigateToTopic(this.args.topic, e.target.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tr
|
||||||
|
{{! template-lint-disable no-invalid-interactive }}
|
||||||
|
{{didInsert this.applyTitleDecorators}}
|
||||||
|
{{didInsert this.highlightIfNeeded}}
|
||||||
|
{{on "keydown" this.keyDown}}
|
||||||
|
{{on "click" this.click}}
|
||||||
|
data-topic-id={{@topic.id}}
|
||||||
|
role={{this.role}}
|
||||||
|
aria-level={{this.ariaLevel}}
|
||||||
|
class={{concatClass
|
||||||
|
"topic-list-item"
|
||||||
|
(if @topic.category (concat "category-" @topic.category.fullSlug))
|
||||||
|
(if (eq @topic @lastVisitedTopic) "last-visit")
|
||||||
|
(if @topic.visited "visited")
|
||||||
|
(if @topic.hasExcerpt "has-excerpt")
|
||||||
|
(if @topic.unseen "unseen-topic")
|
||||||
|
(if @topic.unread_posts "unread-posts")
|
||||||
|
(if @topic.liked "liked")
|
||||||
|
(if @topic.archived "archived")
|
||||||
|
(if @topic.bookmarked "bookmarked")
|
||||||
|
(if @topic.pinned "pinned")
|
||||||
|
(if @topic.closed "closed")
|
||||||
|
this.tagClassNames
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PluginOutlet
|
||||||
|
@name="above-topic-list-item"
|
||||||
|
@outletArgs={{hash topic=@topic}}
|
||||||
|
/>
|
||||||
|
{{#if this.site.desktopView}}
|
||||||
|
<PluginOutlet @name="topic-list-before-columns" />
|
||||||
|
|
||||||
|
{{#if @bulkSelectEnabled}}
|
||||||
|
<td class="bulk-select topic-list-data">
|
||||||
|
<label for="bulk-select-{{@topic.id}}">
|
||||||
|
<input
|
||||||
|
{{on "click" this.onBulkSelectToggle}}
|
||||||
|
checked={{this.isSelected}}
|
||||||
|
type="checkbox"
|
||||||
|
id="bulk-select-{{@topic.id}}"
|
||||||
|
class="bulk-select"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<td class="main-link clearfix topic-list-data" colspan="1">
|
||||||
|
<PluginOutlet @name="topic-list-before-link" />
|
||||||
|
|
||||||
|
<span class="link-top-line">{{! no whitespace
|
||||||
|
}}<PluginOutlet
|
||||||
|
@name="topic-list-before-status"
|
||||||
|
/>{{! no whitespace
|
||||||
|
}}<TopicStatus
|
||||||
|
@topic={{@topic}}
|
||||||
|
/>{{! no whitespace
|
||||||
|
}}<TopicLink
|
||||||
|
{{on "focus" this.onTitleFocus}}
|
||||||
|
{{on "blur" this.onTitleBlur}}
|
||||||
|
@topic={{@topic}}
|
||||||
|
class="raw-link raw-topic-link"
|
||||||
|
/>
|
||||||
|
{{~#if @topic.featured_link~}}
|
||||||
|
|
||||||
|
{{~topicFeaturedLink @topic}}
|
||||||
|
{{~/if~}}
|
||||||
|
<PluginOutlet
|
||||||
|
@name="topic-list-after-title"
|
||||||
|
/>{{! no whitespace
|
||||||
|
}}
|
||||||
|
<UnreadIndicator
|
||||||
|
@includeUnreadIndicator={{this.includeUnreadIndicator}}
|
||||||
|
@topicId={{@topic.id}}
|
||||||
|
class={{this.unreadClass}}
|
||||||
|
/>
|
||||||
|
{{~#if @showTopicPostBadges~}}
|
||||||
|
<TopicPostBadges
|
||||||
|
@unreadPosts={{@topic.unread_posts}}
|
||||||
|
@unseen={{@topic.unseen}}
|
||||||
|
@newDotText={{this.newDotText}}
|
||||||
|
@url={{@topic.lastUnreadUrl}}
|
||||||
|
/>
|
||||||
|
{{~/if~}}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="link-bottom-line">
|
||||||
|
{{#unless @hideCategory}}
|
||||||
|
{{#unless @topic.isPinnedUncategorized}}
|
||||||
|
<PluginOutlet @name="topic-list-before-category" />
|
||||||
|
{{categoryLink @topic.category}}
|
||||||
|
{{/unless}}
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
{{discourseTags @topic mode="list" tagsForUser=@tagsForUser}}
|
||||||
|
|
||||||
|
{{#if this.participantGroups}}
|
||||||
|
<ParticipantGroups @groups={{this.participantGroups}} />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<ActionList
|
||||||
|
@topic={{@topic}}
|
||||||
|
@postNumbers={{@topic.liked_post_numbers}}
|
||||||
|
@icon="heart"
|
||||||
|
class="likes"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if this.expandPinned}}
|
||||||
|
<TopicExcerpt @topic={{@topic}} />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<PluginOutlet @name="topic-list-main-link-bottom" />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<PluginOutlet @name="topic-list-after-main-link" />
|
||||||
|
|
||||||
|
{{#if @showPosters}}
|
||||||
|
<PostersColumn @posters={{@topic.featuredUsers}} />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<PostsCountColumn @topic={{@topic}} />
|
||||||
|
|
||||||
|
{{#if @showLikes}}
|
||||||
|
<td class="num likes topic-list-data">
|
||||||
|
{{#if (gt @topic.like_count 0)}}
|
||||||
|
<a href={{@topic.summaryUrl}}>
|
||||||
|
{{number @topic.like_count}}
|
||||||
|
{{icon "heart"}}
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if @showOpLikes}}
|
||||||
|
<td class="num likes">
|
||||||
|
{{#if (gt @topic.op_like_count 0)}}
|
||||||
|
<a href={{@topic.summaryUrl}}>
|
||||||
|
{{number @topic.op_like_count}}
|
||||||
|
{{icon "heart"}}
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
</td>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<td class={{concatClass "num views topic-list-data" @topic.viewsHeat}}>
|
||||||
|
<PluginOutlet @name="topic-list-before-view-count" />
|
||||||
|
{{number @topic.views numberKey="views_long"}}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<ActivityColumn @topic={{@topic}} class="num topic-list-data" />
|
||||||
|
|
||||||
|
<PluginOutlet @name="topic-list-after-columns" />
|
||||||
|
{{else}}
|
||||||
|
<td class="topic-list-data">
|
||||||
|
<PluginOutlet @name="topic-list-before-columns" />
|
||||||
|
|
||||||
|
<div class="pull-left">
|
||||||
|
{{#if @bulkSelectEnabled}}
|
||||||
|
<label for="bulk-select-{{@topic.id}}">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="bulk-select-{{@topic.id}}"
|
||||||
|
class="bulk-select"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{{else}}
|
||||||
|
<a
|
||||||
|
href={{@topic.lastPostUrl}}
|
||||||
|
aria-label={{i18n
|
||||||
|
"latest_poster_link"
|
||||||
|
username=@topic.lastPosterUser.username
|
||||||
|
}}
|
||||||
|
data-user-card={{@topic.lastPosterUser.username}}
|
||||||
|
>{{avatar @topic.lastPosterUser imageSize="large"}}</a>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="topic-item-metadata right"
|
||||||
|
>{{! no whitespace
|
||||||
|
}}<PluginOutlet
|
||||||
|
@name="topic-list-before-link"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="main-link"
|
||||||
|
>{{! no whitespace
|
||||||
|
}}<PluginOutlet
|
||||||
|
@name="topic-list-before-status"
|
||||||
|
/>{{! no whitespace
|
||||||
|
}}<TopicStatus
|
||||||
|
@topic={{@topic}}
|
||||||
|
/>{{! no whitespace
|
||||||
|
}}<TopicLink
|
||||||
|
{{on "focus" this.onTitleFocus}}
|
||||||
|
{{on "blur" this.onTitleBlur}}
|
||||||
|
@topic={{@topic}}
|
||||||
|
class="raw-link raw-topic-link"
|
||||||
|
/>
|
||||||
|
{{~#if @topic.featured_link~}}
|
||||||
|
{{topicFeaturedLink @topic}}
|
||||||
|
{{~/if~}}
|
||||||
|
<PluginOutlet @name="topic-list-after-title" />
|
||||||
|
{{~#if @topic.unseen~}}
|
||||||
|
<span class="topic-post-badges"> <span
|
||||||
|
class="badge-notification new-topic"
|
||||||
|
></span></span>
|
||||||
|
{{~/if~}}
|
||||||
|
{{~#if this.expandPinned~}}
|
||||||
|
<TopicExcerpt @topic={{@topic}} />
|
||||||
|
{{~/if~}}
|
||||||
|
<PluginOutlet @name="topic-list-main-link-bottom" />
|
||||||
|
</div>{{! no whitespace
|
||||||
|
}}<PluginOutlet
|
||||||
|
@name="topic-list-after-main-link"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="pull-right">
|
||||||
|
<PostCountOrBadges
|
||||||
|
@topic={{@topic}}
|
||||||
|
@postBadgesEnabled={{@showTopicPostBadges}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="topic-item-stats clearfix">
|
||||||
|
<span class="topic-item-stats__category-tags">
|
||||||
|
{{#unless @hideCategory}}
|
||||||
|
<PluginOutlet @name="topic-list-before-category" />
|
||||||
|
{{categoryLink @topic.category}}
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
{{discourseTags @topic mode="list"}}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="num activity last">
|
||||||
|
<span title={{@topic.bumpedAtTitle}} class="age activity">
|
||||||
|
<a href={{@topic.lastPostUrl}}>{{formatDate
|
||||||
|
@topic.bumpedAt
|
||||||
|
format="tiny"
|
||||||
|
noTitle="true"
|
||||||
|
}}</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{{/if}}
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { concat } from "@ember/helper";
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
|
||||||
|
const UnreadIndicator = <template>
|
||||||
|
{{#if @includeUnreadIndicator~}}
|
||||||
|
<span
|
||||||
|
title={{i18n "topic.unread_indicator"}}
|
||||||
|
class={{concatClass
|
||||||
|
"badge badge-notification unread-indicator"
|
||||||
|
(concat "indicator-topic-" @topicId)
|
||||||
|
}}
|
||||||
|
...attributes
|
||||||
|
>
|
||||||
|
{{~icon "asterisk"~}}
|
||||||
|
</span>
|
||||||
|
{{~/if}}
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default UnreadIndicator;
|
|
@ -3,24 +3,37 @@
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
background: var(--secondary);
|
background: var(--secondary);
|
||||||
box-shadow: var(--shadow-card);
|
box-shadow: var(--shadow-card);
|
||||||
z-index: z("dropdown");
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
width: 133px;
|
width: 133px;
|
||||||
@include unselectable;
|
@include unselectable;
|
||||||
|
|
||||||
|
&:not(.--glimmer) {
|
||||||
|
z-index: z("dropdown");
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
button.full .d-icon {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.--glimmer {
|
||||||
|
button.full {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button.full {
|
button.full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.d-icon {
|
.d-icon {
|
||||||
display: block;
|
|
||||||
margin: 2px auto;
|
margin: 2px auto;
|
||||||
width: 100%;
|
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button.btn.jump-bottom {
|
button.btn.jump-bottom {
|
||||||
margin: 5px 0 0 0;
|
margin: 5px 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,12 @@
|
||||||
.title {
|
.title {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.--glimmer button.-trigger {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody {
|
tbody {
|
||||||
|
|
|
@ -74,7 +74,8 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||||
:new_new_view_enabled?,
|
:new_new_view_enabled?,
|
||||||
:use_experimental_topic_bulk_actions?,
|
:use_experimental_topic_bulk_actions?,
|
||||||
:use_admin_sidebar,
|
:use_admin_sidebar,
|
||||||
:can_view_raw_email
|
:can_view_raw_email,
|
||||||
|
:use_glimmer_topic_list?
|
||||||
|
|
||||||
delegate :user_stat, to: :object, private: true
|
delegate :user_stat, to: :object, private: true
|
||||||
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
|
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
|
||||||
|
@ -314,4 +315,8 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||||
def can_view_raw_email
|
def can_view_raw_email
|
||||||
scope.user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
|
scope.user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def use_glimmer_topic_list?
|
||||||
|
scope.user.in_any_groups?(SiteSetting.experimental_glimmer_topic_list_groups_map)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2647,6 +2647,7 @@ en:
|
||||||
enable_experimental_lightbox: "EXPERIMENTAL: Replace the default image lightbox with the revamped design."
|
enable_experimental_lightbox: "EXPERIMENTAL: Replace the default image lightbox with the revamped design."
|
||||||
enable_experimental_bookmark_redesign_groups: "EXPERIMENTAL: Show a quick access menu for bookmarks on posts and a new redesigned modal"
|
enable_experimental_bookmark_redesign_groups: "EXPERIMENTAL: Show a quick access menu for bookmarks on posts and a new redesigned modal"
|
||||||
glimmer_header_mode: "Control whether the new 'glimmer' header implementation is used. Defaults to 'auto', which will enable automatically once all your themes and plugins are ready. https://meta.discourse.org/t/296544"
|
glimmer_header_mode: "Control whether the new 'glimmer' header implementation is used. Defaults to 'auto', which will enable automatically once all your themes and plugins are ready. https://meta.discourse.org/t/296544"
|
||||||
|
experimental_glimmer_topic_list_groups: "EXPERIMENTAL: Enable the new 'glimmer' topic list implementation. This implementation is under active development, and is not intended for production use. Do not develop themes/plugins against it until the implementation is finalized and announced."
|
||||||
experimental_form_templates: "EXPERIMENTAL: Enable the form templates feature. <b>After enabled,</b> manage the templates at <a href='%{base_path}/admin/customize/form-templates'>Customize / Templates</a>."
|
experimental_form_templates: "EXPERIMENTAL: Enable the form templates feature. <b>After enabled,</b> manage the templates at <a href='%{base_path}/admin/customize/form-templates'>Customize / Templates</a>."
|
||||||
admin_sidebar_enabled_groups: "EXPERIMENTAL: Enable sidebar navigation for the admin UI for the specified groups, which replaces the top-level admin navigation buttons."
|
admin_sidebar_enabled_groups: "EXPERIMENTAL: Enable sidebar navigation for the admin UI for the specified groups, which replaces the top-level admin navigation buttons."
|
||||||
lazy_load_categories_groups: "EXPERIMENTAL: Lazy load category information only for users of these groups. This improves performance on sites with many categories."
|
lazy_load_categories_groups: "EXPERIMENTAL: Lazy load category information only for users of these groups. This improves performance on sites with many categories."
|
||||||
|
|
|
@ -2352,6 +2352,13 @@ developer:
|
||||||
default: ""
|
default: ""
|
||||||
allow_any: false
|
allow_any: false
|
||||||
refresh: true
|
refresh: true
|
||||||
|
experimental_glimmer_topic_list_groups:
|
||||||
|
client: true
|
||||||
|
type: group_list
|
||||||
|
list_type: compact
|
||||||
|
default: ""
|
||||||
|
allow_any: false
|
||||||
|
refresh: true
|
||||||
enable_experimental_lightbox:
|
enable_experimental_lightbox:
|
||||||
default: false
|
default: false
|
||||||
client: true
|
client: true
|
||||||
|
|
|
@ -31,5 +31,9 @@
|
||||||
</StyleguideExample>
|
</StyleguideExample>
|
||||||
|
|
||||||
<StyleguideExample @title="<TopicListItem> - latest" class="half-size">
|
<StyleguideExample @title="<TopicListItem> - latest" class="half-size">
|
||||||
<LatestTopicListItem @topic={{@dummy.topic}} />
|
{{#if this.currentUser.use_glimmer_topic_list}}
|
||||||
|
<TopicList::LatestTopicListItem @topic={{@dummy.topic}} />
|
||||||
|
{{else}}
|
||||||
|
<LatestTopicListItem @topic={{@dummy.topic}} />
|
||||||
|
{{/if}}
|
||||||
</StyleguideExample>
|
</StyleguideExample>
|
|
@ -3,6 +3,8 @@
|
||||||
module PageObjects
|
module PageObjects
|
||||||
module Components
|
module Components
|
||||||
class CategoryList < PageObjects::Components::Base
|
class CategoryList < PageObjects::Components::Base
|
||||||
|
TOPIC_LIST_ITEM_SELECTOR = ".category-list.with-topics .featured-topic"
|
||||||
|
|
||||||
def has_category?(category)
|
def has_category?(category)
|
||||||
page.has_css?("tr[data-category-id='#{category.id}']")
|
page.has_css?("tr[data-category-id='#{category.id}']")
|
||||||
end
|
end
|
||||||
|
@ -30,6 +32,10 @@ module PageObjects
|
||||||
def click_topic(topic)
|
def click_topic(topic)
|
||||||
page.find("a", text: topic.title).click
|
page.find("a", text: topic.title).click
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def topic_list_item_class(topic)
|
||||||
|
"#{TOPIC_LIST_ITEM_SELECTOR}[data-topic-id='#{topic.id}']"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -228,6 +228,10 @@ module PageObjects
|
||||||
post_by_number(post).has_css?(".read-state.read", visible: :all, wait: 3)
|
post_by_number(post).has_css?(".read-state.read", visible: :all, wait: 3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_suggested_topic?(topic)
|
||||||
|
page.has_css?("#suggested-topics .topic-list-item[data-topic-id='#{topic.id}']")
|
||||||
|
end
|
||||||
|
|
||||||
def move_to_public_category(category)
|
def move_to_public_category(category)
|
||||||
click_admin_menu_button
|
click_admin_menu_button
|
||||||
find(".topic-admin-menu-content li.topic-admin-convert").click
|
find(".topic-admin-menu-content li.topic-admin-convert").click
|
||||||
|
|
50
spec/system/topic_list/glimmer_spec.rb
Normal file
50
spec/system/topic_list/glimmer_spec.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
describe "glimmer topic list", type: :system do
|
||||||
|
fab!(:user)
|
||||||
|
|
||||||
|
before do
|
||||||
|
SiteSetting.experimental_glimmer_topic_list_groups = "1"
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "/latest" do
|
||||||
|
let(:topic_list) { PageObjects::Components::TopicList.new }
|
||||||
|
|
||||||
|
it "shows the list" do
|
||||||
|
Fabricate.times(5, :topic)
|
||||||
|
visit("/latest")
|
||||||
|
|
||||||
|
expect(topic_list).to have_topics(count: 5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "categories-with-featured-topics page" do
|
||||||
|
let(:category_list) { PageObjects::Components::CategoryList.new }
|
||||||
|
|
||||||
|
it "shows the list" do
|
||||||
|
SiteSetting.desktop_category_page_style = "categories_with_featured_topics"
|
||||||
|
category = Fabricate(:category)
|
||||||
|
topic = Fabricate(:topic, category: category)
|
||||||
|
topic2 = Fabricate(:topic)
|
||||||
|
CategoryFeaturedTopic.feature_topics
|
||||||
|
|
||||||
|
visit("/categories")
|
||||||
|
|
||||||
|
expect(category_list).to have_topic(topic)
|
||||||
|
expect(category_list).to have_topic(topic2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "suggested topics" do
|
||||||
|
let(:topic_page) { PageObjects::Pages::Topic.new }
|
||||||
|
|
||||||
|
it "shows the list" do
|
||||||
|
topic = Fabricate(:post).topic
|
||||||
|
topic2 = Fabricate(:post).topic
|
||||||
|
visit(topic.relative_url)
|
||||||
|
|
||||||
|
expect(topic_page).to have_suggested_topic(topic2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user