mirror of
https://github.com/discourse/discourse.git
synced 2025-01-01 15:15:50 +08:00
DEV: Reusable post-list component (#30312)
This update adds a ✨ _new_ `<PostList />` component, along with it's child components (`<PostListItem/>` and `<PostListItemDetails />`). This new generic component can be used to show a list of posts.
It can be used like so:
```js
/**
* A component that renders a list of posts
*
* @component PostList
*
* @args {Array<Object>} posts - The array of post objects to display
* @args {Function} fetchMorePosts - A function that fetches more posts. Must return a Promise that resolves to an array of new posts.
* @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
*
*/
```
```hbs
<PostList
@posts={{this.posts}}
@fetchMorePosts={{this.loadMorePosts}}
@emptyText={{i18n "custom_identifier.empty"}}
@additionalItemClasses="custom-class"
/>
```
This commit is contained in:
parent
6cd964306f
commit
d886c55f63
|
@ -1,48 +0,0 @@
|
|||
<div class="user-stream-item__header info">
|
||||
<a
|
||||
href={{this.userUrl}}
|
||||
data-user-card={{this.post.user.username}}
|
||||
class="avatar-link"
|
||||
>
|
||||
{{avatar
|
||||
this.post.user
|
||||
imageSize="large"
|
||||
extraClasses="actor"
|
||||
ignoreTitle="true"
|
||||
}}
|
||||
</a>
|
||||
|
||||
<div class="user-stream-item__details">
|
||||
<div class="stream-topic-title">
|
||||
<span class="title">
|
||||
<a href={{this.postUrl}} aria-label={{this.titleAriaLabel}}>
|
||||
{{html-safe this.post.topic.fancyTitle}}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="group-post-category">{{category-link this.post.category}}</div>
|
||||
{{#if this.post.user}}
|
||||
<div class="group-member-info names">
|
||||
<span class="name">{{this.name}}</span>
|
||||
{{#if this.post.user.title}}<span
|
||||
class="user-title"
|
||||
>{{this.post.user.title}}</span>{{/if}}
|
||||
<PluginOutlet
|
||||
@name="group-post-additional-member-info"
|
||||
@outletArgs={{hash user=this.post.user}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<ExpandPost @item={{this.post}} />
|
||||
<span class="time">{{format-date this.post.created_at leaveAgo="true"}}</span>
|
||||
</div>
|
||||
|
||||
<div class="excerpt">
|
||||
{{#if this.post.expandedExcerpt}}
|
||||
{{html-safe this.post.expandedExcerpt}}
|
||||
{{else}}
|
||||
{{html-safe this.post.excerpt}}
|
||||
{{/if}}
|
||||
</div>
|
|
@ -1,49 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import { classNameBindings } from "@ember-decorators/component";
|
||||
import { propertyEqual } from "discourse/lib/computed";
|
||||
import { prioritizeNameInUx } from "discourse/lib/settings";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
@classNameBindings(
|
||||
":user-stream-item",
|
||||
":item",
|
||||
"moderatorAction",
|
||||
"primaryGroup"
|
||||
)
|
||||
export default class GroupPost extends Component {
|
||||
@propertyEqual("post.post_type", "site.post_types.moderator_action")
|
||||
moderatorAction;
|
||||
|
||||
@discourseComputed("post.url")
|
||||
postUrl(url) {
|
||||
return getURL(url);
|
||||
}
|
||||
|
||||
@discourseComputed("post.user")
|
||||
name(postUser) {
|
||||
if (prioritizeNameInUx(postUser.name)) {
|
||||
return postUser.name;
|
||||
}
|
||||
return postUser.username;
|
||||
}
|
||||
|
||||
@discourseComputed("post.user")
|
||||
primaryGroup(postUser) {
|
||||
if (postUser.primary_group_name) {
|
||||
return `group-${postUser.primary_group_name}`;
|
||||
}
|
||||
}
|
||||
|
||||
@discourseComputed("post.user.username")
|
||||
userUrl(username) {
|
||||
return userPath(username.toLowerCase());
|
||||
}
|
||||
|
||||
@discourseComputed("post.title", "post.post_number")
|
||||
titleAriaLabel(title, postNumber) {
|
||||
return i18n("groups.aria_post_number", { postNumber, title });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/**
|
||||
* A component that renders a list of posts
|
||||
*
|
||||
* @component PostList
|
||||
*
|
||||
* @args {Array<Object>} posts - The array of post objects to display
|
||||
* @args {Function} fetchMorePosts - A function that fetches more posts. Must return a Promise that resolves to an array of new posts.
|
||||
* @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
|
||||
*
|
||||
* @template Usage Example:
|
||||
* ```
|
||||
* <PostList
|
||||
* @posts={{this.posts}}
|
||||
* @fetchMorePosts={{this.loadMorePosts}}
|
||||
* @emptyText={{i18n "custom_identifier.empty"}}
|
||||
* @additionalItemClasses="custom-class"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import LoadMore from "discourse/components/load-more";
|
||||
import PostListItem from "discourse/components/post-list/item";
|
||||
import hideApplicationFooter from "discourse/helpers/hide-application-footer";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class PostList extends Component {
|
||||
@tracked loading = false;
|
||||
@tracked canLoadMore = true;
|
||||
@tracked emptyText = this.args.emptyText || i18n("post_list.empty");
|
||||
|
||||
@action
|
||||
async loadMore() {
|
||||
if (
|
||||
!this.canLoadMore ||
|
||||
this.loading ||
|
||||
this.args.fetchMorePosts === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
|
||||
const posts = this.args.posts;
|
||||
if (posts && posts.length) {
|
||||
try {
|
||||
const newPosts = await this.args.fetchMorePosts();
|
||||
this.args.posts.addObjects(newPosts);
|
||||
|
||||
if (newPosts.length === 0) {
|
||||
this.canLoadMore = false;
|
||||
}
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.canLoadMore}}
|
||||
{{hideApplicationFooter}}
|
||||
{{/if}}
|
||||
|
||||
<LoadMore @selector=".post-list-item" @action={{this.loadMore}}>
|
||||
<div class="post-list">
|
||||
{{#each @posts as |post|}}
|
||||
<PostListItem
|
||||
@post={{post}}
|
||||
@additionalItemClasses={{@additionalItemClasses}}
|
||||
@titleAriaLabel={{@titleAriaLabel}}
|
||||
/>
|
||||
{{else}}
|
||||
<div class="post-list__empty-text">{{this.emptyText}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
||||
</LoadMore>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { hash } from "@ember/helper";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import categoryLink from "discourse/helpers/category-link";
|
||||
import { prioritizeNameInUx } from "discourse/lib/settings";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class PostListItemDetails extends Component {
|
||||
get titleAriaLabel() {
|
||||
return (
|
||||
this.args.titleAriaLabel ||
|
||||
i18n("post_list.aria_post_number", {
|
||||
title: this.args.post.title,
|
||||
postNumber: this.args.post.post_number,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get posterName() {
|
||||
if (prioritizeNameInUx(this.args.post.user.name)) {
|
||||
return this.args.post.user.name;
|
||||
}
|
||||
return this.args.post.user.username;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="post-list-item__details">
|
||||
<div class="stream-topic-title">
|
||||
<span class="title">
|
||||
<a
|
||||
href={{getURL @post.url}}
|
||||
aria-label={{this.titleAriaLabel}}
|
||||
>{{htmlSafe @post.topic.fancyTitle}}</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="stream-post-category">
|
||||
{{categoryLink @post.category}}
|
||||
</div>
|
||||
|
||||
{{#if @post.user}}
|
||||
<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}}
|
||||
|
||||
<PluginOutlet
|
||||
@name="post-list-additional-member-info"
|
||||
@outletArgs={{hash user=@post.user}}
|
||||
/>
|
||||
|
||||
{{!
|
||||
Deprecated Outlet:
|
||||
Please use: "post-list-additional-member-info" instead
|
||||
}}
|
||||
<PluginOutlet
|
||||
@name="group-post-additional-member-info"
|
||||
@outletArgs={{hash user=@post.user}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
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 formatDate from "discourse/helpers/format-date";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
|
||||
export default class PostListItem extends Component {
|
||||
@service site;
|
||||
|
||||
get moderatorActionClass() {
|
||||
return this.args.post.post_type === this.site.post_types.moderator_action
|
||||
? "moderator-action"
|
||||
: "";
|
||||
}
|
||||
|
||||
get primaryGroupClass() {
|
||||
if (this.args.post.user && this.args.post.user.primary_group_name) {
|
||||
return `group-${this.args.post.user.primary_group_name}`;
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="post-list-item
|
||||
{{concatClass
|
||||
this.moderatorActionClass
|
||||
this.primaryGroupClass
|
||||
@additionalItemClasses
|
||||
}}"
|
||||
>
|
||||
<div class="post-list-item__header info">
|
||||
<a
|
||||
href={{userPath @post.user.username}}
|
||||
data-user-card={{@post.user.username}}
|
||||
class="avatar-link"
|
||||
>
|
||||
{{avatar
|
||||
@post.user
|
||||
imageSize="large"
|
||||
extraClasses="actor"
|
||||
ignoreTitle="true"
|
||||
}}
|
||||
</a>
|
||||
|
||||
<PostListItemDetails
|
||||
@post={{@post}}
|
||||
@titleAriaLabel={{@titleAriaLabel}}
|
||||
/>
|
||||
<ExpandPost @item={{@post}} />
|
||||
<div class="time">{{formatDate @post.created_at leaveAgo="true"}}</div>
|
||||
</div>
|
||||
|
||||
<div class="excerpt">
|
||||
{{#if @post.expandedExcerpt}}
|
||||
{{htmlSafe @post.expandedExcerpt}}
|
||||
{{else}}
|
||||
{{htmlSafe @post.excerpt}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -1,45 +1,18 @@
|
|||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { fmt } from "discourse/lib/computed";
|
||||
|
||||
export default class GroupActivityPostsController extends Controller {
|
||||
@controller group;
|
||||
@controller groupActivity;
|
||||
@controller application;
|
||||
|
||||
@fmt("type", "groups.empty.%@") emptyText;
|
||||
|
||||
canLoadMore = true;
|
||||
loading = false;
|
||||
|
||||
@action
|
||||
loadMore() {
|
||||
if (!this.canLoadMore) {
|
||||
return;
|
||||
}
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
this.set("loading", true);
|
||||
async fetchMorePosts() {
|
||||
const posts = this.model;
|
||||
if (posts && posts.length) {
|
||||
const before = posts[posts.length - 1].get("created_at");
|
||||
const group = this.get("group.model");
|
||||
const before = posts[posts.length - 1].created_at;
|
||||
const group = this.group.model;
|
||||
const categoryId = this.groupActivity.category_id;
|
||||
const opts = { before, type: this.type, categoryId };
|
||||
|
||||
let categoryId = this.get("groupActivity.category_id");
|
||||
const opts = { before, type: this.type, categoryId };
|
||||
|
||||
group
|
||||
.findPosts(opts)
|
||||
.then((newPosts) => {
|
||||
posts.addObjects(newPosts);
|
||||
if (newPosts.length === 0) {
|
||||
this.set("canLoadMore", false);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("loading", false);
|
||||
});
|
||||
}
|
||||
return await group.findPosts(opts);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,5 @@
|
|||
{{#if this.canLoadMore}}
|
||||
{{hide-application-footer}}
|
||||
{{/if}}
|
||||
|
||||
<LoadMore @selector=".user-stream-item" @action={{action "loadMore"}}>
|
||||
<div class="user-stream">
|
||||
{{#each this.model as |post|}}
|
||||
<GroupPost @post={{post}} />
|
||||
{{else}}
|
||||
<div>{{i18n this.emptyText}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
||||
</LoadMore>
|
||||
<PostList
|
||||
@posts={{this.model}}
|
||||
@fetchMorePosts={{this.fetchMorePosts}}
|
||||
@emptyText={{i18n "groups.empty.posts"}}
|
||||
/>
|
|
@ -29,7 +29,7 @@ acceptance("Group - Anonymous", function (needs) {
|
|||
|
||||
await click(".nav-pills li a[title='Activity']");
|
||||
|
||||
assert.dom(".user-stream-item").exists("lists stream items");
|
||||
assert.dom(".post-list-item").exists("lists stream items");
|
||||
|
||||
await click(".activity-nav li a[href='/g/discourse/activity/topics']");
|
||||
|
||||
|
@ -38,14 +38,14 @@ acceptance("Group - Anonymous", function (needs) {
|
|||
|
||||
await click(".activity-nav li a[href='/g/discourse/activity/mentions']");
|
||||
|
||||
assert.dom(".user-stream-item").exists("lists stream items");
|
||||
assert.dom(".post-list-item").exists("lists stream items");
|
||||
assert
|
||||
.dom(".nav-pills li a[title='Edit Group']")
|
||||
.doesNotExist("does not show messages tab if user is not admin");
|
||||
assert
|
||||
.dom(".nav-pills li a[title='Logs']")
|
||||
.doesNotExist("does not show Logs tab if user is not admin");
|
||||
assert.dom(".user-stream-item").exists("lists stream items");
|
||||
assert.dom(".post-list-item").exists("lists stream items");
|
||||
|
||||
const groupDropdown = selectKit(".group-dropdown");
|
||||
await groupDropdown.expand();
|
||||
|
@ -268,7 +268,7 @@ acceptance("Group - Authenticated", function (needs) {
|
|||
await visit("/g/discourse/activity/posts");
|
||||
|
||||
assert
|
||||
.dom(".user-stream-item a.avatar-link")
|
||||
.dom(".post-list-item a.avatar-link")
|
||||
.hasAttribute(
|
||||
"href",
|
||||
"/u/awesomerobot",
|
||||
|
|
42
app/assets/javascripts/discourse/tests/fixtures/post-list.js
vendored
Normal file
42
app/assets/javascripts/discourse/tests/fixtures/post-list.js
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
const postModel = [
|
||||
{
|
||||
id: 1,
|
||||
title: "My dog is so cute",
|
||||
created_at: "2024-03-15T18:45:38.720Z",
|
||||
category: {
|
||||
id: 1,
|
||||
name: "Pets",
|
||||
color: "f00",
|
||||
},
|
||||
user: {
|
||||
id: 1,
|
||||
username: "uwe_keim",
|
||||
name: "Uwe Keim",
|
||||
avatar_template: "/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png"
|
||||
},
|
||||
cooked:
|
||||
"<p>I am really enjoying having a dog. My dog is so cute. He is a toy poodle, and he loves to play fetch.</p><p>He also loves to go outside to the dog park, eat treats, and take naps.</p>",
|
||||
excerpt: "<p>I am really enjoying having a dog. My dog is so cute. He is a toy poodle...</p>",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "My cat is adorable",
|
||||
created_at: "2024-03-16T18:45:38.720Z",
|
||||
category: {
|
||||
id: 1,
|
||||
name: "Pets",
|
||||
color: "f00",
|
||||
},
|
||||
user: {
|
||||
id: 1,
|
||||
username: "uwe_keim",
|
||||
name: "Uwe Keim",
|
||||
avatar_template: "/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png"
|
||||
},
|
||||
cooked:
|
||||
"<p>I am really enjoying having a cat. My cat is so cute. She loves to cuddle.</p><p>She also loves to go wander the neighbourhood.</p>",
|
||||
excerpt: "<p>I am really enjoying having a cat. My cat is so cute...</p>",
|
||||
},
|
||||
];
|
||||
|
||||
export default postModel;
|
|
@ -0,0 +1,46 @@
|
|||
import { render } from "@ember/test-helpers";
|
||||
import { module, test } from "qunit";
|
||||
import PostList from "discourse/components/post-list";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import postModel from "../../fixtures/post-list";
|
||||
|
||||
module("Integration | Component | PostList | Index", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("@posts", async function (assert) {
|
||||
const posts = postModel;
|
||||
await render(<template><PostList @posts={{posts}} /></template>);
|
||||
assert.dom(".post-list").exists();
|
||||
assert.dom(".post-list__empty-text").doesNotExist();
|
||||
assert.dom(".post-list-item").exists({ count: 2 });
|
||||
});
|
||||
|
||||
test("@additionalItemClasses", async function (assert) {
|
||||
const posts = postModel;
|
||||
const additionalClasses = ["first-class", "second-class"];
|
||||
await render(<template>
|
||||
<PostList @posts={{posts}} @additionalItemClasses={{additionalClasses}} />
|
||||
</template>);
|
||||
assert.dom(".post-list-item").hasClass("first-class");
|
||||
assert.dom(".post-list-item").hasClass("second-class");
|
||||
});
|
||||
|
||||
test("@titleAriaLabel", async function (assert) {
|
||||
const posts = postModel;
|
||||
const titleAriaLabel = "My custom aria title label";
|
||||
await render(<template>
|
||||
<PostList @posts={{posts}} @titleAriaLabel={{titleAriaLabel}} />
|
||||
</template>);
|
||||
assert
|
||||
.dom(".post-list-item__details .title a")
|
||||
.hasAttribute("aria-label", titleAriaLabel);
|
||||
});
|
||||
|
||||
test("@emptyText", async function (assert) {
|
||||
const posts = [];
|
||||
await render(<template>
|
||||
<PostList @posts={{posts}} @emptyText="My custom empty text" />
|
||||
</template>);
|
||||
assert.dom(".post-list__empty-text").hasText("My custom empty text");
|
||||
});
|
||||
});
|
|
@ -54,6 +54,7 @@
|
|||
@import "user-status-picker";
|
||||
@import "user-stream-item";
|
||||
@import "user-stream";
|
||||
@import "post-list";
|
||||
@import "widget-dropdown";
|
||||
@import "welcome-header";
|
||||
@import "notifications-tracking";
|
||||
|
|
81
app/assets/stylesheets/common/components/post-list.scss
Normal file
81
app/assets/stylesheets/common/components/post-list.scss
Normal file
|
@ -0,0 +1,81 @@
|
|||
.post-list {
|
||||
margin: 0;
|
||||
|
||||
.post-list-item {
|
||||
background: var(--d-content-background, var(--secondary));
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
padding: 1em 0.53em;
|
||||
list-style: none;
|
||||
|
||||
&.moderator-action {
|
||||
background-color: var(--highlight-bg);
|
||||
}
|
||||
|
||||
&.deleted {
|
||||
background-color: var(--danger-low-mid);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: block;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&__details {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
.badge-category__wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.stream-topic-title {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.relative-date {
|
||||
line-height: var(--line-height-small);
|
||||
color: var(--primary-medium);
|
||||
font-size: var(--font-down-2);
|
||||
padding-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-link {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: var(--font-0);
|
||||
max-width: 400px;
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.excerpt {
|
||||
margin: 1em 0 0 0;
|
||||
font-size: var(--font-0);
|
||||
word-wrap: break-word;
|
||||
color: var(--primary);
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
details.disabled {
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
.emoji.only-emoji {
|
||||
// oversized emoji break excerpt layout
|
||||
// so we match inline emoji size
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.post-member-info {
|
||||
display: flex;
|
||||
}
|
||||
}
|
|
@ -1008,13 +1008,13 @@ en:
|
|||
public_admission: "Allow users to join the group freely (Requires publicly visible group)"
|
||||
public_exit: "Allow users to leave the group freely"
|
||||
empty:
|
||||
posts: "There are no posts by members of this group."
|
||||
members: "There are no members in this group."
|
||||
requests: "There are no membership requests for this group."
|
||||
mentions: "There are no mentions of this group."
|
||||
messages: "There are no messages for this group."
|
||||
topics: "There are no topics by members of this group."
|
||||
logs: "There are no logs for this group."
|
||||
posts: "There are no posts by members of this group"
|
||||
members: "There are no members in this group"
|
||||
requests: "There are no membership requests for this group"
|
||||
mentions: "There are no mentions of this group"
|
||||
messages: "There are no messages for this group"
|
||||
topics: "There are no topics by members of this group"
|
||||
logs: "There are no logs for this group"
|
||||
add: "Add"
|
||||
join: "Join"
|
||||
leave: "Leave"
|
||||
|
@ -3747,6 +3747,10 @@ en:
|
|||
other: "You have selected <b>%{count}</b> posts."
|
||||
deleted_by_author_simple: "(topic deleted by author)"
|
||||
|
||||
post_list:
|
||||
empty: "There are no posts"
|
||||
aria_post_number: "%{title} - post #%{postNumber}"
|
||||
|
||||
post:
|
||||
confirm_delete: "Are you sure you want to delete this post?"
|
||||
quote_reply: "Quote"
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import PostList from "discourse/components/post-list";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import StyleguideExample from "../../styleguide-example";
|
||||
|
||||
const StyleguidePostList = <template>
|
||||
<StyleguideExample
|
||||
@title={{i18n "styleguide.sections.post_list.empty_example"}}
|
||||
>
|
||||
<PostList @posts="" @additionalItemClasses="styleguide-post-list-item" />
|
||||
</StyleguideExample>
|
||||
|
||||
<StyleguideExample
|
||||
@title={{i18n "styleguide.sections.post_list.populated_example"}}
|
||||
>
|
||||
<PostList
|
||||
@posts={{@dummy.postList}}
|
||||
@additionalItemClasses="styleguide-post-list-item"
|
||||
/>
|
||||
</StyleguideExample>
|
||||
</template>;
|
||||
|
||||
export default StyleguidePostList;
|
|
@ -153,14 +153,29 @@ export function createData(store) {
|
|||
|
||||
<p>Case everti equidem ius ea, ubique veritus vim id. Eros omnium conclusionemque qui te, usu error alienum imperdiet ut, ex ius meis adipisci. Libris reprehendunt eos ex, mea at nisl suavitate. Altera virtute democritum pro cu, melius latine in ius.</p>`;
|
||||
|
||||
const excerpt =
|
||||
"<p>Lorem ipsum dolor sit amet, et nec quis viderer prompta, ex omnium ponderum insolens eos, sed discere invenire principes in. Fuisset constituto per ad. Est no scripta propriae facilisis, viderer impedit deserunt in mel. Quot debet facilisis ne vix, nam in detracto tacimates.</p>";
|
||||
|
||||
const transformedPost = {
|
||||
id: 1234,
|
||||
topic,
|
||||
user: {
|
||||
avatar_template: user.avatar_template,
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
},
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
avatar_template: user.avatar_template,
|
||||
category: {
|
||||
id: categories[0].id,
|
||||
name: categories[0].name,
|
||||
color: categories[0].color,
|
||||
},
|
||||
created_at: "2024-11-13T21:12:37.835Z",
|
||||
cooked,
|
||||
excerpt,
|
||||
post_number: 1,
|
||||
post_type: 1,
|
||||
updated_at: moment().subtract(2, "days"),
|
||||
|
@ -262,8 +277,64 @@ export function createData(store) {
|
|||
const postModel = store.createRecord("post", {
|
||||
transformedPost,
|
||||
});
|
||||
|
||||
postModel.set("topic", store.createRecord("topic", transformedPost.topic));
|
||||
|
||||
const postList = [
|
||||
transformedPost,
|
||||
{
|
||||
id: 145,
|
||||
topic: pinnedTopic,
|
||||
created_at: "2024-03-15T18:45:38.720Z",
|
||||
category: {
|
||||
id: categories[2].id,
|
||||
color: categories[2].color,
|
||||
name: categories[2].name,
|
||||
},
|
||||
user: {
|
||||
avatar_template: user.avatar_template,
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
},
|
||||
excerpt,
|
||||
},
|
||||
{
|
||||
id: 144,
|
||||
topic: archivedTopic,
|
||||
created_at: "2024-02-15T18:45:38.720Z",
|
||||
category: {
|
||||
id: categories[1].id,
|
||||
color: categories[1].color,
|
||||
name: categories[1].name,
|
||||
},
|
||||
user: {
|
||||
avatar_template: user.avatar_template,
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
},
|
||||
excerpt,
|
||||
},
|
||||
{
|
||||
id: 143,
|
||||
topic: closedTopic,
|
||||
created_at: "2024-01-15T18:45:38.720Z",
|
||||
category: {
|
||||
id: categories[0].id,
|
||||
color: categories[0].color,
|
||||
name: categories[0].name,
|
||||
},
|
||||
user: {
|
||||
avatar_template: user.avatar_template,
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
},
|
||||
excerpt,
|
||||
},
|
||||
];
|
||||
|
||||
_data = {
|
||||
options: [
|
||||
{ id: 1, name: "Orange" },
|
||||
|
@ -316,6 +387,7 @@ export function createData(store) {
|
|||
|
||||
transformedPost,
|
||||
postModel,
|
||||
postList,
|
||||
|
||||
user,
|
||||
|
||||
|
|
|
@ -25,7 +25,8 @@ import topicListItem from "../components/sections/molecules/topic-list-item";
|
|||
import topicNotifications from "../components/sections/molecules/topic-notifications";
|
||||
import topicTimerInfo from "../components/sections/molecules/topic-timer-info";
|
||||
import post from "../components/sections/organisms/00-post";
|
||||
import topicMap from "../components/sections/organisms/01-topic-map";
|
||||
import postList from "../components/sections/organisms/01-post-list";
|
||||
import topicMap from "../components/sections/organisms/02-topic-map";
|
||||
import topicFooterButtons from "../components/sections/organisms/03-topic-footer-buttons";
|
||||
import topicList from "../components/sections/organisms/04-topic-list";
|
||||
import basicTopicList from "../components/sections/organisms/basic-topic-list";
|
||||
|
@ -84,7 +85,8 @@ const SECTIONS = [
|
|||
},
|
||||
{ component: topicTimerInfo, category: "molecules", id: "topic-timer-info" },
|
||||
{ component: post, category: "organisms", id: "post", priority: 0 },
|
||||
{ component: topicMap, category: "organisms", id: "topic-map", priority: 1 },
|
||||
{ component: postList, category: "organisms", id: "post-list", priority: 1 },
|
||||
{ component: topicMap, category: "organisms", id: "topic-map", priority: 2 },
|
||||
{
|
||||
component: topicFooterButtons,
|
||||
category: "organisms",
|
||||
|
|
|
@ -70,6 +70,10 @@ en:
|
|||
title: "Topic Notifications"
|
||||
post:
|
||||
title: "Post"
|
||||
post_list:
|
||||
title: "Post List"
|
||||
empty_example: "<PostList /> empty state"
|
||||
populated_example: "<PostList /> populated state"
|
||||
topic_map:
|
||||
title: "Topic Map"
|
||||
site_header:
|
||||
|
|
|
@ -40,6 +40,7 @@ RSpec.describe "Styleguide Smoke Test", type: :system do
|
|||
],
|
||||
"ORGANISMS" => [
|
||||
{ href: "/organisms/post", title: "Post" },
|
||||
{ href: "/organisms/post-list", title: "Post List" },
|
||||
{ href: "/organisms/topic-map", title: "Topic Map" },
|
||||
{ href: "/organisms/topic-footer-buttons", title: "Topic Footer Buttons" },
|
||||
{ href: "/organisms/topic-list", title: "Topic List" },
|
||||
|
|
|
@ -9,12 +9,12 @@ module PageObjects
|
|||
end
|
||||
|
||||
def has_user_stream_item?(count:)
|
||||
has_css?(".user-stream-item", count: count)
|
||||
has_css?(".post-list-item", count: count)
|
||||
end
|
||||
|
||||
def scroll_to_last_item
|
||||
page.execute_script <<~JS
|
||||
document.querySelector('.user-stream-item:last-of-type').scrollIntoView(true);
|
||||
document.querySelector('.post-list-item:last-of-type').scrollIntoView(true);
|
||||
JS
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue
Block a user