DEV: Adopt post list component and new posts route front-end (#30604)
Recently we introduced a new `PostList` component (d886c55f63
). In this update, we make broader adoption of this component. In particular, these areas include using the new component in the user activity stream pages, user's deleted posts, and pending posts page. This update also takes the existing `posts` route and adds a barebones front-end for it to view posts all in one page.
---------
Co-authored-by: David Taylor <david@taylorhq.com>
This commit is contained in:
parent
2b63830496
commit
7b76d25946
|
@ -1 +0,0 @@
|
|||
<UserStreamItem @item={{@post}} />
|
|
@ -1,30 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import { resolveAllShortUrls } from "pretty-text/upload-short-url";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { afterRender } from "discourse/lib/decorators";
|
||||
import { loadOneboxes } from "discourse/lib/load-oneboxes";
|
||||
|
||||
export default class PendingPost extends Component {
|
||||
didRender() {
|
||||
super.didRender(...arguments);
|
||||
this._loadOneboxes();
|
||||
this._resolveUrls();
|
||||
}
|
||||
|
||||
@afterRender
|
||||
_loadOneboxes() {
|
||||
loadOneboxes(
|
||||
this.element,
|
||||
ajax,
|
||||
this.post.topic_id,
|
||||
this.post.category_id,
|
||||
this.siteSettings.max_oneboxes_per_post,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
@afterRender
|
||||
_resolveUrls() {
|
||||
resolveAllShortUrls(ajax, this.siteSettings, this.element, this.opts);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { actionDescriptionHtml } from "discourse/widgets/post-small-action";
|
||||
|
||||
export default class PostActionDescription extends Component {
|
||||
get description() {
|
||||
if (this.args.actionCode && this.args.createdAt) {
|
||||
return actionDescriptionHtml(
|
||||
this.args.actionCode,
|
||||
this.args.createdAt,
|
||||
this.args.username,
|
||||
this.args.path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.description}}
|
||||
<p class="excerpt">{{this.description}}</p>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -8,14 +8,21 @@
|
|||
* @args {String} emptyText (optional) - Custom text to display when there are no posts
|
||||
* @args {String|Array} additionalItemClasses (optional) - Additional classes to add to each post list item
|
||||
* @args {String} titleAriaLabel (optional) - Custom Aria label for the post title
|
||||
* @args {String} showUserInfo (optional) - Whether to show user info in the post list items
|
||||
* @args {String} idPath (optional) - The path to the key in the post object that contains the post ID
|
||||
* @args {String} urlPath (optional) - The path to the key in the post object that contains the post URL
|
||||
* @args {String} titlePath (optional) - The path to the key in the post object that contains the post title
|
||||
* @args {String} usernamePath (optional) - The path to the key in the post object that contains the post author's username
|
||||
*
|
||||
* @template Usage Example:
|
||||
* ```
|
||||
* <PostList
|
||||
* @posts={{this.posts}}
|
||||
* @titlePath="topic_html_title"
|
||||
* @fetchMorePosts={{this.loadMorePosts}}
|
||||
* @emptyText={{i18n "custom_identifier.empty"}}
|
||||
* @additionalItemClasses="custom-class"
|
||||
*
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
|
@ -68,13 +75,31 @@ export default class PostList extends Component {
|
|||
{{/if}}
|
||||
|
||||
<LoadMore @selector=".post-list-item" @action={{this.loadMore}}>
|
||||
<div class="post-list">
|
||||
<div class="post-list" ...attributes>
|
||||
{{#each @posts as |post|}}
|
||||
<PostListItem
|
||||
@post={{post}}
|
||||
@idPath={{@idPath}}
|
||||
@urlPath={{@urlPath}}
|
||||
@titlePath={{@titlePath}}
|
||||
@usernamePath={{@usernamePath}}
|
||||
@additionalItemClasses={{@additionalItemClasses}}
|
||||
@titleAriaLabel={{@titleAriaLabel}}
|
||||
/>
|
||||
@showUserInfo={{@showUserInfo}}
|
||||
>
|
||||
<:abovePostItemHeader>
|
||||
{{yield post to="abovePostItemHeader"}}
|
||||
</:abovePostItemHeader>
|
||||
<:belowPostItemMetaData>
|
||||
{{yield post to="belowPostItemMetaData"}}
|
||||
</:belowPostItemMetaData>
|
||||
<:abovePostItemExcerpt>
|
||||
{{yield post to="abovePostItemExcerpt"}}
|
||||
</:abovePostItemExcerpt>
|
||||
<:belowPostItem>
|
||||
{{yield post to="belowPostItem"}}
|
||||
</:belowPostItem>
|
||||
</PostListItem>
|
||||
{{else}}
|
||||
<div class="post-list__empty-text">{{this.emptyText}}</div>
|
||||
{{/each}}
|
||||
|
|
|
@ -1,56 +1,84 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { hash } from "@ember/helper";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import TopicStatus from "discourse/components/topic-status";
|
||||
import categoryLink from "discourse/helpers/category-link";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import { prioritizeNameInUx } from "discourse/lib/settings";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class PostListItemDetails extends Component {
|
||||
get url() {
|
||||
return this.args.urlPath
|
||||
? this.args.post[this.args.urlPath]
|
||||
: this.args.post.url;
|
||||
}
|
||||
|
||||
get showUserInfo() {
|
||||
if (this.args.showUserInfo !== undefined) {
|
||||
return this.args.showUserInfo && this.args.user;
|
||||
}
|
||||
|
||||
return this.args.user;
|
||||
}
|
||||
|
||||
get topicTitle() {
|
||||
return this.args.titlePath
|
||||
? this.args.post[this.args.titlePath]
|
||||
: this.args.post.title;
|
||||
}
|
||||
|
||||
get titleAriaLabel() {
|
||||
return (
|
||||
this.args.titleAriaLabel ||
|
||||
i18n("post_list.aria_post_number", {
|
||||
title: this.args.post.title,
|
||||
if (this.args.titleAriaLabel) {
|
||||
return this.args.titleAriaLabel;
|
||||
}
|
||||
|
||||
if (this.args.post.post_number && this.topicTitle) {
|
||||
return i18n("post_list.aria_post_number", {
|
||||
title: this.topicTitle,
|
||||
postNumber: this.args.post.post_number,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get posterName() {
|
||||
if (prioritizeNameInUx(this.args.post.user.name)) {
|
||||
return this.args.post.user.name;
|
||||
if (prioritizeNameInUx(this.args.user.name)) {
|
||||
return this.args.user.name;
|
||||
}
|
||||
return this.args.post.user.username;
|
||||
return this.args.user.username;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="post-list-item__details">
|
||||
<div class="stream-topic-title">
|
||||
<TopicStatus @topic={{@post}} @disableActions={{true}} />
|
||||
<span class="title">
|
||||
{{#if this.url}}
|
||||
<a
|
||||
href={{getURL @post.url}}
|
||||
href={{getURL this.url}}
|
||||
aria-label={{this.titleAriaLabel}}
|
||||
>{{htmlSafe @post.topic.fancyTitle}}</a>
|
||||
>{{this.topicTitle}}</a>
|
||||
{{else}}
|
||||
{{this.topicTitle}}
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="stream-post-category">
|
||||
<div class="category stream-post-category">
|
||||
{{categoryLink @post.category}}
|
||||
</div>
|
||||
|
||||
{{#if @post.user}}
|
||||
{{#if this.showUserInfo}}
|
||||
<div class="post-member-info names">
|
||||
<span class="name">{{this.posterName}}</span>
|
||||
|
||||
{{#if @post.user.title}}
|
||||
<span class="user-title">{{@post.user.title}}</span>
|
||||
{{#if @user.title}}
|
||||
<span class="user-title">{{@user.user_title}}</span>
|
||||
{{/if}}
|
||||
|
||||
<PluginOutlet
|
||||
@name="post-list-additional-member-info"
|
||||
@outletArgs={{hash user=@post.user}}
|
||||
@outletArgs={{hash user=@user}}
|
||||
/>
|
||||
|
||||
{{!
|
||||
|
@ -59,7 +87,7 @@ export default class PostListItemDetails extends Component {
|
|||
}}
|
||||
<PluginOutlet
|
||||
@name="group-post-additional-member-info"
|
||||
@outletArgs={{hash user=@post.user}}
|
||||
@outletArgs={{hash user=@user}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -5,11 +5,14 @@ import ExpandPost from "discourse/components/expand-post";
|
|||
import PostListItemDetails from "discourse/components/post-list/item/details";
|
||||
import avatar from "discourse/helpers/avatar";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import dIcon from "discourse/helpers/d-icon";
|
||||
import formatDate from "discourse/helpers/format-date";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
|
||||
export default class PostListItem extends Component {
|
||||
@service site;
|
||||
@service siteSettings;
|
||||
@service currentUser;
|
||||
|
||||
get moderatorActionClass() {
|
||||
return this.args.post.post_type === this.site.post_types.moderator_action
|
||||
|
@ -23,44 +26,114 @@ export default class PostListItem extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
get hiddenClass() {
|
||||
return this.args.post.hidden && !this.currentUser?.staff;
|
||||
}
|
||||
|
||||
get deletedClass() {
|
||||
return this.args.post.deleted ? "deleted" : "";
|
||||
}
|
||||
|
||||
get user() {
|
||||
return {
|
||||
id: this.args.post.user_id,
|
||||
name: this.args.post.name,
|
||||
username: this.args.usernamePath
|
||||
? this.args.post[this.args.usernamePath]
|
||||
: this.args.post.username,
|
||||
avatar_template: this.args.post.avatar_template,
|
||||
title: this.args.post.user_title,
|
||||
primary_group_name: this.args.post.primary_group_name,
|
||||
};
|
||||
}
|
||||
|
||||
get postId() {
|
||||
return this.args.idPath
|
||||
? this.args.post[this.args.idPath]
|
||||
: this.args.post.id;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="post-list-item
|
||||
{{concatClass
|
||||
this.moderatorActionClass
|
||||
this.primaryGroupClass
|
||||
this.hiddenClass
|
||||
@additionalItemClasses
|
||||
}}"
|
||||
>
|
||||
{{yield to="abovePostItemHeader"}}
|
||||
|
||||
<div class="post-list-item__header info">
|
||||
<a
|
||||
href={{userPath @post.user.username}}
|
||||
data-user-card={{@post.user.username}}
|
||||
href={{userPath this.user.username}}
|
||||
data-user-card={{this.user.username}}
|
||||
class="avatar-link"
|
||||
>
|
||||
<div class="avatar-wrapper">
|
||||
{{avatar
|
||||
@post.user
|
||||
this.user
|
||||
imageSize="large"
|
||||
extraClasses="actor"
|
||||
ignoreTitle="true"
|
||||
}}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<PostListItemDetails
|
||||
@post={{@post}}
|
||||
@titleAriaLabel={{@titleAriaLabel}}
|
||||
@titlePath={{@titlePath}}
|
||||
@urlPath={{@urlPath}}
|
||||
@user={{this.user}}
|
||||
@showUserInfo={{@showUserInfo}}
|
||||
/>
|
||||
<ExpandPost @item={{@post}} />
|
||||
<div class="time">{{formatDate @post.created_at leaveAgo="true"}}</div>
|
||||
</div>
|
||||
|
||||
<div class="excerpt">
|
||||
{{#if @post.expandedExcerpt}}
|
||||
{{htmlSafe @post.expandedExcerpt}}
|
||||
{{#if @post.draftType}}
|
||||
<span class="draft-type">{{@post.draftType}}</span>
|
||||
{{else}}
|
||||
{{htmlSafe @post.excerpt}}
|
||||
<ExpandPost @item={{@post}} />
|
||||
{{/if}}
|
||||
|
||||
<div class="post-list-item__metadata">
|
||||
<span class="time">
|
||||
{{formatDate @post.created_at leaveAgo="true"}}
|
||||
</span>
|
||||
|
||||
{{#if @post.deleted_by}}
|
||||
<span class="delete-info">
|
||||
{{dIcon "trash-can"}}
|
||||
{{avatar
|
||||
@post.deleted_by
|
||||
imageSize="tiny"
|
||||
extraClasses="actor"
|
||||
ignoreTitle="true"
|
||||
}}
|
||||
{{formatDate @item.deleted_at leaveAgo="true"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{yield to="belowPostItemMetadata"}}
|
||||
</div>
|
||||
|
||||
{{yield to="abovePostItemExcerpt"}}
|
||||
|
||||
<div
|
||||
data-topic-id={{@post.topic_id}}
|
||||
data-post-id={{this.postId}}
|
||||
data-user-id={{@post.user_id}}
|
||||
class="excerpt"
|
||||
>
|
||||
{{#if @post.expandedExcerpt}}
|
||||
{{~htmlSafe @post.expandedExcerpt~}}
|
||||
{{else}}
|
||||
{{~htmlSafe @post.excerpt~}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{yield to="belowPostItem"}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { computed } from "@ember/object";
|
|||
import { classNameBindings, tagName } from "@ember-decorators/component";
|
||||
import { propertyEqual } from "discourse/lib/computed";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import deprecated from "discourse/lib/deprecated";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import { actionDescription } from "discourse/widgets/post-small-action";
|
||||
|
||||
|
@ -26,6 +27,18 @@ export default class UserStreamItem extends Component {
|
|||
)
|
||||
actionDescription;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
deprecated(
|
||||
`<UserStreamItem /> component is deprecated. Use <PostList /> or <UserStream /> component to render a post list instead.`,
|
||||
{
|
||||
since: "3.4.0.beta4",
|
||||
dropFrom: "3.5.0.beta1",
|
||||
id: "discourse.user-stream-item",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@computed("item.hidden")
|
||||
get hidden() {
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn, hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import { getOwner } from "@ember/owner";
|
||||
import { later } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { modifier } from "ember-modifier";
|
||||
import $ from "jquery";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import PostActionDescription from "discourse/components/post-action-description";
|
||||
import PostList from "discourse/components/post-list";
|
||||
import avatar from "discourse/helpers/avatar";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import dIcon from "discourse/helpers/d-icon";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import ClickTrack from "discourse/lib/click-track";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { NEW_TOPIC_KEY } from "discourse/models/composer";
|
||||
import Draft from "discourse/models/draft";
|
||||
import Post from "discourse/models/post";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class UserStreamComponent extends Component {
|
||||
@service dialog;
|
||||
@service composer;
|
||||
@service appEvents;
|
||||
@service currentUser;
|
||||
@service router;
|
||||
@tracked lastDecoratedElement;
|
||||
|
||||
eventListeners = modifier((element) => {
|
||||
$(element).on("click.details-disabled", "details.disabled", () => false);
|
||||
$(element).on("click.discourse-redirect", ".excerpt a", (e) => {
|
||||
return ClickTrack.trackClick(e, getOwner(this));
|
||||
});
|
||||
later(() => {
|
||||
this.updateLastDecoratedElement();
|
||||
this.appEvents.trigger("decorate-non-stream-cooked-element", element);
|
||||
});
|
||||
|
||||
return () => {
|
||||
$(element).off("click.details-disabled", "details.disabled");
|
||||
// Unbind link tracking
|
||||
$(element).off("click.discourse-redirect", ".excerpt a");
|
||||
};
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
}
|
||||
|
||||
get filterClassName() {
|
||||
const filter = this.args.stream?.filter;
|
||||
|
||||
if (filter) {
|
||||
return `filter-${filter.toString().replace(",", "-")}`;
|
||||
}
|
||||
}
|
||||
|
||||
get usernamePath() {
|
||||
// We want the draft_username for the drafts route,
|
||||
// in-case you are editing a post that was created by another user
|
||||
// the draft usernmae will show the post item to show the editing user
|
||||
if (this.router.currentRouteName === "userActivity.drafts") {
|
||||
return "draft_username";
|
||||
}
|
||||
|
||||
return "username";
|
||||
}
|
||||
|
||||
@action
|
||||
updateLastDecoratedElement() {
|
||||
const nodes = document.querySelectorAll(".user-stream-item");
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastElement = nodes[nodes.length - 1];
|
||||
if (lastElement === this.lastDecoratedElement) {
|
||||
return;
|
||||
}
|
||||
this.lastDecoratedElement = lastElement;
|
||||
}
|
||||
|
||||
@action
|
||||
async removeBookmark(userAction) {
|
||||
try {
|
||||
await Post.updateBookmark(userAction.get("post_id"), false);
|
||||
this.args.stream.remove(userAction);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async resumeDraft(item) {
|
||||
if (this.composer.get("model.viewOpen")) {
|
||||
this.composer.close();
|
||||
}
|
||||
if (item.get("postUrl")) {
|
||||
DiscourseURL.routeTo(item.get("postUrl"));
|
||||
} else {
|
||||
try {
|
||||
const draftData = await Draft.get(item.draft_key);
|
||||
const draft = draftData.draft || item.data;
|
||||
if (!draft) {
|
||||
return;
|
||||
}
|
||||
this.composer.open({
|
||||
draft,
|
||||
draftKey: item.draft_key,
|
||||
draftSequence: draftData.draft_sequence,
|
||||
});
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
removeDraft(draft) {
|
||||
this.dialog.yesNoConfirm({
|
||||
message: i18n("drafts.remove_confirmation"),
|
||||
didConfirm: async () => {
|
||||
try {
|
||||
await Draft.clear(draft.draft_key, draft.sequence);
|
||||
this.args.stream.remove(draft);
|
||||
|
||||
if (draft.draft_key === NEW_TOPIC_KEY) {
|
||||
this.currentUser.has_topic_draft = false;
|
||||
}
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async loadMore() {
|
||||
await this.args.stream.findItems();
|
||||
|
||||
if (this.args.stream.canLoadMore === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
later(() => {
|
||||
let element = this.lastDecoratedElement?.nextElementSibling;
|
||||
while (element) {
|
||||
this.appEvents.trigger("user-stream:new-item-inserted", element);
|
||||
this.appEvents.trigger("decorate-non-stream-cooked-element", element);
|
||||
element = element.nextElementSibling;
|
||||
}
|
||||
this.updateLastDecoratedElement();
|
||||
});
|
||||
|
||||
return this.args.stream.content;
|
||||
}
|
||||
|
||||
<template>
|
||||
<PostList
|
||||
@posts={{@stream.content}}
|
||||
@idPath="post_id"
|
||||
@urlPath="postUrl"
|
||||
@usernamePath={{this.usernamePath}}
|
||||
@fetchMorePosts={{this.loadMore}}
|
||||
@titlePath="titleHtml"
|
||||
@additionalItemClasses="user-stream-item"
|
||||
@showUserInfo={{false}}
|
||||
class={{concatClass "user-stream" this.filterClassName}}
|
||||
{{this.eventListeners @stream}}
|
||||
>
|
||||
<:abovePostItemHeader as |post|>
|
||||
<PluginOutlet
|
||||
@name="user-stream-item-above"
|
||||
@outletArgs={{hash item=post}}
|
||||
/>
|
||||
</:abovePostItemHeader>
|
||||
<:belowPostItemMetaData as |post|>
|
||||
<span>
|
||||
<PluginOutlet
|
||||
@name="user-stream-item-header"
|
||||
@connectorTagName="div"
|
||||
@outletArgs={{hash item=post}}
|
||||
/>
|
||||
</span>
|
||||
</:belowPostItemMetaData>
|
||||
<:abovePostItemExcerpt as |post|>
|
||||
<PostActionDescription
|
||||
@actionCode={{post.action_code}}
|
||||
@createdAt={{post.created_at}}
|
||||
@username={{post.action_code_who}}
|
||||
@path={{post.action_code_path}}
|
||||
/>
|
||||
|
||||
{{#each post.children as |child|}}
|
||||
<div class="user-stream-item-actions">
|
||||
{{dIcon child.icon class="icon"}}
|
||||
{{#each child.items as |grandChild|}}
|
||||
<a
|
||||
href={{grandChild.userUrl}}
|
||||
data-user-card={{grandChild.username}}
|
||||
class="avatar-link"
|
||||
>
|
||||
<div class="avatar-wrapper">
|
||||
{{avatar
|
||||
grandChild
|
||||
imageSize="tiny"
|
||||
extraClasses="actor"
|
||||
ignoreTitle="true"
|
||||
avatarTemplatePath="acting_avatar_template"
|
||||
}}
|
||||
</div>
|
||||
</a>
|
||||
{{#if grandChild.edit_reason}}
|
||||
—
|
||||
<span class="edit-reason">{{grandChild.edit_reason}}</span>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{#if post.editableDraft}}
|
||||
<div class="user-stream-item-draft-actions">
|
||||
<DButton
|
||||
@action={{fn this.resumeDraft post}}
|
||||
@icon="pencil"
|
||||
@label="drafts.resume"
|
||||
class="btn-default resume-draft"
|
||||
/>
|
||||
<DButton
|
||||
@action={{fn this.removeDraft post}}
|
||||
@icon="trash-can"
|
||||
@title="drafts.remove"
|
||||
class="btn-danger remove-draft"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</:abovePostItemExcerpt>
|
||||
|
||||
<:belowPostItem as |post|>
|
||||
{{yield post to="bottom"}}
|
||||
</:belowPostItem>
|
||||
</PostList>
|
||||
</template>
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
{{#each @stream.content as |item|}}
|
||||
<UserStreamItem
|
||||
@item={{item}}
|
||||
@removeBookmark={{action "removeBookmark"}}
|
||||
@resumeDraft={{action "resumeDraft"}}
|
||||
@removeDraft={{action "removeDraft"}}
|
||||
/>
|
||||
{{/each}}
|
|
@ -1,156 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { getOwner } from "@ember/owner";
|
||||
import { later } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { classNames, tagName } from "@ember-decorators/component";
|
||||
import { on } from "@ember-decorators/object";
|
||||
import $ from "jquery";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import ClickTrack from "discourse/lib/click-track";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import LoadMore from "discourse/mixins/load-more";
|
||||
import { NEW_TOPIC_KEY } from "discourse/models/composer";
|
||||
import Draft from "discourse/models/draft";
|
||||
import Post from "discourse/models/post";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
@tagName("ul")
|
||||
@classNames("user-stream")
|
||||
export default class UserStream extends Component.extend(LoadMore) {
|
||||
@service dialog;
|
||||
@service composer;
|
||||
|
||||
loading = false;
|
||||
eyelineSelector = ".user-stream .item";
|
||||
_lastDecoratedElement = null;
|
||||
|
||||
@on("init")
|
||||
_initialize() {
|
||||
const filter = this.get("stream.filter");
|
||||
if (filter) {
|
||||
this.set("classNames", [
|
||||
"user-stream",
|
||||
"filter-" + filter.toString().replace(",", "-"),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@on("didInsertElement")
|
||||
_inserted() {
|
||||
$(this.element).on(
|
||||
"click.details-disabled",
|
||||
"details.disabled",
|
||||
() => false
|
||||
);
|
||||
$(this.element).on("click.discourse-redirect", ".excerpt a", (e) => {
|
||||
return ClickTrack.trackClick(e, getOwner(this));
|
||||
});
|
||||
this._updateLastDecoratedElement();
|
||||
this.appEvents.trigger("decorate-non-stream-cooked-element", this.element);
|
||||
}
|
||||
|
||||
// This view is being removed. Shut down operations
|
||||
@on("willDestroyElement")
|
||||
_destroyed() {
|
||||
$(this.element).off("click.details-disabled", "details.disabled");
|
||||
|
||||
// Unbind link tracking
|
||||
$(this.element).off("click.discourse-redirect", ".excerpt a");
|
||||
}
|
||||
|
||||
_updateLastDecoratedElement() {
|
||||
const nodes = this.element.querySelectorAll(".user-stream-item");
|
||||
if (nodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
const lastElement = nodes[nodes.length - 1];
|
||||
if (lastElement === this._lastDecoratedElement) {
|
||||
return;
|
||||
}
|
||||
this._lastDecoratedElement = lastElement;
|
||||
}
|
||||
|
||||
@action
|
||||
removeBookmark(userAction) {
|
||||
const stream = this.stream;
|
||||
Post.updateBookmark(userAction.get("post_id"), false)
|
||||
.then(() => {
|
||||
stream.remove(userAction);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
|
||||
@action
|
||||
resumeDraft(item) {
|
||||
if (this.composer.get("model.viewOpen")) {
|
||||
this.composer.close();
|
||||
}
|
||||
if (item.get("postUrl")) {
|
||||
DiscourseURL.routeTo(item.get("postUrl"));
|
||||
} else {
|
||||
Draft.get(item.draft_key)
|
||||
.then((d) => {
|
||||
const draft = d.draft || item.data;
|
||||
if (!draft) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.composer.open({
|
||||
draft,
|
||||
draftKey: item.draft_key,
|
||||
draftSequence: d.draft_sequence,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
popupAjaxError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
removeDraft(draft) {
|
||||
const stream = this.stream;
|
||||
|
||||
this.dialog.yesNoConfirm({
|
||||
message: i18n("drafts.remove_confirmation"),
|
||||
didConfirm: () => {
|
||||
Draft.clear(draft.draft_key, draft.sequence)
|
||||
.then(() => {
|
||||
stream.remove(draft);
|
||||
if (draft.draft_key === NEW_TOPIC_KEY) {
|
||||
this.currentUser.set("has_topic_draft", false);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
popupAjaxError(error);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
loadMore() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("loading", true);
|
||||
const stream = this.stream;
|
||||
stream.findItems().then(() => {
|
||||
this.set("loading", false);
|
||||
|
||||
// The next elements are not rendered on the page yet, we need to
|
||||
// wait for that before trying to decorate them.
|
||||
later(() => {
|
||||
let element = this._lastDecoratedElement?.nextElementSibling;
|
||||
while (element) {
|
||||
this.trigger("user-stream:new-item-inserted", element);
|
||||
this.appEvents.trigger("decorate-non-stream-cooked-element", element);
|
||||
element = element.nextElementSibling;
|
||||
}
|
||||
this._updateLastDecoratedElement();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import { dependentKeyCompat } from "@ember/object/compat";
|
|||
import { and, equal, not, or, reads } from "@ember/object/computed";
|
||||
import { next, throttle } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { isHTMLSafe } from "@ember/template";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { observes, on } from "@ember-decorators/object";
|
||||
import { Promise } from "rsvp";
|
||||
|
@ -643,6 +644,9 @@ export default class Composer extends RestModel {
|
|||
@discourseComputed("title")
|
||||
titleLength(title) {
|
||||
title = title || "";
|
||||
if (isHTMLSafe(title)) {
|
||||
return title.toString().length;
|
||||
}
|
||||
return title.replace(/\s+/gim, " ").trim().length;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { htmlSafe } from "@ember/template";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import Category from "discourse/models/category";
|
||||
import Post from "discourse/models/post";
|
||||
import RestModel from "discourse/models/rest";
|
||||
|
||||
export default class Posts extends RestModel {
|
||||
static async find(opts = {}) {
|
||||
const data = {};
|
||||
|
||||
if (opts.before) {
|
||||
data.before = opts.before;
|
||||
}
|
||||
|
||||
if (opts.id) {
|
||||
data.id = opts.id;
|
||||
}
|
||||
|
||||
const { latest_posts } = await ajax("/posts.json", { data });
|
||||
return latest_posts.map((post) => {
|
||||
post.category = Category.findById(post.category_id);
|
||||
post.topic_html_title = htmlSafe(post.topic_html_title);
|
||||
return Post.create(post);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { service } from "@ember/service";
|
||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import { postUrl } from "discourse/lib/utilities";
|
||||
|
@ -12,6 +13,10 @@ import { i18n } from "discourse-i18n";
|
|||
export default class UserDraft extends RestModel {
|
||||
@service currentUser;
|
||||
|
||||
get titleHtml() {
|
||||
return replaceEmoji(this.get("title"));
|
||||
}
|
||||
|
||||
@discourseComputed("draft_username")
|
||||
editableDraft(draftUsername) {
|
||||
return draftUsername === this.currentUser?.get("username");
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { Promise } from "rsvp";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import { cook, emojiUnescape, excerpt } from "discourse/lib/text";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { cook, excerpt } from "discourse/lib/text";
|
||||
import Category from "discourse/models/category";
|
||||
import {
|
||||
NEW_PRIVATE_MESSAGE_KEY,
|
||||
|
@ -82,7 +81,6 @@ export default class UserDraftsStream extends RestModel {
|
|||
) {
|
||||
draft.title = draft.data.title;
|
||||
}
|
||||
draft.title = emojiUnescape(escapeExpression(draft.title));
|
||||
if (draft.data.categoryId) {
|
||||
draft.category = Category.findById(draft.data.categoryId) || null;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { A } from "@ember/array";
|
||||
import { Promise } from "rsvp";
|
||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { url } from "discourse/lib/computed";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import RestModel from "discourse/models/rest";
|
||||
import Site from "discourse/models/site";
|
||||
import UserAction from "discourse/models/user-action";
|
||||
|
@ -116,8 +115,8 @@ export default class UserStream extends RestModel {
|
|||
Site.current().updateCategory(category);
|
||||
});
|
||||
|
||||
result.user_actions.forEach((action) => {
|
||||
action.title = emojiUnescape(escapeExpression(action.title));
|
||||
result.user_actions?.forEach((action) => {
|
||||
action.titleHtml = replaceEmoji(action.title);
|
||||
copy.pushObject(UserAction.create(action));
|
||||
});
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ export default function () {
|
|||
this.route("about", { resetNamespace: true });
|
||||
|
||||
this.route("post", { path: "/p/:id" });
|
||||
this.route("posts");
|
||||
|
||||
// Topic routes
|
||||
this.route(
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { service } from "@ember/service";
|
||||
import Posts from "discourse/models/posts";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class PostsRoute extends DiscourseRoute {
|
||||
@service router;
|
||||
|
||||
async model() {
|
||||
return Posts.find();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
<PostList
|
||||
@posts={{this.model}}
|
||||
@titlePath="topic_html_title"
|
||||
@fetchMorePosts={{this.fetchMorePosts}}
|
||||
@emptyText={{i18n "groups.empty.posts"}}
|
||||
/>
|
|
@ -0,0 +1,25 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import PostList from "discourse/components/post-list";
|
||||
import Posts from "discourse/models/posts";
|
||||
|
||||
export default RouteTemplate(
|
||||
class extends Component {
|
||||
@action
|
||||
async loadMorePosts() {
|
||||
const posts = this.args.model;
|
||||
const before = posts[posts.length - 1].created_at;
|
||||
|
||||
return Posts.find({ before });
|
||||
}
|
||||
|
||||
<template>
|
||||
<PostList
|
||||
@posts={{@model}}
|
||||
@fetchMorePosts={{this.loadMorePosts}}
|
||||
@titlePath="topic_html_title"
|
||||
/>
|
||||
</template>
|
||||
}
|
||||
);
|
|
@ -1,5 +1,9 @@
|
|||
<ul class="user-stream">
|
||||
{{#each this.model as |pending_post|}}
|
||||
<PendingPost @post={{pending_post}} />
|
||||
{{/each}}
|
||||
<PostList
|
||||
@posts={{this.model}}
|
||||
@urlPath="postUrl"
|
||||
@showUserInfo={{false}}
|
||||
@additionalItemClasses="user-stream-item"
|
||||
class="user-stream"
|
||||
/>
|
||||
</ul>
|
|
@ -1,5 +1 @@
|
|||
{{#if this.model.canLoadMore}}
|
||||
{{hide-application-footer}}
|
||||
{{/if}}
|
||||
|
||||
<UserStream @stream={{this.model}} />
|
|
@ -1,11 +1,8 @@
|
|||
{{#if (or this.loading this.model.stream.canLoadMore)}}
|
||||
{{hide-application-footer}}
|
||||
{{/if}}
|
||||
|
||||
{{#if this.model.stream.noContent}}
|
||||
<EmptyState
|
||||
@title={{this.model.emptyState.title}}
|
||||
@body={{this.model.emptyState.body}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<UserStream @stream={{this.model.stream}} />
|
|
@ -22,15 +22,15 @@ acceptance("User Anonymous", function () {
|
|||
.dom(document.body)
|
||||
.hasClass("user-activity-page", "has the body class");
|
||||
assert.dom(".user-main .about").exists("has the about section");
|
||||
assert.dom(".user-stream .item").exists("has stream items");
|
||||
assert.dom(".user-stream-item").exists("has stream items");
|
||||
|
||||
await visit("/u/eviltrout/activity/topics");
|
||||
assert.dom(".user-stream .item").doesNotExist("has no stream displayed");
|
||||
assert.dom(".user-stream-item").doesNotExist("has no stream displayed");
|
||||
assert.dom(".topic-list tr").exists("has a topic list");
|
||||
|
||||
await visit("/u/eviltrout/activity/replies");
|
||||
assert.dom(".user-main .about").exists("has the about section");
|
||||
assert.dom(".user-stream .item").exists("has stream items");
|
||||
assert.dom(".user-stream-item").exists("has stream items");
|
||||
|
||||
assert.dom(".user-stream.filter-5").exists("stream has filter class");
|
||||
});
|
||||
|
|
|
@ -306,14 +306,11 @@ export default {
|
|||
name_lower: "ux",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 2770,
|
||||
user_id: 2770,
|
||||
username: "awesomerobot",
|
||||
uploaded_avatar_id: 33872,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 94603,
|
||||
cooked:
|
||||
|
@ -358,14 +355,11 @@ export default {
|
|||
name_lower: "ux",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 2770,
|
||||
user_id: 2770,
|
||||
username: "awesomerobot",
|
||||
uploaded_avatar_id: 33872,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 94601,
|
||||
cooked:
|
||||
|
@ -410,13 +404,11 @@ export default {
|
|||
name_lower: "ux",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 2770,
|
||||
user_id: 2770,
|
||||
username: "awesomerobot",
|
||||
uploaded_avatar_id: 33872,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png",
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
id: 94577,
|
||||
|
@ -463,14 +455,11 @@ export default {
|
|||
name_lower: "feature",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 1995,
|
||||
user_id: 1995,
|
||||
username: "zogstrip",
|
||||
uploaded_avatar_id: 8630,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 94574,
|
||||
cooked:
|
||||
|
@ -516,13 +505,11 @@ export default {
|
|||
name_lower: "feature",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 1995,
|
||||
user_id: 1995,
|
||||
username: "zogstrip",
|
||||
uploaded_avatar_id: 8630,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png",
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
id: 94572,
|
||||
|
@ -569,13 +556,11 @@ export default {
|
|||
name_lower: "translations",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 1995,
|
||||
user_id: 1995,
|
||||
username: "zogstrip",
|
||||
uploaded_avatar_id: 8630,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png",
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
id: 94555,
|
||||
|
@ -623,13 +608,11 @@ export default {
|
|||
name_lower: "dev",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 1995,
|
||||
user_id: 1995,
|
||||
username: "zogstrip",
|
||||
uploaded_avatar_id: 8630,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png",
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
id: 94544,
|
||||
|
@ -676,13 +659,9 @@ export default {
|
|||
name_lower: "ux",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 32,
|
||||
user_id: 32,
|
||||
username: "codinghorror",
|
||||
uploaded_avatar_id: 5297,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
avatar_template: "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
{
|
||||
id: 94543,
|
||||
|
@ -729,13 +708,10 @@ export default {
|
|||
name_lower: "feature",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 32,
|
||||
user_id: 32,
|
||||
username: "codinghorror",
|
||||
uploaded_avatar_id: 5297,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
avatar_template: "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
|
||||
},
|
||||
{
|
||||
id: 94542,
|
||||
|
@ -782,13 +758,9 @@ export default {
|
|||
name_lower: "support",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 32,
|
||||
user_id: 32,
|
||||
username: "codinghorror",
|
||||
uploaded_avatar_id: 5297,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
avatar_template: "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
{
|
||||
id: 94522,
|
||||
|
@ -835,13 +807,9 @@ export default {
|
|||
name_lower: "bug",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 32,
|
||||
user_id: 32,
|
||||
username: "codinghorror",
|
||||
uploaded_avatar_id: 5297,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
avatar_template: "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
{
|
||||
id: 94521,
|
||||
|
@ -888,13 +856,9 @@ export default {
|
|||
name_lower: "ux",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 32,
|
||||
user_id: 32,
|
||||
username: "codinghorror",
|
||||
uploaded_avatar_id: 5297,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
avatar_template: "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
{
|
||||
id: 94519,
|
||||
|
@ -941,13 +905,9 @@ export default {
|
|||
name_lower: "meta",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 32,
|
||||
user_id: 32,
|
||||
username: "codinghorror",
|
||||
uploaded_avatar_id: 5297,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
avatar_template: "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
{
|
||||
id: 94518,
|
||||
|
@ -994,13 +954,9 @@ export default {
|
|||
name_lower: "support",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 32,
|
||||
user_id: 32,
|
||||
username: "codinghorror",
|
||||
uploaded_avatar_id: 5297,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
avatar_template: "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
{
|
||||
id: 94517,
|
||||
|
@ -1047,13 +1003,9 @@ export default {
|
|||
name_lower: "bug",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 32,
|
||||
user_id: 32,
|
||||
username: "codinghorror",
|
||||
uploaded_avatar_id: 5297,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
avatar_template: "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
{
|
||||
id: 94516,
|
||||
|
@ -1100,13 +1052,9 @@ export default {
|
|||
name_lower: "support",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 32,
|
||||
user_id: 32,
|
||||
username: "codinghorror",
|
||||
uploaded_avatar_id: 5297,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
avatar_template: "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
{
|
||||
id: 94515,
|
||||
|
@ -1154,13 +1102,9 @@ export default {
|
|||
name_lower: "dev",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 32,
|
||||
user_id: 32,
|
||||
username: "codinghorror",
|
||||
uploaded_avatar_id: 5297,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
avatar_template: "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
{
|
||||
id: 94514,
|
||||
|
@ -1207,13 +1151,9 @@ export default {
|
|||
name_lower: "extensibility",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 32,
|
||||
user_id: 32,
|
||||
username: "codinghorror",
|
||||
uploaded_avatar_id: 5297,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
avatar_template: "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
{
|
||||
id: 94512,
|
||||
|
@ -1260,13 +1200,9 @@ export default {
|
|||
name_lower: "support",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 32,
|
||||
user_id: 32,
|
||||
username: "codinghorror",
|
||||
uploaded_avatar_id: 5297,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
avatar_template: "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
{
|
||||
id: 94511,
|
||||
|
@ -1313,13 +1249,9 @@ export default {
|
|||
name_lower: "feature",
|
||||
auto_close_based_on_last_post: false,
|
||||
},
|
||||
user: {
|
||||
id: 32,
|
||||
user_id: 32,
|
||||
username: "codinghorror",
|
||||
uploaded_avatar_id: 5297,
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
avatar_template: "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -2,6 +2,8 @@ const postModel = [
|
|||
{
|
||||
id: 1,
|
||||
title: "My dog is so cute",
|
||||
url: "/t/my-dog-is-so-cute/1/1",
|
||||
topic_id: 1,
|
||||
created_at: "2024-03-15T18:45:38.720Z",
|
||||
category: {
|
||||
id: 1,
|
||||
|
@ -21,6 +23,8 @@ const postModel = [
|
|||
{
|
||||
id: 2,
|
||||
title: "My cat is adorable",
|
||||
url: "/t/my-cat-is-so-adorable/2/1",
|
||||
topic_id: 2,
|
||||
created_at: "2024-03-16T18:45:38.720Z",
|
||||
category: {
|
||||
id: 1,
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import { getOwner } from "@ember/owner";
|
||||
import { render } from "@ember/test-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
|
||||
module("Integration | Component | pending-post", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("it renders", async function (assert) {
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
store.createRecord("category", { id: 2 });
|
||||
const post = store.createRecord("pending-post", {
|
||||
id: 1,
|
||||
topic_url: "topic-url",
|
||||
username: "USERNAME",
|
||||
category_id: 2,
|
||||
raw_text: "**bold text**",
|
||||
});
|
||||
this.set("post", post);
|
||||
|
||||
await render(hbs`<PendingPost @post={{this.post}}/>`);
|
||||
|
||||
assert.dom("p.excerpt").hasText("bold text", "renders the cooked text");
|
||||
});
|
||||
});
|
|
@ -43,4 +43,61 @@ module("Integration | Component | PostList | Index", function (hooks) {
|
|||
</template>);
|
||||
assert.dom(".post-list__empty-text").hasText("My custom empty text");
|
||||
});
|
||||
|
||||
test("@showUserInfo", async function (assert) {
|
||||
const posts = postModel;
|
||||
await render(<template>
|
||||
<PostList @posts={{posts}} @showUserInfo={{false}} />
|
||||
</template>);
|
||||
assert.dom(".post-list-item__details .post-member-info").doesNotExist();
|
||||
});
|
||||
|
||||
test("@titlePath", async function (assert) {
|
||||
const posts = postModel.map((post) => {
|
||||
post.topic_html_title = `Fancy title`;
|
||||
return post;
|
||||
});
|
||||
await render(<template>
|
||||
<PostList @posts={{posts}} @titlePath="topic_html_title" />
|
||||
</template>);
|
||||
assert.dom(".post-list-item__details .title a").hasText("Fancy title");
|
||||
});
|
||||
|
||||
test("@idPath", async function (assert) {
|
||||
const posts = postModel.map((post) => {
|
||||
post.post_id = post.id;
|
||||
return post;
|
||||
});
|
||||
await render(<template>
|
||||
<PostList @posts={{posts}} @idPath="post_id" />
|
||||
</template>);
|
||||
assert.dom(".post-list-item .excerpt").hasAttribute("data-post-id", "1");
|
||||
});
|
||||
|
||||
test("@urlPath", async function (assert) {
|
||||
const posts = postModel.map((post) => {
|
||||
post.postUrl = `/t/${post.topic_id}/${post.id}`;
|
||||
return post;
|
||||
});
|
||||
await render(<template>
|
||||
<PostList @posts={{posts}} @urlPath="postUrl" />
|
||||
</template>);
|
||||
assert
|
||||
.dom(".post-list-item__details .title a")
|
||||
.hasAttribute("href", "/t/1/1");
|
||||
});
|
||||
|
||||
test("@usernamePath", async function (assert) {
|
||||
const posts = postModel.map((post) => {
|
||||
post.draft_username = "john";
|
||||
return post;
|
||||
});
|
||||
|
||||
await render(<template>
|
||||
<PostList @posts={{posts}} @usernamePath="draft_username" />
|
||||
</template>);
|
||||
assert
|
||||
.dom(".post-list-item__header .avatar-link")
|
||||
.hasAttribute("data-user-card", "john");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -62,7 +62,8 @@
|
|||
line-height: var(--line-height-small);
|
||||
color: var(--primary-medium);
|
||||
font-size: var(--font-down-2);
|
||||
padding-top: 5px;
|
||||
padding-top: 6px;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.delete-info {
|
||||
|
|
|
@ -25,7 +25,8 @@ class PostsController < ApplicationController
|
|||
|
||||
skip_before_action :preload_json,
|
||||
:check_xhr,
|
||||
only: %i[markdown_id markdown_num short_link latest user_posts_feed]
|
||||
only: %i[markdown_id markdown_num short_link user_posts_feed]
|
||||
skip_before_action :preload_json, :check_xhr, if: -> { request.format.rss? }
|
||||
|
||||
MARKDOWN_TOPIC_PAGE_SIZE = 100
|
||||
|
||||
|
@ -128,6 +129,7 @@ class PostsController < ApplicationController
|
|||
scope: guardian,
|
||||
root: params[:id],
|
||||
add_raw: true,
|
||||
add_excerpt: true,
|
||||
add_title: true,
|
||||
all_post_actions: counts,
|
||||
),
|
||||
|
|
|
@ -5,15 +5,43 @@ require_relative "post_item_excerpt"
|
|||
class GroupPostSerializer < ApplicationSerializer
|
||||
include PostItemExcerpt
|
||||
|
||||
attributes :id, :created_at, :title, :url, :category_id, :post_number, :topic_id, :post_type
|
||||
attributes :id,
|
||||
:created_at,
|
||||
:topic_id,
|
||||
:topic_title,
|
||||
:topic_slug,
|
||||
:topic_html_title,
|
||||
:url,
|
||||
:category_id,
|
||||
:post_number,
|
||||
:posts_count,
|
||||
:post_type,
|
||||
:username,
|
||||
:name,
|
||||
:avatar_template,
|
||||
:user_title,
|
||||
:primary_group_name
|
||||
|
||||
# TODO(keegan): Remove `embed: :object` after updating references in discourse-reactions
|
||||
has_one :user, serializer: GroupPostUserSerializer, embed: :object
|
||||
has_one :topic, serializer: BasicTopicSerializer, embed: :object
|
||||
|
||||
def title
|
||||
def topic_title
|
||||
object.topic.title
|
||||
end
|
||||
|
||||
def topic_html_title
|
||||
object.topic.fancy_title
|
||||
end
|
||||
|
||||
def topic_slug
|
||||
object.topic.slug
|
||||
end
|
||||
|
||||
def posts_count
|
||||
object.topic.posts_count
|
||||
end
|
||||
|
||||
def include_user_long_name?
|
||||
SiteSetting.enable_names?
|
||||
end
|
||||
|
@ -21,4 +49,24 @@ class GroupPostSerializer < ApplicationSerializer
|
|||
def category_id
|
||||
object.topic.category_id
|
||||
end
|
||||
|
||||
def username
|
||||
object&.user&.username
|
||||
end
|
||||
|
||||
def name
|
||||
object&.user&.name
|
||||
end
|
||||
|
||||
def avatar_template
|
||||
object&.user&.avatar_template
|
||||
end
|
||||
|
||||
def user_title
|
||||
object&.user&.title
|
||||
end
|
||||
|
||||
def primary_group_name
|
||||
object&.user&.primary_group&.name
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,6 +17,7 @@ class PostSerializer < BasicPostSerializer
|
|||
|
||||
attributes :post_number,
|
||||
:post_type,
|
||||
:posts_count,
|
||||
:updated_at,
|
||||
:reply_count,
|
||||
:reply_to_post_number,
|
||||
|
@ -84,12 +85,14 @@ class PostSerializer < BasicPostSerializer
|
|||
:last_wiki_edit,
|
||||
:locked,
|
||||
:excerpt,
|
||||
:truncated,
|
||||
:reviewable_id,
|
||||
:reviewable_score_count,
|
||||
:reviewable_score_pending_count,
|
||||
:user_suspended,
|
||||
:user_status,
|
||||
:mentioned_users
|
||||
:mentioned_users,
|
||||
:post_url
|
||||
|
||||
def initialize(object, opts)
|
||||
super(object, opts)
|
||||
|
@ -99,6 +102,10 @@ class PostSerializer < BasicPostSerializer
|
|||
end
|
||||
end
|
||||
|
||||
def post_url
|
||||
object&.url
|
||||
end
|
||||
|
||||
def topic_slug
|
||||
topic&.slug
|
||||
end
|
||||
|
@ -119,6 +126,14 @@ class PostSerializer < BasicPostSerializer
|
|||
@add_excerpt
|
||||
end
|
||||
|
||||
def include_truncated?
|
||||
@add_excerpt
|
||||
end
|
||||
|
||||
def truncated
|
||||
true
|
||||
end
|
||||
|
||||
def topic_title
|
||||
topic&.title
|
||||
end
|
||||
|
@ -127,6 +142,10 @@ class PostSerializer < BasicPostSerializer
|
|||
topic&.fancy_title
|
||||
end
|
||||
|
||||
def posts_count
|
||||
topic&.posts_count
|
||||
end
|
||||
|
||||
def category_id
|
||||
topic&.category_id
|
||||
end
|
||||
|
|
|
@ -32,6 +32,9 @@
|
|||
"post_type": {
|
||||
"type": "integer"
|
||||
},
|
||||
"posts_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -155,8 +158,7 @@
|
|||
},
|
||||
"actions_summary": {
|
||||
"type": "array",
|
||||
"items":
|
||||
{
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
|
@ -223,6 +225,9 @@
|
|||
},
|
||||
"reviewable_score_pending_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"post_url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -234,6 +239,7 @@
|
|||
"cooked",
|
||||
"post_number",
|
||||
"post_type",
|
||||
"posts_count",
|
||||
"updated_at",
|
||||
"reply_count",
|
||||
"reply_to_post_number",
|
||||
|
@ -274,7 +280,8 @@
|
|||
"wiki",
|
||||
"reviewable_id",
|
||||
"reviewable_score_count",
|
||||
"reviewable_score_pending_count"
|
||||
"reviewable_score_pending_count",
|
||||
"post_url"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -22,6 +22,9 @@
|
|||
"post_type": {
|
||||
"type": "integer"
|
||||
},
|
||||
"posts_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -203,6 +206,9 @@
|
|||
"reviewable_score_pending_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"post_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"mentioned_users": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
|
@ -228,6 +234,7 @@
|
|||
"cooked",
|
||||
"post_number",
|
||||
"post_type",
|
||||
"posts_count",
|
||||
"updated_at",
|
||||
"reply_count",
|
||||
"reply_to_post_number",
|
||||
|
@ -266,6 +273,7 @@
|
|||
"wiki",
|
||||
"reviewable_id",
|
||||
"reviewable_score_count",
|
||||
"reviewable_score_pending_count"
|
||||
"reviewable_score_pending_count",
|
||||
"post_url"
|
||||
]
|
||||
}
|
|
@ -26,6 +26,9 @@
|
|||
"post_type": {
|
||||
"type": "integer"
|
||||
},
|
||||
"posts_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -205,6 +208,9 @@
|
|||
"reviewable_score_pending_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"post_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"mentioned_users": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
|
@ -230,6 +236,7 @@
|
|||
"cooked",
|
||||
"post_number",
|
||||
"post_type",
|
||||
"posts_count",
|
||||
"updated_at",
|
||||
"reply_count",
|
||||
"reply_to_post_number",
|
||||
|
@ -269,7 +276,8 @@
|
|||
"wiki",
|
||||
"reviewable_id",
|
||||
"reviewable_score_count",
|
||||
"reviewable_score_pending_count"
|
||||
"reviewable_score_pending_count",
|
||||
"post_url"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -31,6 +31,9 @@
|
|||
"post_type": {
|
||||
"type": "integer"
|
||||
},
|
||||
"posts_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -142,8 +145,7 @@
|
|||
},
|
||||
"actions_summary": {
|
||||
"type": "array",
|
||||
"items":
|
||||
{
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
|
@ -214,10 +216,12 @@
|
|||
"reviewable_score_pending_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"post_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"mentioned_users": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
}
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -229,6 +233,7 @@
|
|||
"cooked",
|
||||
"post_number",
|
||||
"post_type",
|
||||
"posts_count",
|
||||
"updated_at",
|
||||
"reply_count",
|
||||
"reply_to_post_number",
|
||||
|
@ -268,6 +273,7 @@
|
|||
"wiki",
|
||||
"reviewable_id",
|
||||
"reviewable_score_count",
|
||||
"reviewable_score_pending_count"
|
||||
"reviewable_score_pending_count",
|
||||
"post_url"
|
||||
]
|
||||
}
|
|
@ -18,6 +18,7 @@ RSpec.describe WebHookPostSerializer do
|
|||
:cooked,
|
||||
:post_number,
|
||||
:post_type,
|
||||
:posts_count,
|
||||
:updated_at,
|
||||
:reply_count,
|
||||
:reply_to_post_number,
|
||||
|
@ -50,6 +51,7 @@ RSpec.describe WebHookPostSerializer do
|
|||
:reviewable_id,
|
||||
:reviewable_score_count,
|
||||
:reviewable_score_pending_count,
|
||||
:post_url,
|
||||
:topic_posts_count,
|
||||
:topic_filtered_posts_count,
|
||||
:topic_archetype,
|
||||
|
|
Loading…
Reference in New Issue