From 7b76d259462afbc8a343ae93971eead90d41a602 Mon Sep 17 00:00:00 2001 From: Keegan George Date: Fri, 24 Jan 2025 03:20:45 +0900 Subject: [PATCH] DEV: Adopt post list component and new posts route front-end (#30604) Recently we introduced a new `PostList` component (https://github.com/discourse/discourse/commit/d886c55f633a8408997b504d4723bbf44f64f7f9). 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 --- .../discourse/app/components/pending-post.hbs | 1 - .../discourse/app/components/pending-post.js | 30 --- .../components/post-action-description.gjs | 21 ++ .../app/components/post-list/index.gjs | 29 +- .../app/components/post-list/item/details.gjs | 74 ++++-- .../app/components/post-list/item/index.gjs | 99 ++++++- .../app/components/user-stream-item.js | 13 + .../discourse/app/components/user-stream.gjs | 248 ++++++++++++++++++ .../discourse/app/components/user-stream.hbs | 8 - .../discourse/app/components/user-stream.js | 156 ----------- .../discourse/app/models/composer.js | 4 + .../javascripts/discourse/app/models/posts.js | 26 ++ .../discourse/app/models/user-draft.js | 5 + .../app/models/user-drafts-stream.js | 4 +- .../discourse/app/models/user-stream.js | 7 +- .../discourse/app/routes/app-route-map.js | 1 + .../javascripts/discourse/app/routes/posts.js | 11 + .../app/templates/group-activity-posts.hbs | 1 + .../discourse/app/templates/posts.gjs | 25 ++ .../app/templates/user-activity-pending.hbs | 10 +- .../discourse/app/templates/user/posts.hbs | 4 - .../discourse/app/templates/user/stream.hbs | 5 +- .../tests/acceptance/user-anonymous-test.js | 6 +- .../tests/fixtures/group-fixtures.js | 212 +++++---------- .../discourse/tests/fixtures/post-list.js | 4 + .../components/pending-post-test.js | 26 -- .../integration/components/post-list-test.gjs | 57 ++++ .../common/components/user-stream-item.scss | 3 +- app/controllers/posts_controller.rb | 4 +- app/serializers/group_post_serializer.rb | 52 +++- app/serializers/post_serializer.rb | 21 +- .../schemas/json/post_replies_response.json | 43 +-- .../api/schemas/json/post_show_response.json | 12 +- .../schemas/json/post_update_response.json | 12 +- .../schemas/json/topic_create_response.json | 46 ++-- .../web_hook_post_serializer_spec.rb | 2 + 36 files changed, 815 insertions(+), 467 deletions(-) delete mode 100644 app/assets/javascripts/discourse/app/components/pending-post.hbs delete mode 100644 app/assets/javascripts/discourse/app/components/pending-post.js create mode 100644 app/assets/javascripts/discourse/app/components/post-action-description.gjs create mode 100644 app/assets/javascripts/discourse/app/components/user-stream.gjs delete mode 100644 app/assets/javascripts/discourse/app/components/user-stream.hbs delete mode 100644 app/assets/javascripts/discourse/app/components/user-stream.js create mode 100644 app/assets/javascripts/discourse/app/models/posts.js create mode 100644 app/assets/javascripts/discourse/app/routes/posts.js create mode 100644 app/assets/javascripts/discourse/app/templates/posts.gjs delete mode 100644 app/assets/javascripts/discourse/tests/integration/components/pending-post-test.js diff --git a/app/assets/javascripts/discourse/app/components/pending-post.hbs b/app/assets/javascripts/discourse/app/components/pending-post.hbs deleted file mode 100644 index a0f045cc386..00000000000 --- a/app/assets/javascripts/discourse/app/components/pending-post.hbs +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/pending-post.js b/app/assets/javascripts/discourse/app/components/pending-post.js deleted file mode 100644 index b0e09b0ddca..00000000000 --- a/app/assets/javascripts/discourse/app/components/pending-post.js +++ /dev/null @@ -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); - } -} diff --git a/app/assets/javascripts/discourse/app/components/post-action-description.gjs b/app/assets/javascripts/discourse/app/components/post-action-description.gjs new file mode 100644 index 00000000000..a433a565c32 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/post-action-description.gjs @@ -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 + ); + } + } + + +} diff --git a/app/assets/javascripts/discourse/app/components/post-list/index.gjs b/app/assets/javascripts/discourse/app/components/post-list/index.gjs index 0735faf18aa..5a7e0891303 100644 --- a/app/assets/javascripts/discourse/app/components/post-list/index.gjs +++ b/app/assets/javascripts/discourse/app/components/post-list/index.gjs @@ -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: * ``` * * ``` */ @@ -68,13 +75,31 @@ export default class PostList extends Component { {{/if}} -
+
{{#each @posts as |post|}} + @showUserInfo={{@showUserInfo}} + > + <:abovePostItemHeader> + {{yield post to="abovePostItemHeader"}} + + <:belowPostItemMetaData> + {{yield post to="belowPostItemMetaData"}} + + <:abovePostItemExcerpt> + {{yield post to="abovePostItemExcerpt"}} + + <:belowPostItem> + {{yield post to="belowPostItem"}} + + {{else}}
{{this.emptyText}}
{{/each}} diff --git a/app/assets/javascripts/discourse/app/components/post-list/item/details.gjs b/app/assets/javascripts/discourse/app/components/post-list/item/details.gjs index dbee29bf63e..9f5189d05ac 100644 --- a/app/assets/javascripts/discourse/app/components/post-list/item/details.gjs +++ b/app/assets/javascripts/discourse/app/components/post-list/item/details.gjs @@ -1,65 +1,93 @@ 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; } ); assert.dom(".post-list__empty-text").hasText("My custom empty text"); }); + + test("@showUserInfo", async function (assert) { + const posts = postModel; + await render(); + 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(); + 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(); + 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(); + 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(); + assert + .dom(".post-list-item__header .avatar-link") + .hasAttribute("data-user-card", "john"); + }); }); diff --git a/app/assets/stylesheets/common/components/user-stream-item.scss b/app/assets/stylesheets/common/components/user-stream-item.scss index 8ed3a54c323..a57e540dbb4 100644 --- a/app/assets/stylesheets/common/components/user-stream-item.scss +++ b/app/assets/stylesheets/common/components/user-stream-item.scss @@ -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 { diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 96c3621f8b8..9dc6c8434dc 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -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, ), diff --git a/app/serializers/group_post_serializer.rb b/app/serializers/group_post_serializer.rb index 0eed59b7a63..2ff638e7a70 100644 --- a/app/serializers/group_post_serializer.rb +++ b/app/serializers/group_post_serializer.rb @@ -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 diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index d98be8d6813..8ae1f9e5b0e 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -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 diff --git a/spec/requests/api/schemas/json/post_replies_response.json b/spec/requests/api/schemas/json/post_replies_response.json index 8c9ecb3e582..d93837743e6 100644 --- a/spec/requests/api/schemas/json/post_replies_response.json +++ b/spec/requests/api/schemas/json/post_replies_response.json @@ -32,6 +32,9 @@ "post_type": { "type": "integer" }, + "posts_count": { + "type": "integer" + }, "updated_at": { "type": "string" }, @@ -155,23 +158,22 @@ }, "actions_summary": { "type": "array", - "items": - { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "integer" - }, - "can_act": { - "type": "boolean" - } + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer" }, - "required": [ - "id", - "can_act" - ] - } + "can_act": { + "type": "boolean" + } + }, + "required": [ + "id", + "can_act" + ] + } }, "moderator": { "type": "boolean" @@ -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" ] } -} +} \ No newline at end of file diff --git a/spec/requests/api/schemas/json/post_show_response.json b/spec/requests/api/schemas/json/post_show_response.json index 2148cd0a0f9..85ba81115b4 100644 --- a/spec/requests/api/schemas/json/post_show_response.json +++ b/spec/requests/api/schemas/json/post_show_response.json @@ -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" ] -} +} \ No newline at end of file diff --git a/spec/requests/api/schemas/json/post_update_response.json b/spec/requests/api/schemas/json/post_update_response.json index bb004f4ca06..27ee8bbf9ed 100644 --- a/spec/requests/api/schemas/json/post_update_response.json +++ b/spec/requests/api/schemas/json/post_update_response.json @@ -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,11 +276,12 @@ "wiki", "reviewable_id", "reviewable_score_count", - "reviewable_score_pending_count" + "reviewable_score_pending_count", + "post_url" ] } }, "required": [ "post" ] -} +} \ No newline at end of file diff --git a/spec/requests/api/schemas/json/topic_create_response.json b/spec/requests/api/schemas/json/topic_create_response.json index 5cb486c8508..9849ca27e64 100644 --- a/spec/requests/api/schemas/json/topic_create_response.json +++ b/spec/requests/api/schemas/json/topic_create_response.json @@ -31,6 +31,9 @@ "post_type": { "type": "integer" }, + "posts_count": { + "type": "integer" + }, "updated_at": { "type": "string" }, @@ -142,23 +145,22 @@ }, "actions_summary": { "type": "array", - "items": - { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "integer" - }, - "can_act": { - "type": "boolean" - } + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer" }, - "required": [ - "id", - "can_act" - ] - } + "can_act": { + "type": "boolean" + } + }, + "required": [ + "id", + "can_act" + ] + } }, "moderator": { "type": "boolean" @@ -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" ] -} +} \ No newline at end of file diff --git a/spec/serializers/web_hook_post_serializer_spec.rb b/spec/serializers/web_hook_post_serializer_spec.rb index f4c364a542d..c3ceca71e6a 100644 --- a/spec/serializers/web_hook_post_serializer_spec.rb +++ b/spec/serializers/web_hook_post_serializer_spec.rb @@ -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,