UX: improve drafts list (#31122)

Improves the layout for the drafts list page, including the addition of
icons to represent the content type.

Internal ref: /t/129117
This commit is contained in:
David Battersby 2025-02-04 11:42:17 +04:00 committed by GitHub
parent 503f9b6f02
commit 41ce3d868e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 170 additions and 82 deletions

View File

@ -86,6 +86,8 @@ export default class PostList extends Component {
@additionalItemClasses={{@additionalItemClasses}}
@titleAriaLabel={{@titleAriaLabel}}
@showUserInfo={{@showUserInfo}}
@resumeDraft={{@resumeDraft}}
@removeDraft={{@removeDraft}}
>
<:abovePostItemHeader>
{{yield post to="abovePostItemHeader"}}

View File

@ -1,8 +1,13 @@
import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import { fn, hash } from "@ember/helper";
import { or } from "truth-helpers";
import DButton from "discourse/components/d-button";
import PluginOutlet from "discourse/components/plugin-outlet";
import TopicStatus from "discourse/components/topic-status";
import avatar from "discourse/helpers/avatar";
import categoryLink from "discourse/helpers/category-link";
import icon from "discourse/helpers/d-icon";
import formatDate from "discourse/helpers/format-date";
import getURL from "discourse/lib/get-url";
import { prioritizeNameInUx } from "discourse/lib/settings";
import { i18n } from "discourse-i18n";
@ -28,6 +33,10 @@ export default class PostListItemDetails extends Component {
: this.args.post.title;
}
get draftTitle() {
return this.args.post.title ?? this.args.post.data.title;
}
get titleAriaLabel() {
if (this.args.titleAriaLabel) {
return this.args.titleAriaLabel;
@ -58,14 +67,42 @@ export default class PostListItemDetails extends Component {
href={{getURL this.url}}
aria-label={{this.titleAriaLabel}}
>{{this.topicTitle}}</a>
{{else if @isDraft}}
<DButton
@action={{fn @resumeDraft @post}}
class="btn-transparent draft-title"
>
{{or this.draftTitle (i18n "drafts.dropdown.untitled")}}
</DButton>
{{else}}
{{this.topicTitle}}
{{/if}}
</span>
</div>
<div class="category stream-post-category">
{{categoryLink @post.category}}
<div class="post-list-item__metadata">
{{#if @post.category}}
<span class="category stream-post-category">
{{categoryLink @post.category}}
</span>
{{/if}}
<span class="time">
{{formatDate @post.created_at leaveAgo="true"}}
</span>
{{#if @post.deleted_by}}
<span class="delete-info">
{{icon "trash-can"}}
{{avatar
@post.deleted_by
imageSize="tiny"
extraClasses="actor"
ignoreTitle="true"
}}
{{formatDate @item.deleted_at leaveAgo="true"}}
</span>
{{/if}}
</div>
{{#if this.showUserInfo}}

View File

@ -1,12 +1,13 @@
import Component from "@glimmer/component";
import { fn } from "@ember/helper";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import DButton from "discourse/components/d-button";
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 icon from "discourse/helpers/d-icon";
import formatDate from "discourse/helpers/format-date";
import { userPath } from "discourse/lib/url";
export default class PostListItem extends Component {
@ -53,6 +54,22 @@ export default class PostListItem extends Component {
: this.args.post.id;
}
get isDraft() {
return this.args.post.constructor.name === "UserDraft";
}
get draftIcon() {
const key = this.args.post.draft_key;
if (key.startsWith("new_private_message")) {
return "envelope";
} else if (key.startsWith("new_topic")) {
return "layer-group";
} else {
return "reply";
}
}
<template>
<div
class="post-list-item
@ -66,20 +83,26 @@ export default class PostListItem extends Component {
{{yield to="abovePostItemHeader"}}
<div class="post-list-item__header info">
<a
href={{userPath this.user.username}}
data-user-card={{this.user.username}}
class="avatar-link"
>
<div class="avatar-wrapper">
{{avatar
this.user
imageSize="large"
extraClasses="actor"
ignoreTitle="true"
}}
{{#if this.isDraft}}
<div class="draft-icon">
{{icon this.draftIcon class="icon"}}
</div>
</a>
{{else}}
<a
href={{userPath this.user.username}}
data-user-card={{this.user.username}}
class="avatar-link"
>
<div class="avatar-wrapper">
{{avatar
this.user
imageSize="large"
extraClasses="actor"
ignoreTitle="true"
}}
</div>
</a>
{{/if}}
<PostListItemDetails
@post={{@post}}
@ -88,33 +111,31 @@ export default class PostListItem extends Component {
@urlPath={{@urlPath}}
@user={{this.user}}
@showUserInfo={{@showUserInfo}}
@isDraft={{this.isDraft}}
@resumeDraft={{@resumeDraft}}
/>
{{#if @post.draftType}}
<span class="draft-type">{{@post.draftType}}</span>
{{else}}
{{#unless @post.draftType}}
<ExpandPost @item={{@post}} />
{{/unless}}
{{#if @post.editableDraft}}
<div class="user-stream-item-draft-actions">
<DButton
@action={{fn @resumeDraft @post}}
@icon="pencil"
@title="drafts.resume"
class="btn-default resume-draft"
/>
<DButton
@action={{fn @removeDraft @post}}
@icon="trash-can"
@title="drafts.remove"
class="btn-danger remove-draft"
/>
</div>
{{/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">
{{icon "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>

View File

@ -1,13 +1,12 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { fn, hash } from "@ember/helper";
import { 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";
@ -164,6 +163,8 @@ export default class UserStreamComponent extends Component {
@titlePath="titleHtml"
@additionalItemClasses="user-stream-item"
@showUserInfo={{false}}
@resumeDraft={{this.resumeDraft}}
@removeDraft={{this.removeDraft}}
class={{concatClass "user-stream" this.filterClassName}}
{{this.eventListeners @stream}}
>
@ -216,23 +217,6 @@ export default class UserStreamComponent extends Component {
{{/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|>

View File

@ -47,7 +47,7 @@ acceptance("User Drafts", function (needs) {
);
assert
.dom(".user-stream-item:nth-child(2) a.avatar-link")
.hasAttribute("href", "/u/eviltrout", "has correct avatar link");
.dom(".user-stream-item:nth-child(2) .draft-icon .d-icon")
.hasClass("d-icon-reply", "has correct icon");
});
});

View File

@ -24,31 +24,64 @@
display: block;
opacity: 0.4;
}
}
.user-stream-item__header {
display: flex;
align-items: flex-start;
}
.user-stream-item__details {
flex-grow: 1;
min-width: 0;
.badge-category__wrapper {
width: 100%;
.post-list-item__header {
display: flex;
align-items: flex-start;
}
.post-list-item__details {
flex-grow: 1;
min-width: 0;
.badge-category__wrapper {
width: auto;
}
}
.post-list-item__metadata {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.5em;
span + span {
&::before {
content: "";
margin-right: 0.25em;
vertical-align: middle;
color: var(--primary-medium);
}
}
}
}
.draft-icon {
color: var(--primary-high);
font-size: var(--font-up-4);
margin-right: 0.5rem;
text-align: center;
}
.stream-topic-title {
overflow-wrap: anywhere;
}
.user-stream-item__metadata {
align-items: end;
display: flex;
flex-direction: column;
margin-left: 0.25em;
.draft-title {
color: var(--tertiary);
padding: 0;
}
.user-stream-item__metadata ul {
display: inline-block;
margin: 0;
padding: 0;
color: var(--primary-medium);
li {
display: inline-flex;
margin-left: 0.5em;
}
}
.type,
@ -62,7 +95,7 @@
line-height: var(--line-height-small);
color: var(--primary-medium);
font-size: var(--font-down-2);
padding-top: 6px;
padding-top: unset !important;
margin-right: 0.5rem;
}
@ -138,10 +171,6 @@
padding: 3px 5px 5px 5px;
}
.remove-draft {
float: right;
}
.excerpt {
margin: 1em 0 0 0;
font-size: var(--font-0);
@ -168,6 +197,13 @@
.group-member-info {
display: flex;
}
.user-stream-item .user-stream-item-draft-actions {
display: flex;
align-items: end;
flex-direction: row;
gap: 0.75em;
}
}
.user-stream .child-actions, /* DEPRECATED: '.user-stream .child-actions' selector*/

View File

@ -4,3 +4,4 @@
@import "sidebar/sidebar-section";
@import "user-card";
@import "user-info";
@import "user-stream-item";

View File

@ -0,0 +1,7 @@
.user-stream-item .draft-icon {
width: 3rem;
}
.user-stream-item .excerpt {
margin: 0.75em 0 0 3.5em;
}