Revert "REFACTOR: user directories without `<table>` (#20316)" (#20513)

This reverts commit e206bd8907.
This commit is contained in:
Kris 2023-03-02 12:52:02 -05:00 committed by GitHub
parent e204c61bd8
commit 654ba44723
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 735 additions and 957 deletions

View File

@ -15,7 +15,7 @@ module.exports = {
"directory-item-value",
"directory-table-header-title",
"loading-spinner",
"directory-item-label",
"mobile-directory-item-label",
],
},
"no-implicit-this": {

View File

@ -31,21 +31,6 @@ export default Controller.extend(CanCheckEmails, {
return I18n.t("admin.users.titles." + query);
},
@discourseComputed("showEmails")
columnCount(showEmails) {
let colCount = 7; // note that the first column is hardcoded in the template
if (showEmails) {
colCount += 1;
}
if (this.siteSettings.must_approve_users) {
colCount += 1;
}
return colCount;
},
@observes("listFilter")
_filterUsers() {
discourseDebounce(this, this.resetFilters, INPUT_DELAY);

View File

@ -26,29 +26,17 @@
@title={{this.searchHint}}
/>
</div>
<LoadMore
@class="users-list-container"
@selector=".users-list tr"
@action={{action "loadMore"}}
>
{{#if this.model}}
<ResponsiveTable
@className="users-list"
@aria-label={{this.title}}
@style={{html-safe
(concat
"grid-template-columns: minmax(min-content, 2fr) repeat("
(html-safe this.columnCount)
", minmax(min-content, 1fr))"
)
}}
@updates={{this.model.email}}
>
<:header>
<table class="table users-list grid" role="table" aria-label={{this.title}}>
<thead>
<tr>
<TableHeaderToggle
@class="directory-table__column-header--username"
@field="username"
@labelKey="username"
@order={{this.order}}
@ -56,11 +44,7 @@
@automatic={{true}}
/>
<TableHeaderToggle
@class={{if
this.showEmails
"directory-table__column-header--email"
"hidden"
}}
@class={{if this.showEmails "" "hidden"}}
@field="email"
@labelKey="email"
@order={{this.order}}
@ -115,115 +99,67 @@
/>
{{#if this.siteSettings.must_approve_users}}
<div class="directory-table__column-header">{{i18n
"admin.users.approved"
}}</div>
<th>{{i18n "admin.users.approved"}}</th>
{{/if}}
<div class="directory-table__column-header">&nbsp;</div>
</:header>
<:body>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{{#each this.model as |user|}}
<div
<tr
class="user
{{user.selected}}
{{unless user.active 'not-activated'}}
directory-table__row"
>
<div class="directory-table__cell username">
<a
class="avatar"
href={{user.path}}
data-user-card={{user.username}}
{{unless user.active 'not-activated'}}"
>
<td class="username">
<a href={{user.path}} data-user-card={{user.username}}>
{{avatar user imageSize="small"}}
</a>
<LinkTo @route="adminUser" @model={{user}}>
{{user.username}}
</LinkTo>
<LinkTo
@route="adminUser"
@model={{user}}
>{{user.username}}</LinkTo>
{{#if user.staged}}
{{d-icon "far-envelope" title="user.staged"}}
{{/if}}
</div>
<div
class="directory-table__cell email
{{if this.showEmails '' 'hidden'}}"
>
<span class="directory-table__value">
</td>
<td class="email {{if this.showEmails '' 'hidden'}}">
{{~user.email~}}
</span>
</div>
</td>
{{#if user.last_emailed_at}}
<div
class="directory-table__cell last-emailed"
title={{raw-date user.last_emailed_at}}
>
<span class="directory-table__label">
<span>{{i18n "admin.users.last_emailed"}}</span>
</span>
<span class="directory-table__value">
{{format-duration user.last_emailed_age}}
</span>
</div>
<td class="last-emailed" title={{raw-date user.last_emailed_at}}>
<div class="label">{{i18n "admin.users.last_emailed"}}</div>
<div>{{format-duration user.last_emailed_age}}</div>
</td>
{{else}}
<div class="directory-table__cell last-emailed">
<span class="directory-table__label">
<span>{{i18n "admin.users.last_emailed"}}</span>
</span>
<span class="directory-table__value">
{{format-duration user.last_emailed_age}}
</span>
</div>
<td class="last-emailed">
<div class="label">{{i18n "admin.users.last_emailed"}}</div>
<div>{{format-duration user.last_emailed_age}}</div>
</td>
{{/if}}
<div
class="directory-table__cell last-seen"
title={{raw-date user.last_seen_at}}
>
<span class="directory-table__label">
<span>{{i18n "last_seen"}}</span>
</span>
<span class="directory-table__value">
{{format-duration user.last_seen_age}}
</span>
</div>
<div class="directory-table__cell topics-entered">
<span class="directory-table__label">
<span>{{i18n "admin.user.topics_entered"}}</span>
</span>
<span class="directory-table__value">
{{number user.topics_entered}}
</span>
</div>
<div class="directory-table__cell posts-read">
<span class="directory-table__label">
<span>{{i18n "admin.user.posts_read_count"}}</span>
</span>
<span class="directory-table__value">
{{number user.posts_read_count}}
</span>
</div>
<div class="directory-table__cell time-read">
<span class="directory-table__label">
<span>{{i18n "admin.user.time_read"}}</span>
</span>
<span class="directory-table__value">
{{format-duration user.time_read}}
</span>
</div>
<div
class="directory-table__cell created"
title={{raw-date user.created_at}}
>
<span class="directory-table__label">
<span>{{i18n "created"}}</span>
</span>
<span class="directory-table__value">
{{format-duration user.created_at_age}}
</span>
</div>
<td class="last-seen" title={{raw-date user.last_seen_at}}>
<div class="label">{{i18n "last_seen"}}</div>
<div>{{format-duration user.last_seen_age}}</div>
</td>
<td class="topics-entered">
<div class="label">{{i18n "admin.user.topics_entered"}}</div>
<div>{{number user.topics_entered}}</div>
</td>
<td class="posts-read">
<div class="label">{{i18n "admin.user.posts_read_count"}}</div>
<div>{{number user.posts_read_count}}</div>
</td>
<td class="time-read">
<div class="label">{{i18n "admin.user.time_read"}}</div>
<div>{{format-duration user.time_read}}</div>
</td>
<td class="created" title={{raw-date user.created_at}}>
<div class="label">{{i18n "created"}}</div>
<div>{{format-duration user.created_at_age}}</div>
</td>
<PluginOutlet
@name="admin-users-list-td-after"
@ -231,21 +167,10 @@
/>
{{#if this.siteSettings.must_approve_users}}
<div class="directory-table__cell">
<span class="directory-table__label">
<span>{{i18n "admin.users.approved"}}</span>
</span>
<span class="directory-table__value">
{{i18n-yes-no user.approved}}
</span>
</div>
<td>{{i18n-yes-no user.approved}}</td>
{{/if}}
<div class="directory-table__cell user-status">
<span class="directory-table__label">
<span>{{i18n "admin.users.status"}}</span>
</span>
<span class="directory-table__value">
<td class="user-status">
{{#if user.admin}}
{{d-icon "shield-alt" title="admin.title"}}
{{/if}}
@ -255,19 +180,16 @@
{{#if user.second_factor_enabled}}
{{d-icon "lock" title="admin.user.second_factor_enabled"}}
{{/if}}
</span>
<PluginOutlet
@name="admin-users-list-icon"
@connectorTagName="div"
@outletArgs={{hash user=user query=this.query}}
/>
</div>
</div>
</td>
</tr>
{{/each}}
</:body>
</ResponsiveTable>
</tbody>
</table>
<ConditionalLoadingSpinner @condition={{this.refreshing}} />
{{else}}
<p>{{i18n "search.no_results"}}</p>

View File

@ -1,38 +1,16 @@
<div class="directory-table__cell">
<UserInfo @user={{this.item.user}} />
</div>
<td><UserInfo @user={{this.item.user}} /></td>
{{#each this.columns as |column|}}
<td>
{{#if (directory-column-is-user-field column=column)}}
<div class="directory-table__cell--user-field">
<span class="directory-table__label">
<span>{{column.name}}</span>
</span>
{{directory-item-user-field-value item=this.item column=column}}
</div>
{{else}}
<div class="directory-table__cell">
<span class="directory-table__label">
<span>
{{#if column.icon}}
{{d-icon column.icon}}
{{/if}}
{{directory-item-label item=this.item column=column}}
</span>
</span>
{{directory-item-value item=this.item column=column}}
</div>
{{/if}}
</td>
{{/each}}
{{#if this.showTimeRead}}
<div class="directory-table__cell time-read">
<span class="directory-table__label">
<span>{{i18n "directory.time_read"}}</span>
</span>
<span class="directory-table__value">
{{format-duration this.item.time_read}}
</span>
</div>
<td><span class="time-read">{{format-duration
this.item.time_read
}}</span></td>
{{/if}}

View File

@ -2,8 +2,7 @@ import Component from "@ember/component";
import { propertyEqual } from "discourse/lib/computed";
export default Component.extend({
tagName: "div",
classNames: ["directory-table__row"],
tagName: "tr",
classNameBindings: ["me"],
me: propertyEqual("item.user.id", "currentUser.id"),
columns: null,

View File

@ -1,5 +1,10 @@
<ResponsiveTable>
<:header>
<div class="directory-table-top-scroll">
<div class="directory-table-top-scroll-fake-content"></div>
</div>
<div class="directory-table-container">
<table class="directory-table">
<thead>
<TableHeaderToggle
@field="username"
@order={{this.order}}
@ -18,14 +23,10 @@
{{/each}}
{{#if this.showTimeRead}}
<div class="directory-table__column-header">
<div class="header-contents">
{{i18n "directory.time_read"}}
</div>
</div>
<th>{{i18n "directory.time_read"}}</th>
{{/if}}
</:header>
<:body>
</thead>
<tbody>
{{#each this.items as |item|}}
<DirectoryItem
@item={{item}}
@ -33,5 +34,6 @@
@showTimeRead={{this.showTimeRead}}
/>
{{/each}}
</:body>
</ResponsiveTable>
</tbody>
</table>
</div>

View File

@ -2,31 +2,109 @@ import Component from "@ember/component";
import { action } from "@ember/object";
export default Component.extend({
lastScrollPosition: 0,
ticking: false,
_topHorizontalScrollBar: null,
_tableContainer: null,
_table: null,
_fakeScrollContent: null,
didInsertElement() {
this._super(...arguments);
this.setProperties({
_tableContainer: this.element.querySelector(".directory-table-container"),
_topHorizontalScrollBar: this.element.querySelector(
".directory-table-top-scroll"
),
_fakeScrollContent: this.element.querySelector(
".directory-table-top-scroll-fake-content"
),
_table: this.element.querySelector(".directory-table"),
_columnCount: this.showTimeRead
? this.attrs.columns.value.length + 1
: this.attrs.columns.value.length,
});
this._table.style.gridTemplateColumns = `minmax(13em, 3fr) repeat(${this._columnCount}, minmax(max-content, 1fr))`;
this._tableContainer.addEventListener("scroll", this.onBottomScroll);
this._topHorizontalScrollBar.addEventListener("scroll", this.onTopScroll);
// Set active header might have already scrolled the _tableContainer.
// Call onHorizontalScroll manually to scroll the _topHorizontalScrollBar
this.onResize();
this.onHorizontalScroll(this._tableContainer, this._topHorizontalScrollBar);
window.addEventListener("resize", this.onResize);
},
@action
onResize() {
if (
this._tableContainer.getBoundingClientRect().bottom < window.innerHeight
) {
// Bottom of the table is visible. Hide the scrollbar
this._fakeScrollContent.style.height = 0;
} else {
this._fakeScrollContent.style.width = `${this._table.offsetWidth}px`;
this._fakeScrollContent.style.height = "1px";
}
},
@action
onTopScroll() {
this.onHorizontalScroll(this._topHorizontalScrollBar, this._tableContainer);
},
@action
onBottomScroll() {
this.onHorizontalScroll(this._tableContainer, this._topHorizontalScrollBar);
},
@action
onHorizontalScroll(primary, replica) {
if (
this.isDestroying ||
this.isDestroyed ||
this.lastScrollPosition === primary.scrollLeft
) {
return;
}
this.set("lastScrollPosition", primary.scrollLeft);
if (!this.ticking) {
window.requestAnimationFrame(() => {
if (!this.isDestroying && !this.isDestroyed) {
replica.scrollLeft = this.lastScrollPosition;
this.set("ticking", false);
}
});
this.set("ticking", true);
}
},
willDestroyElement() {
this._tableContainer.removeEventListener("scroll", this.onBottomScroll);
this._topHorizontalScrollBar.removeEventListener(
"scroll",
this.onTopScroll
);
window.removeEventListener("resize", this.onResize);
},
@action
setActiveHeader(header) {
// After render, scroll table left to ensure the order by column is visible
if (!this._table) {
this.set("_table", document.querySelector(".directory-table"));
if (!this._tableContainer) {
this.set(
"_tableContainer",
document.querySelector(".directory-table-container")
);
}
const scrollPixels =
header.offsetLeft + header.offsetWidth + 10 - this._table.offsetWidth;
header.offsetLeft +
header.offsetWidth +
10 -
this._tableContainer.offsetWidth;
if (scrollPixels > 0) {
this._table.scrollLeft = scrollPixels;
this._tableContainer.scrollLeft = scrollPixels;
}
},
});

View File

@ -1,20 +0,0 @@
<div class="directory-table-top-scroll" {{on "scroll" this.onTopScroll}}>
<div class="directory-table-top-scroll-fake-content"></div>
</div>
<div
class={{concat-class "directory-table" @className}}
role="table"
aria-label={{@ariaLabel}}
style={{@style}}
{{did-insert this.checkScroll}}
{{did-update this.checkScroll @updates}}
{{on-resize this.checkScroll}}
{{on "scroll" this.onBottomScroll}}
>
<div class="directory-table__header">
{{yield to="header"}}
</div>
<div class="directory-table__body">
{{yield to="body"}}
</div>
</div>

View File

@ -1,51 +0,0 @@
import Component from "@ember/component";
import { bind } from "discourse-common/utils/decorators";
import { tracked } from "@glimmer/tracking";
export default class ResponsiveTable extends Component {
@tracked lastScrollPosition = 0;
@tracked ticking = false;
@tracked _table = document.querySelector(".directory-table");
@tracked _topHorizontalScrollBar = document.querySelector(
".directory-table-top-scroll"
);
@bind
checkScroll() {
const _fakeScrollContent = document.querySelector(
".directory-table-top-scroll-fake-content"
);
if (this._table.getBoundingClientRect().bottom < window.innerHeight) {
// Bottom of the table is visible. Hide the scrollbar
_fakeScrollContent.style.height = 0;
} else {
_fakeScrollContent.style.width = `${this._table.scrollWidth}px`;
_fakeScrollContent.style.height = "1px";
}
}
@bind
onTopScroll() {
this.onHorizontalScroll(this._topHorizontalScrollBar, this._table);
}
@bind
onBottomScroll() {
this.onHorizontalScroll(this._table, this._topHorizontalScrollBar);
}
@bind
onHorizontalScroll(primary, replica) {
this.set("lastScrollPosition", primary?.scrollLeft);
if (!this.ticking) {
window.requestAnimationFrame(() => {
replica.scrollLeft = this.lastScrollPosition;
this.set("ticking", false);
});
this.set("ticking", true);
}
}
}

View File

@ -1,4 +1,4 @@
<div
<span
class="header-contents"
id={{this.id}}
role="button"
@ -6,9 +6,6 @@
aria-label={{this.ariaLabel}}
aria-pressed={{this.pressedState}}
>
{{yield}}
<span class="text">
{{directory-table-header-title
field=this.field
labelKey=this.labelKey
@ -17,4 +14,3 @@
}}
{{this.chevronIcon}}
</span>
</div>

View File

@ -6,8 +6,8 @@ import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
export default Component.extend({
tagName: "div",
classNames: ["directory-table__column-header", "sortable"],
tagName: "th",
classNames: ["sortable"],
attributeBindings: ["title", "colspan", "ariaSort:aria-sort", "role"],
role: "columnheader",
labelKey: null,

View File

@ -3,7 +3,7 @@ import { number } from "discourse/lib/formatter";
import { registerUnbound } from "discourse-common/lib/helpers";
import I18n from "I18n";
registerUnbound("directory-item-label", function (args) {
registerUnbound("mobile-directory-item-label", function (args) {
// Args should include key/values { item, column }
const count = args.item.get(args.column.name);
const translationPrefix =
@ -14,9 +14,7 @@ registerUnbound("directory-item-label", function (args) {
registerUnbound("directory-item-value", function (args) {
// Args should include key/values { item, column }
return htmlSafe(
`<span class='directory-table__value'>${number(
args.item.get(args.column.name)
)}</span>`
`<span class='number'>${number(args.item.get(args.column.name))}</span>`
);
});
@ -27,9 +25,7 @@ registerUnbound("directory-item-user-field-value", function (args) {
? args.item.user.user_fields[args.column.user_field_id]
: null;
const content = value || "-";
return htmlSafe(
`<span class='directory-table__value--user-field'>${content}</span>`
);
return htmlSafe(`<span class='user-field-value'>${content}</span>`);
});
registerUnbound("directory-column-is-automatic", function (args) {

View File

@ -1,15 +1,5 @@
<section class="user-content">
<div class="group-members-actions">
{{#if this.canManageGroup}}
<DButton
@class="bulk-select"
@icon="list"
@action={{action "toggleBulkSelect"}}
@title="topics.bulk.toggle"
/>
{{/if}}
{{#if this.model.can_see_members}}
<TextField
@value={{this.filterInput}}
@ -20,35 +10,6 @@
{{/if}}
{{#if this.canManageGroup}}
{{#if this.isBulk}}
<span class="bulk-select-buttons-wrap">
{{#if this.bulkSelection}}
<BulkGroupMemberDropdown
@bulkSelection={{this.bulkSelection}}
@canAdminGroup={{this.model.can_admin_group}}
@canEditGroup={{this.model.can_edit_group}}
@onChange={{action "actOnSelection" this.bulkSelection}}
/>
<DButton
@action={{action "bulkClearAll"}}
@label="topics.bulk.clear_all"
@icon="far-square"
@class="bulk-select-clear"
/>
{{/if}}
<DButton
@action={{action "bulkSelectAll"}}
@label="topics.bulk.select_all"
@icon="check-square"
@class="bulk-select-all"
/>
</span>
{{/if}}
<div class="group-members-manage">
<DButton
@icon="plus"
@ -70,17 +31,44 @@
</div>
{{#if this.hasMembers}}
<LoadMore
@selector=".group-members .directory-table-row"
@action={{action "loadMore"}}
<LoadMore @selector=".group-members tr" @action={{action "loadMore"}}>
<table
class={{if this.isBulk "group-members sticky-header" "group-members"}}
>
<ResponsiveTable
@className="group-members
{{if this.isBulk 'sticky-header' ''}}
{{if this.canManageGroup 'group-members--can-manage' ''}}"
>
<:header>
<thead>
<tr>
<th class="bulk-select">
{{#if this.canManageGroup}}
<FlatButton
@class="bulk-select"
@icon="list"
@action={{action "toggleBulkSelect"}}
@title="topics.bulk.toggle"
/>
{{/if}}
</th>
{{#if this.isBulk}}
<th class="bulk-select-buttons">
<span class="bulk-select-buttons-wrap">
{{#if this.bulkSelection}}
<BulkGroupMemberDropdown
@bulkSelection={{this.bulkSelection}}
@canAdminGroup={{this.model.can_admin_group}}
@canEditGroup={{this.model.can_edit_group}}
@onChange={{action "actOnSelection" this.bulkSelection}}
/>
{{/if}}
<DButton
@action={{action "bulkSelectAll"}}
@label="topics.bulk.select_all"
/>
<DButton
@action={{action "bulkClearAll"}}
@label="topics.bulk.clear_all"
/>
</span>
</th>
{{/if}}
<TableHeaderToggle
@order={{this.order}}
@asc={{this.asc}}
@ -90,13 +78,7 @@
@automatic={{true}}
@colspan="2"
/>
{{#if this.canManageGroup}}
<div class="directory-table__column-header"></div>
{{/if}}
<TableHeaderToggle
@class="directory-table__column-header"
@order={{this.order}}
@asc={{this.asc}}
@field="added_at"
@ -104,7 +86,6 @@
@automatic={{true}}
/>
<TableHeaderToggle
@class="directory-table__column-header"
@order={{this.order}}
@asc={{this.asc}}
@field="last_posted_at"
@ -112,49 +93,39 @@
@automatic={{true}}
/>
<TableHeaderToggle
@class="directory-table__column-header"
@order={{this.order}}
@asc={{this.asc}}
@field="last_seen_at"
@labelKey="last_seen"
@automatic={{true}}
/>
<th></th>
</tr>
</thead>
{{#if this.canManageGroup}}
<div class="directory-table__column-header"></div>
{{/if}}
</:header>
<:body>
<tbody>
{{#each this.model.members as |m|}}
<div class="directory-table__row">
<div class="directory-table__cell group-member" colspan="2">
{{#if this.canManageGroup}}
<tr>
{{#if this.isBulk}}
<td class="bulk-select">
<Input
@type="checkbox"
class="bulk-select"
{{on "click" (action "selectMember" m)}}
/>
</td>
{{/if}}
{{/if}}
<td class="group-member" colspan="2">
<UserInfo
@user={{m}}
@skipName={{this.skipName}}
@showStatus={{true}}
@showStatusTooltip={{true}}
/>
</div>
</td>
{{#if this.canManageGroup}}
<div class="directory-table__cell group-owner">
{{#if (or m.owner m.primary)}}
<span class="directory-table__label">
<span>{{i18n "groups.members.status"}}</span>
</span>
{{/if}}
<span class="directory-table__value">
<td class="group-owner">
{{#if m.owner}}
{{d-icon "shield-alt"}}
{{i18n "groups.members.owner"}}<br />
@ -162,55 +133,32 @@
{{#if m.primary}}
{{i18n "groups.members.primary"}}
{{/if}}
</span>
</td>
<td>
<span class="text">{{bound-date m.added_at}}</span>
</td>
<td>
<span class="text">{{bound-date m.last_posted_at}}</span>
</td>
<td>
<span class="text">{{bound-date m.last_seen_at}}</span>
</td>
</div>
{{/if}}
<div class="directory-table__cell">
<span class="directory-table__label">
<span>{{i18n "groups.member_added"}}</span>
</span>
<span class="directory-table__value">
{{bound-date m.added_at}}
</span>
</div>
<div class="directory-table__cell">
{{#if m.last_posted_at}}
<span class="directory-table__label">
<span>{{i18n "last_post"}}</span>
</span>
{{/if}}
<span class="directory-table__value">
{{bound-date m.last_posted_at}}
</span>
</div>
<div class="directory-table__cell">
{{#if m.last_seen_at}}
<span class="directory-table__label">
<span>{{i18n "last_seen"}}</span>
</span>
{{/if}}
<span class="directory-table__value">
{{bound-date m.last_seen_at}}
</span>
</div>
<td>
{{#if this.canManageGroup}}
<div class="directory-table__cell member-settings">
<GroupMemberDropdown
@member={{m}}
@canAdminGroup={{this.model.can_admin_group}}
@canEditGroup={{this.model.can_edit_group}}
@onChange={{action "actOnGroup" m}}
/>
{{! group parameter is used by plugins }}
</div>
{{/if}}
</div>
{{! group parameter is used by plugins }}
</td>
</tr>
{{/each}}
</:body>
</ResponsiveTable>
</tbody>
</table>
</LoadMore>
<ConditionalLoadingSpinner @condition={{this.loading}} />

View File

@ -9,14 +9,10 @@
</div>
{{#if this.hasRequesters}}
<LoadMore
@selector=".group-members .directory-table__row"
@action={{action "loadMore"}}
>
<ResponsiveTable @className="group-members group-members__requests">
<:header>
<LoadMore @selector=".group-members tr" @action={{action "loadMore"}}>
<table class="group-members">
<thead>
<TableHeaderToggle
@class="username"
@order={{this.order}}
@asc={{this.asc}}
@field="username_lower"
@ -30,34 +26,22 @@
@labelKey="groups.member_requested"
@automatic={{true}}
/>
<div class="directory-table__column-header">{{i18n
"groups.requests.reason"
}}</div>
<div class="directory-table__column-header"></div>
</:header>
<:body>
<th>{{i18n "groups.requests.reason"}}</th>
<th></th>
<th></th>
</thead>
<tbody>
{{#each this.model.requesters as |m|}}
<div class="directory-table__row">
<div class="directory-table__cell group-member">
<tr>
<td class="group-member">
<UserInfo @user={{m}} @skipName={{this.skipName}} />
</div>
<div class="directory-table__cell">
<span class="directory-table__label">
<span>{{i18n "groups.member_requested"}}</span>
</span>
<span class="directory-table__value">
<span>{{bound-date m.requested_at}}</span>
</span>
</div>
<div class="directory-table__cell">
<span class="directory-table__label">
<span>{{i18n "groups.requests.reason"}}</span>
</span>
<span class="directory-table__value">
{{m.reason}}
</span>
</div>
<div class="directory-table__cell group-accept-deny-buttons">
</td>
<td>
<span class="text">{{bound-date m.requested_at}}</span>
</td>
<td>{{m.reason}}</td>
<td>
{{#if m.request_undone}}
{{i18n "groups.requests.undone"}}
{{else if m.request_accepted}}
@ -83,14 +67,17 @@
@class="btn-danger"
/>
{{/if}}
</div>
</div>
</td>
<td></td>
</tr>
{{/each}}
</:body>
</ResponsiveTable>
</tbody>
</table>
</LoadMore>
<ConditionalLoadingSpinner @condition={{this.loading}} />
{{else}}
<div>{{i18n "groups.empty.requests"}}</div>
{{/if}}
</section>

View File

@ -0,0 +1,37 @@
<UserInfo @user={{this.item.user}} />
{{#each this.columns as |column|}}
{{#if (directory-column-is-user-field column=column)}}
{{#if (get this.item.user.user_fields column.user_field_id)}}
<div class="user-stat">
<span class="value user-field">
{{directory-item-user-field-value item=this.item column=column}}
</span>
<span class="label">
{{column.name}}
</span>
</div>
{{/if}}
{{else}}
<div class="user-stat">
<span class="value">
{{directory-item-value item=this.item column=column}}
</span>
<span class="label">
{{#if column.icon}}
{{d-icon column.icon}}
{{/if}}
{{mobile-directory-item-label item=this.item column=column}}
</span>
</div>
{{/if}}
{{/each}}
{{#if this.showTimeRead}}
<UserStat
@value={{this.item.time_read}}
@label="directory.time_read"
@type="duration"
/>
{{/if}}

View File

@ -0,0 +1,80 @@
<LoadMore @selector=".directory .user" @action={{action "loadMore"}}>
<div class="container">
<div class="users-directory directory">
<span>
<PluginOutlet
@name="users-top"
@connectorTagName="div"
@outletArgs={{hash model=this.model}}
/>
</span>
<div class="directory-controls">
<PeriodChooser
@period={{this.period}}
@onChange={{action (mut this.period)}}
@fullDay={{false}}
/>
{{#if this.lastUpdatedAt}}
<div class="directory-last-updated">
{{i18n "directory.last_updated"}}
{{this.lastUpdatedAt}}
</div>
{{/if}}
<div class="inline-form full-width">
<Input
@value={{readonly this.nameInput}}
placeholder={{i18n "directory.filter_name"}}
class="filter-name no-blur"
{{on
"input"
(action "onUsernameFilterChanged" value="target.value")
}}
/>
<ComboBox
@class="directory-group-selector"
@value={{this.group}}
@content={{this.groupOptions}}
@onChange={{action this.groupChanged}}
@options={{hash none="directory.group.all"}}
/>
{{#if this.currentUser.staff}}
<DButton
@icon="wrench"
@action={{action "showEditColumnsModal"}}
@class="btn-default open-edit-columns-btn"
/>
{{/if}}
</div>
<PluginOutlet
@name="users-directory-controls"
@outletArgs={{hash model=this.model}}
/>
</div>
<ConditionalLoadingSpinner @condition={{this.isLoading}}>
{{#if this.model.length}}
<div class="total-rows">{{i18n
"directory.total_rows"
count=this.model.totalRows
}}</div>
{{#each this.model as |item|}}
<DirectoryItem
@tagName="div"
@class="user"
@item={{item}}
@columns={{this.columns}}
@showTimeRead={{this.showTimeRead}}
/>
{{/each}}
<ConditionalLoadingSpinner @condition={{this.model.loadingMore}} />
{{else}}
<div class="clearfix"></div>
<p>{{i18n "directory.no_results"}}</p>
{{/if}}
</ConditionalLoadingSpinner>
</div>
</div>
</LoadMore>

View File

@ -68,8 +68,7 @@ acceptance("Admin - Users List", function (needs) {
await click(".hide-emails");
assert.strictEqual(
query(".users-list .user:nth-child(1) .email .directory-table__value")
.innerText,
query(".users-list .user:nth-child(1) .email").innerText,
"",
"hides the emails"
);

View File

@ -20,7 +20,7 @@ acceptance("Group Members - Anonymous", function () {
1,
"it displays the group's avatar flair"
);
assert.ok(exists(".group-members .group-member"), "it lists group members");
assert.ok(exists(".group-members tr"), "it lists group members");
assert.ok(
!exists(".group-member-dropdown"),
@ -137,7 +137,7 @@ acceptance("Group Members", function (needs) {
);
await click("button.bulk-select");
await click(".bulk-select-all");
await click(".bulk-select-buttons button:nth-child(1)");
assert.ok(
exists(".bulk-select-buttons-wrap details"),

View File

@ -12,7 +12,7 @@ acceptance("Managing Group Category Notification Defaults", function () {
await visit("/g/discourse/manage/categories");
assert.ok(
exists(".group-members .group-member"),
exists(".group-members tr"),
"it should redirect to members page for an anonymous user"
);
});

View File

@ -12,7 +12,7 @@ acceptance("Managing Group Profile", function () {
await visit("/g/discourse/manage/profile");
assert.ok(
exists(".group-members .group-member"),
exists(".group-members tr"),
"it should redirect to members page for an anonymous user"
);
});

View File

@ -12,7 +12,7 @@ acceptance("Managing Group Tag Notification Defaults", function () {
await visit("/g/discourse/manage/tags");
assert.ok(
exists(".group-members .group-member"),
exists(".group-members tr"),
"it should redirect to members page for an anonymous user"
);
});

View File

@ -89,49 +89,37 @@ acceptance("Group Requests", function (needs) {
test("Group Requests", async function (assert) {
await visit("/g/Macdonald/requests");
assert.strictEqual(count(".group-members .group-member"), 2);
assert.strictEqual(count(".group-members tr"), 2);
assert.strictEqual(
query(".group-members .directory-table__row:first-child .user-detail")
query(".group-members tr:first-child td:nth-child(1)")
.innerText.trim()
.replace(/\s+/g, " "),
"eviltrout Robin Ward"
);
assert.strictEqual(
query(
".group-members .directory-table__row:first-child .directory-table__cell:nth-child(3)"
).innerText.trim(),
query(".group-members tr:first-child td:nth-child(3)").innerText.trim(),
"Please accept my membership request."
);
assert.strictEqual(
query(
".group-members .directory-table__row:first-child .btn-primary"
).innerText.trim(),
query(".group-members tr:first-child .btn-primary").innerText.trim(),
"Accept"
);
assert.strictEqual(
query(
".group-members .directory-table__row:first-child .btn-danger"
).innerText.trim(),
query(".group-members tr:first-child .btn-danger").innerText.trim(),
"Deny"
);
await click(
".group-members .directory-table__row:first-child .btn-primary"
);
await click(".group-members tr:first-child .btn-primary");
assert.ok(
query(
".group-members .directory-table__row:first-child .directory-table__cell:nth-child(4)"
)
query(".group-members tr:first-child td:nth-child(4)")
.innerText.trim()
.startsWith("accepted")
);
assert.deepEqual(requests, [["19", "true"]]);
await click(".group-members .directory-table__row:last-child .btn-danger");
await click(".group-members tr:last-child .btn-danger");
assert.strictEqual(
query(
".group-members .directory-table__row:last-child .directory-table__cell:nth-child(4)"
).innerText.trim(),
query(".group-members tr:last-child td:nth-child(4)").innerText.trim(),
"denied"
);
assert.deepEqual(requests, [

View File

@ -7,9 +7,6 @@ acceptance("User Directory - Mobile", function (needs) {
test("Visit Page", async function (assert) {
await visit("/u");
assert.ok(
exists(".directory .directory-table__row"),
"has a list of users"
);
assert.ok(exists(".directory .user"), "has a list of users");
});
});

View File

@ -14,10 +14,7 @@ acceptance("User Directory", function () {
document.body.classList.contains("users-page"),
"has the body class"
);
assert.ok(
exists(".directory .directory-table .directory-table__row"),
"has a list of users"
);
assert.ok(exists(".directory table tr"), "has a list of users");
});
test("Visit All Time", async function (assert) {
@ -31,10 +28,7 @@ acceptance("User Directory", function () {
document.body.classList.contains("users-page"),
"has the body class"
);
assert.ok(
exists(".directory .directory-table .directory-table__row"),
"has a list of users"
);
assert.ok(exists(".directory table tr"), "has a list of users");
});
test("Visit With Group Filter", async function (assert) {
@ -43,27 +37,27 @@ acceptance("User Directory", function () {
document.body.classList.contains("users-page"),
"has the body class"
);
assert.ok(
exists(".directory .directory-table .directory-table__row"),
"has a list of users"
);
assert.ok(exists(".directory table tr"), "has a list of users");
});
test("Custom user fields are present", async function (assert) {
await visit("/u");
const firstRowUserField = query(
".directory .directory-table__body .directory-table__row:first-child .directory-table__value--user-field"
);
const firstRow = query(".users-directory table tr");
const columnData = firstRow.querySelectorAll("td");
const favoriteColorTd = columnData[columnData.length - 1];
assert.strictEqual(firstRowUserField.textContent, "Blue");
assert.strictEqual(
favoriteColorTd.querySelector("span").textContent,
"Blue"
);
});
test("Can sort table via keyboard", async function (assert) {
await visit("/u");
const secondHeading =
".users-directory .directory-table__header div:nth-child(2) .header-contents";
".users-directory table th:nth-child(2) .header-contents";
await triggerKeyEvent(secondHeading, "keypress", "Enter");

View File

@ -802,15 +802,19 @@ section.details {
}
}
.directory-table {
.not-activated {
.directory-table__cell {
&,
a,
a:visited {
tr.not-activated {
td,
td a,
td a:visited {
color: #bbb;
}
}
.details.not-activated {
.username .value,
.email .value a,
.email .value a:visited {
color: #bbb;
}
}

View File

@ -99,54 +99,48 @@
}
.admin-users-list {
.directory-table__cell {
&.username {
justify-content: start;
td.username {
@include ellipsis;
overflow-wrap: break-word;
}
&.email {
justify-content: start;
span {
display: flex;
min-width: 17em;
word-break: break-all;
@media screen and (max-width: 970px) and (min-width: 768px) {
td.username {
max-width: 23vw; // Prevents horizontal scroll down to 768px
}
td.email {
max-width: 28vw; // Prevents horizontal scroll down to 768px
overflow-wrap: break-word;
}
}
@media screen and (max-width: 767px) {
tr {
td.username {
grid-column-start: 1;
grid-column-end: -2;
font-weight: bold;
}
.directory-table {
margin-top: 1em;
&__column-header--username,
&__column-header--email {
.header-contents {
text-align: left;
td.user-status {
text-align: right;
grid-row: 1;
grid-column-end: -1;
.d-icon {
margin-left: 0.25em;
}
}
&__cell.username {
align-items: center;
}
&__cell.email {
@include breakpoint("tablet") {
td.email {
grid-column-start: 1;
grid-column-end: -1;
span {
max-width: 100%;
}
}
}
}
word-wrap: break-word;
overflow-wrap: break-word;
overflow: hidden;
min-width: 0;
margin: 0.5em 0 0 0;
.directory-table__cell {
padding: 0.5em 0.25em;
&:empty {
display: none;
}
}
.user-status span {
gap: 0.15em;
}
.avatar {
margin-right: 0.25em;
}
}

View File

@ -1,10 +1,15 @@
.directory-table-top-scroll {
.directory {
margin-bottom: 100px;
.directory-table-container {
width: 100%;
overflow-x: auto;
}
.directory {
margin-bottom: 100px;
.directory-table-top-scroll {
width: 100%;
overflow-x: auto;
}
&.users-directory {
.directory-group-selector {
@ -34,6 +39,58 @@
color: var(--primary-medium);
font-size: var(--font-down-1);
}
table {
width: 100%;
margin-bottom: 1em;
td,
th {
padding: 0.5em;
text-align: left;
border-bottom: 1px solid var(--primary-low);
@media screen and (max-width: $small-width) {
padding: 0.5em 0.25em;
}
.number,
.time-read {
font-size: var(--font-up-3);
color: var(--primary-medium);
@media screen and (max-width: $small-width) {
font-size: var(--font-up-1);
}
}
.time-read {
white-space: nowrap;
}
.user-field-value {
font-size: var(--font-up-1);
color: var(--primary-medium);
@media screen and (max-width: $small-width) {
font-size: var(--font-0);
}
}
}
th.sortable {
width: 13%;
.d-icon-heart {
color: var(--love);
margin: 0 0.25em 0 0;
}
}
}
.me {
background-color: var(--highlight-bg);
.username a,
.name a,
.title,
.number,
.time-read {
color: var(--primary-medium);
}
}
}
.edit-user-directory-columns-modal {
@ -82,221 +139,3 @@
.edit-user-directory-columns-modal .modal-inner-container {
min-width: 450px;
}
@container (min-width: 47em) {
.users-directory {
.directory-table {
&__value {
white-space: nowrap;
font-size: var(--font-up-2);
&,
&--user-field {
color: var(--primary-medium);
}
}
}
}
}
.directory-table {
display: grid;
gap: 0;
width: 100%;
margin-top: 1em;
overflow-x: auto;
.me {
.directory-table__cell {
&,
&--user-field {
background-color: var(--highlight-low-or-medium);
}
}
}
&__header,
&__body,
&__row {
display: contents; // we'll be able to remove this with subgrid support
}
&__column-header,
&__cell,
&__cell--user-field {
display: flex;
border-bottom: 1px solid var(--primary-low);
justify-content: center;
align-items: center;
}
&__column-header {
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
color: var(--primary-medium);
padding: 0.5em;
.d-icon {
margin-right: 0.25em;
}
&:first-child {
.header-contents {
text-align: left;
}
}
}
&__cell {
&,
&--user-field {
padding: 0.75em 0.5em;
}
}
&__value {
white-space: nowrap;
&--user-field {
max-width: 30em;
}
}
&__label {
display: none;
}
.d-icon-heart {
font-size: var(--font-down-1);
color: var(--love);
}
.user-detail {
display: flex;
flex-direction: column;
min-width: 0; // allow content to shrink and hide overflow
}
.user-info {
display: flex;
min-width: 0;
margin: 0;
width: 100%;
.user-image {
padding-right: 0.5em;
margin-right: 0.5em;
}
.user-detail {
padding: 0;
width: 100%;
@media screen and (max-width: 600px) {
// overrides existing media query
font-size: var(--font-0);
}
@include breakpoint("mobile-medium") {
font-size: var(--font-down-1);
}
}
.title {
margin: 0;
}
}
.header-contents {
width: 100%;
text-align: center;
}
}
// using a container query to switch to a flex-based layout
// browsers without support for container queries
// fallback to big horizontal scrolling table
@container (max-width: 47em) {
.directory-table {
display: flex;
flex-direction: column;
.me {
background-color: var(--highlight-low-or-medium);
}
&__label {
display: inline-flex;
color: var(--primary-medium);
padding-right: 0.5em;
align-items: baseline;
align-self: start;
white-space: nowrap;
overflow: hidden;
span {
// caution: display flex here can interfere with overflow hiding
flex: 0 1 auto; // can shrink if needed
margin-right: 0.25em;
@include ellipsis;
}
// flexible divider between the label and value
&:after {
flex: 1 1 0; // can grow or shrink, but should be 0 width if needed
color: var(--primary-300);
min-width: 0;
overflow: hidden;
// this needs to be long to account for all possible widths
content: "...................................................................................................................................";
}
.d-icon {
font-size: 0.8em;
vertical-align: baseline;
}
}
&__value {
font-size: var(--font-0);
color: var(--primary);
}
&__row {
&:first-child {
border-top: 1px solid var(--primary-low);
}
display: grid;
grid-template-columns: repeat(auto-fill, minmax(11em, 1fr));
border-bottom: 1px solid var(--primary-low);
padding: 0.85em 0.75em 1em;
gap: 0 15%;
}
&__header {
display: none;
}
&__cell {
&,
&--user-field {
padding: 0.25em;
border: none;
&:first-child {
width: 100%;
padding: 0.5em 0.25em 1em;
justify-content: start;
// force full width of the cell
grid-column-start: 1;
grid-column-end: -1;
}
}
&--user-field {
order: 2;
// force full width of the cell
// because we don't know how much content there is
grid-column-start: 1;
grid-column-end: -1;
.directory-table__label {
margin-right: 0.25em;
}
}
}
}
}

View File

@ -596,8 +596,6 @@ table {
#main-outlet {
grid-area: content;
container-type: inline-size;
container-name: main-outlet;
}
}

View File

@ -25,27 +25,15 @@
display: flex;
flex-wrap: wrap;
width: 100%;
gap: 0.5em 0;
.bulk-select + input {
margin-left: 0.5em;
input + .group-members-manage {
margin-left: auto;
}
input {
margin: 0 auto 0 0;
.group-username-filter {
margin: 0 0 5px 0;
vertical-align: middle;
}
.bulk-select-buttons-wrap {
margin-right: 0.5em;
display: flex;
flex-wrap: wrap;
gap: 0.5em;
}
}
.group-members-manage {
display: flex;
gap: 0.5em;
}
.group-info {
@ -130,45 +118,58 @@ table.group-manage-logs {
}
}
.group-members {
grid-template-columns: 3fr repeat(3, minmax(min-content, 1fr));
table.group-members {
width: 100%;
&--can-manage {
grid-template-columns: 3fr repeat(4, minmax(min-content, 1fr)) 3em;
@container (max-width: 47em) {
// positioning the member settings button within the same cell
// and avoiding overlap with padding-right on user-info
.group-member,
.member-settings {
grid-row-start: 1;
grid-column-start: 1;
grid-column-end: -1;
th {
text-align: center;
&.bulk-select {
height: 30px;
width: 30px;
}
.member-settings {
margin-left: auto;
&.bulk-select-buttons {
text-align: left;
white-space: nowrap;
width: 1%;
.bulk-select-buttons-wrap {
display: flex;
}
.btn {
margin-right: 0.25em;
}
}
&.username {
text-align: left;
}
}
td {
color: var(--primary-medium);
padding: 0.8em 0;
text-align: center;
&.group-member {
text-align: left;
}
}
.user-info {
padding-right: 3.5em;
}
}
}
display: block;
&.group-members__requests {
grid-template-columns: 3fr repeat(3, minmax(min-content, 1fr));
}
.directory-table__value {
font-size: var(--font-0);
.avatar-flair {
color: var(--primary);
}
.group-accept-deny-buttons {
gap: 0.5em;
.user-status-message {
img.emoji {
width: 1em;
height: 1em;
}
@container (max-width: 47em) {
.directory-table__cell.group-owner {
order: 2;
}
}
}

View File

@ -21,6 +21,35 @@
color: var(--primary-medium);
padding: 5px;
}
.user {
border-top: 1px solid var(--primary-low);
padding: 1em;
display: flex;
flex-wrap: wrap;
.user-info {
width: 100%;
margin-bottom: 1em;
}
.user-stat {
flex: 1 1 50%;
.value {
font-weight: bold;
&.user-field {
font-size: var(--font-down-1);
}
}
.label {
margin-left: 0.2em;
color: var(--primary-medium);
}
.d-icon-heart {
color: var(--love);
}
}
}
}
.edit-user-directory-columns-modal .modal-inner-container {

View File

@ -923,7 +923,6 @@ en:
make_all_primary_description: "Make this the primary group for all selected users"
remove_all_primary: "Remove as Primary"
remove_all_primary_description: "Remove this group as primary"
status: "Status"
owner: "Owner"
primary: "Primary"
forbidden: "You're not allowed to view the members."
@ -5630,7 +5629,6 @@ en:
not_found: "Sorry, that username doesn't exist in our system."
id_not_found: "Sorry, that user id doesn't exist in our system."
active: "Activated"
status: "Status"
show_emails: "Show Emails"
hide_emails: "Hide Emails"
nav: