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}}>
|
||||
{{#if this.topics}}
|
||||
<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 this.currentUser.use_glimmer_topic_list}}
|
||||
<TopicList::List
|
||||
@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}}
|
||||
/>
|
||||
{{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}}
|
||||
{{#unless this.loadingMore}}
|
||||
<div class="alert alert-info">
|
||||
|
|
|
@ -8,8 +8,13 @@
|
|||
|
||||
{{#if this.topics}}
|
||||
{{#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}}
|
||||
|
||||
<div class="more-topics">
|
||||
{{#if
|
||||
(eq
|
||||
|
|
|
@ -7,15 +7,27 @@
|
|||
{{/if}}
|
||||
|
||||
{{#if @model.sharedDrafts}}
|
||||
<TopicList
|
||||
@listTitle="shared_drafts.title"
|
||||
@top={{this.top}}
|
||||
@hideCategory="true"
|
||||
@category={{@category}}
|
||||
@topics={{@model.sharedDrafts}}
|
||||
@discoveryList={{true}}
|
||||
class="shared-drafts"
|
||||
/>
|
||||
{{#if this.currentUser.use_glimmer_topic_list}}
|
||||
<TopicList::List
|
||||
@listTitle="shared_drafts.title"
|
||||
@top={{this.top}}
|
||||
@hideCategory="true"
|
||||
@category={{@category}}
|
||||
@topics={{@model.sharedDrafts}}
|
||||
@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}}
|
||||
|
||||
<DiscoveryTopicsList
|
||||
|
@ -75,31 +87,59 @@
|
|||
</span>
|
||||
|
||||
{{#if this.hasTopics}}
|
||||
<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 this.currentUser.use_glimmer_topic_list}}
|
||||
<TopicList::List
|
||||
@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}}
|
||||
/>
|
||||
{{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}}
|
||||
|
||||
<span>
|
||||
<PluginOutlet
|
||||
@name="after-topic-list"
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { on } from "@ember/modifier";
|
||||
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";
|
||||
|
||||
export default class NewListHeaderControlsWrapper extends Component {
|
||||
@service currentUser;
|
||||
|
||||
@action
|
||||
click(e) {
|
||||
const target = e.target;
|
||||
|
@ -17,18 +21,30 @@ export default class NewListHeaderControlsWrapper extends Component {
|
|||
}
|
||||
|
||||
<template>
|
||||
<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 this.currentUser.use_glimmer_topic_list}}
|
||||
<div class="topic-replies-toggle-wrapper">
|
||||
<NewListHeaderControls
|
||||
@current={{@current}}
|
||||
@newRepliesCount={{@newRepliesCount}}
|
||||
@newTopicsCount={{@newTopicsCount}}
|
||||
@noStaticLabel={{true}}
|
||||
@changeNewListSubset={{@changeNewListSubset}}
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
<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>
|
||||
}
|
||||
|
|
|
@ -71,7 +71,11 @@
|
|||
{{#if this.showTopics}}
|
||||
<td class="latest">
|
||||
{{#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}}
|
||||
</td>
|
||||
{{/if}}
|
||||
|
|
|
@ -10,12 +10,14 @@ import TopicBulkActions from "./modal/topic-bulk-actions";
|
|||
export default Component.extend(LoadMore, {
|
||||
modal: service(),
|
||||
router: service(),
|
||||
siteSettings: service(),
|
||||
|
||||
tagName: "table",
|
||||
classNames: ["topic-list"],
|
||||
classNameBindings: ["bulkSelectEnabled:sticky-header"],
|
||||
showTopicPostBadges: true,
|
||||
listTitle: "topic.title",
|
||||
lastCheckedElementId: null,
|
||||
|
||||
get canDoBulkActions() {
|
||||
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;
|
||||
background: var(--secondary);
|
||||
box-shadow: var(--shadow-card);
|
||||
z-index: z("dropdown");
|
||||
|
||||
position: absolute;
|
||||
width: 133px;
|
||||
@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 {
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.d-icon {
|
||||
display: block;
|
||||
margin: 2px auto;
|
||||
width: 100%;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
button.btn.jump-bottom {
|
||||
margin: 5px 0 0 0;
|
||||
}
|
||||
|
|
|
@ -85,6 +85,12 @@
|
|||
.title {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
&.--glimmer button.-trigger {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
|
|
|
@ -74,7 +74,8 @@ class CurrentUserSerializer < BasicUserSerializer
|
|||
:new_new_view_enabled?,
|
||||
:use_experimental_topic_bulk_actions?,
|
||||
:use_admin_sidebar,
|
||||
:can_view_raw_email
|
||||
:can_view_raw_email,
|
||||
:use_glimmer_topic_list?
|
||||
|
||||
delegate :user_stat, to: :object, private: true
|
||||
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
|
||||
scope.user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
|
||||
end
|
||||
|
||||
def use_glimmer_topic_list?
|
||||
scope.user.in_any_groups?(SiteSetting.experimental_glimmer_topic_list_groups_map)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2647,6 +2647,7 @@ en:
|
|||
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"
|
||||
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>."
|
||||
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."
|
||||
|
|
|
@ -2352,6 +2352,13 @@ developer:
|
|||
default: ""
|
||||
allow_any: false
|
||||
refresh: true
|
||||
experimental_glimmer_topic_list_groups:
|
||||
client: true
|
||||
type: group_list
|
||||
list_type: compact
|
||||
default: ""
|
||||
allow_any: false
|
||||
refresh: true
|
||||
enable_experimental_lightbox:
|
||||
default: false
|
||||
client: true
|
||||
|
|
|
@ -31,5 +31,9 @@
|
|||
</StyleguideExample>
|
||||
|
||||
<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>
|
|
@ -3,6 +3,8 @@
|
|||
module PageObjects
|
||||
module Components
|
||||
class CategoryList < PageObjects::Components::Base
|
||||
TOPIC_LIST_ITEM_SELECTOR = ".category-list.with-topics .featured-topic"
|
||||
|
||||
def has_category?(category)
|
||||
page.has_css?("tr[data-category-id='#{category.id}']")
|
||||
end
|
||||
|
@ -30,6 +32,10 @@ module PageObjects
|
|||
def click_topic(topic)
|
||||
page.find("a", text: topic.title).click
|
||||
end
|
||||
|
||||
def topic_list_item_class(topic)
|
||||
"#{TOPIC_LIST_ITEM_SELECTOR}[data-topic-id='#{topic.id}']"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -228,6 +228,10 @@ module PageObjects
|
|||
post_by_number(post).has_css?(".read-state.read", visible: :all, wait: 3)
|
||||
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)
|
||||
click_admin_menu_button
|
||||
find(".topic-admin-menu-content li.topic-admin-convert").click
|
||||
|
|
|
@ -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