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-item-value",
"directory-table-header-title", "directory-table-header-title",
"loading-spinner", "loading-spinner",
"directory-item-label", "mobile-directory-item-label",
], ],
}, },
"no-implicit-this": { "no-implicit-this": {

View File

@ -31,21 +31,6 @@ export default Controller.extend(CanCheckEmails, {
return I18n.t("admin.users.titles." + query); 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") @observes("listFilter")
_filterUsers() { _filterUsers() {
discourseDebounce(this, this.resetFilters, INPUT_DELAY); discourseDebounce(this, this.resetFilters, INPUT_DELAY);

View File

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

View File

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

View File

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

View File

@ -1,37 +1,39 @@
<ResponsiveTable> <div class="directory-table-top-scroll">
<:header> <div class="directory-table-top-scroll-fake-content"></div>
<TableHeaderToggle </div>
@field="username"
@order={{this.order}} <div class="directory-table-container">
@asc={{this.asc}} <table class="directory-table">
/> <thead>
{{#each this.columns as |column|}}
<TableHeaderToggle <TableHeaderToggle
@field={{column.name}} @field="username"
@icon={{column.icon}}
@order={{this.order}} @order={{this.order}}
@asc={{this.asc}} @asc={{this.asc}}
@automatic={{directory-column-is-automatic column=column}}
@translated={{column.user_field_id}}
@onActiveRender={{this.setActiveHeader}}
/> />
{{/each}} {{#each this.columns as |column|}}
<TableHeaderToggle
@field={{column.name}}
@icon={{column.icon}}
@order={{this.order}}
@asc={{this.asc}}
@automatic={{directory-column-is-automatic column=column}}
@translated={{column.user_field_id}}
@onActiveRender={{this.setActiveHeader}}
/>
{{/each}}
{{#if this.showTimeRead}} {{#if this.showTimeRead}}
<div class="directory-table__column-header"> <th>{{i18n "directory.time_read"}}</th>
<div class="header-contents"> {{/if}}
{{i18n "directory.time_read"}} </thead>
</div> <tbody>
</div> {{#each this.items as |item|}}
{{/if}} <DirectoryItem
</:header> @item={{item}}
<:body> @columns={{this.columns}}
{{#each this.items as |item|}} @showTimeRead={{this.showTimeRead}}
<DirectoryItem />
@item={{item}} {{/each}}
@columns={{this.columns}} </tbody>
@showTimeRead={{this.showTimeRead}} </table>
/> </div>
{{/each}}
</:body>
</ResponsiveTable>

View File

@ -2,31 +2,109 @@ import Component from "@ember/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
export default Component.extend({ export default Component.extend({
lastScrollPosition: 0,
ticking: false,
_topHorizontalScrollBar: null,
_tableContainer: null,
_table: null, _table: null,
_fakeScrollContent: null,
didInsertElement() { didInsertElement() {
this._super(...arguments); this._super(...arguments);
this.setProperties({ 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"), _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 @action
setActiveHeader(header) { setActiveHeader(header) {
// After render, scroll table left to ensure the order by column is visible // After render, scroll table left to ensure the order by column is visible
if (!this._table) { if (!this._tableContainer) {
this.set("_table", document.querySelector(".directory-table")); this.set(
"_tableContainer",
document.querySelector(".directory-table-container")
);
} }
const scrollPixels = const scrollPixels =
header.offsetLeft + header.offsetWidth + 10 - this._table.offsetWidth; header.offsetLeft +
header.offsetWidth +
10 -
this._tableContainer.offsetWidth;
if (scrollPixels > 0) { 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" class="header-contents"
id={{this.id}} id={{this.id}}
role="button" role="button"
@ -6,15 +6,11 @@
aria-label={{this.ariaLabel}} aria-label={{this.ariaLabel}}
aria-pressed={{this.pressedState}} aria-pressed={{this.pressedState}}
> >
{{directory-table-header-title
{{yield}} field=this.field
<span class="text"> labelKey=this.labelKey
{{directory-table-header-title icon=this.icon
field=this.field translated=this.translated
labelKey=this.labelKey }}
icon=this.icon {{this.chevronIcon}}
translated=this.translated </span>
}}
{{this.chevronIcon}}
</span>
</div>

View File

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

View File

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

View File

@ -1,15 +1,5 @@
<section class="user-content"> <section class="user-content">
<div class="group-members-actions"> <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}} {{#if this.model.can_see_members}}
<TextField <TextField
@value={{this.filterInput}} @value={{this.filterInput}}
@ -20,35 +10,6 @@
{{/if}} {{/if}}
{{#if this.canManageGroup}} {{#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"> <div class="group-members-manage">
<DButton <DButton
@icon="plus" @icon="plus"
@ -70,147 +31,134 @@
</div> </div>
{{#if this.hasMembers}} {{#if this.hasMembers}}
<LoadMore <LoadMore @selector=".group-members tr" @action={{action "loadMore"}}>
@selector=".group-members .directory-table-row" <table
@action={{action "loadMore"}} 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>
<TableHeaderToggle <tr>
@order={{this.order}} <th class="bulk-select">
@asc={{this.asc}} {{#if this.canManageGroup}}
@field="username_lower" <FlatButton
@labelKey="username" @class="bulk-select"
@class="username" @icon="list"
@automatic={{true}} @action={{action "toggleBulkSelect"}}
@colspan="2" @title="topics.bulk.toggle"
/> />
{{/if}}
{{#if this.canManageGroup}} </th>
<div class="directory-table__column-header"></div> {{#if this.isBulk}}
{{/if}} <th class="bulk-select-buttons">
<span class="bulk-select-buttons-wrap">
<TableHeaderToggle {{#if this.bulkSelection}}
@class="directory-table__column-header" <BulkGroupMemberDropdown
@order={{this.order}} @bulkSelection={{this.bulkSelection}}
@asc={{this.asc}} @canAdminGroup={{this.model.can_admin_group}}
@field="added_at" @canEditGroup={{this.model.can_edit_group}}
@labelKey="groups.member_added" @onChange={{action "actOnSelection" this.bulkSelection}}
@automatic={{true}}
/>
<TableHeaderToggle
@class="directory-table__column-header"
@order={{this.order}}
@asc={{this.asc}}
@field="last_posted_at"
@labelKey="last_post"
@automatic={{true}}
/>
<TableHeaderToggle
@class="directory-table__column-header"
@order={{this.order}}
@asc={{this.asc}}
@field="last_seen_at"
@labelKey="last_seen"
@automatic={{true}}
/>
{{#if this.canManageGroup}}
<div class="directory-table__column-header"></div>
{{/if}}
</:header>
<:body>
{{#each this.model.members as |m|}}
<div class="directory-table__row">
<div class="directory-table__cell group-member" colspan="2">
{{#if this.canManageGroup}}
{{#if this.isBulk}}
<Input
@type="checkbox"
class="bulk-select"
{{on "click" (action "selectMember" m)}}
/> />
{{/if}} {{/if}}
{{/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}}
@field="username_lower"
@labelKey="username"
@class="username"
@automatic={{true}}
@colspan="2"
/>
<TableHeaderToggle
@order={{this.order}}
@asc={{this.asc}}
@field="added_at"
@labelKey="groups.member_added"
@automatic={{true}}
/>
<TableHeaderToggle
@order={{this.order}}
@asc={{this.asc}}
@field="last_posted_at"
@labelKey="last_post"
@automatic={{true}}
/>
<TableHeaderToggle
@order={{this.order}}
@asc={{this.asc}}
@field="last_seen_at"
@labelKey="last_seen"
@automatic={{true}}
/>
<th></th>
</tr>
</thead>
<tbody>
{{#each this.model.members as |m|}}
<tr>
{{#if this.isBulk}}
<td class="bulk-select">
<Input
@type="checkbox"
class="bulk-select"
{{on "click" (action "selectMember" m)}}
/>
</td>
{{/if}}
<td class="group-member" colspan="2">
<UserInfo <UserInfo
@user={{m}} @user={{m}}
@skipName={{this.skipName}} @skipName={{this.skipName}}
@showStatus={{true}} @showStatus={{true}}
@showStatusTooltip={{true}} @showStatusTooltip={{true}}
/> />
</div> </td>
{{#if this.canManageGroup}} <td class="group-owner">
<div class="directory-table__cell group-owner"> {{#if m.owner}}
{{#if (or m.owner m.primary)}} {{d-icon "shield-alt"}}
<span class="directory-table__label"> {{i18n "groups.members.owner"}}<br />
<span>{{i18n "groups.members.status"}}</span> {{/if}}
</span> {{#if m.primary}}
{{/if}} {{i18n "groups.members.primary"}}
<span class="directory-table__value"> {{/if}}
{{#if m.owner}} </td>
{{d-icon "shield-alt"}} <td>
{{i18n "groups.members.owner"}}<br /> <span class="text">{{bound-date m.added_at}}</span>
{{/if}} </td>
{{#if m.primary}} <td>
{{i18n "groups.members.primary"}} <span class="text">{{bound-date m.last_posted_at}}</span>
{{/if}} </td>
</span> <td>
<span class="text">{{bound-date m.last_seen_at}}</span>
</td>
</div> <td>
{{/if}} {{#if this.canManageGroup}}
<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>
{{#if this.canManageGroup}}
<div class="directory-table__cell member-settings">
<GroupMemberDropdown <GroupMemberDropdown
@member={{m}} @member={{m}}
@canAdminGroup={{this.model.can_admin_group}} @canAdminGroup={{this.model.can_admin_group}}
@canEditGroup={{this.model.can_edit_group}} @canEditGroup={{this.model.can_edit_group}}
@onChange={{action "actOnGroup" m}} @onChange={{action "actOnGroup" m}}
/> />
{{! group parameter is used by plugins }} {{/if}}
</div> {{! group parameter is used by plugins }}
{{/if}} </td>
</div> </tr>
{{/each}} {{/each}}
</:body> </tbody>
</table>
</ResponsiveTable>
</LoadMore> </LoadMore>
<ConditionalLoadingSpinner @condition={{this.loading}} /> <ConditionalLoadingSpinner @condition={{this.loading}} />

View File

@ -9,14 +9,10 @@
</div> </div>
{{#if this.hasRequesters}} {{#if this.hasRequesters}}
<LoadMore <LoadMore @selector=".group-members tr" @action={{action "loadMore"}}>
@selector=".group-members .directory-table__row" <table class="group-members">
@action={{action "loadMore"}} <thead>
>
<ResponsiveTable @className="group-members group-members__requests">
<:header>
<TableHeaderToggle <TableHeaderToggle
@class="username"
@order={{this.order}} @order={{this.order}}
@asc={{this.asc}} @asc={{this.asc}}
@field="username_lower" @field="username_lower"
@ -30,34 +26,22 @@
@labelKey="groups.member_requested" @labelKey="groups.member_requested"
@automatic={{true}} @automatic={{true}}
/> />
<div class="directory-table__column-header">{{i18n <th>{{i18n "groups.requests.reason"}}</th>
"groups.requests.reason" <th></th>
}}</div> <th></th>
<div class="directory-table__column-header"></div> </thead>
</:header>
<:body> <tbody>
{{#each this.model.requesters as |m|}} {{#each this.model.requesters as |m|}}
<div class="directory-table__row"> <tr>
<div class="directory-table__cell group-member"> <td class="group-member">
<UserInfo @user={{m}} @skipName={{this.skipName}} /> <UserInfo @user={{m}} @skipName={{this.skipName}} />
</div> </td>
<div class="directory-table__cell"> <td>
<span class="directory-table__label"> <span class="text">{{bound-date m.requested_at}}</span>
<span>{{i18n "groups.member_requested"}}</span> </td>
</span> <td>{{m.reason}}</td>
<span class="directory-table__value"> <td>
<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">
{{#if m.request_undone}} {{#if m.request_undone}}
{{i18n "groups.requests.undone"}} {{i18n "groups.requests.undone"}}
{{else if m.request_accepted}} {{else if m.request_accepted}}
@ -83,14 +67,17 @@
@class="btn-danger" @class="btn-danger"
/> />
{{/if}} {{/if}}
</div> </td>
</div> <td></td>
</tr>
{{/each}} {{/each}}
</:body> </tbody>
</ResponsiveTable> </table>
</LoadMore> </LoadMore>
<ConditionalLoadingSpinner @condition={{this.loading}} /> <ConditionalLoadingSpinner @condition={{this.loading}} />
{{else}} {{else}}
<div>{{i18n "groups.empty.requests"}}</div> <div>{{i18n "groups.empty.requests"}}</div>
{{/if}} {{/if}}
</section> </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"); await click(".hide-emails");
assert.strictEqual( assert.strictEqual(
query(".users-list .user:nth-child(1) .email .directory-table__value") query(".users-list .user:nth-child(1) .email").innerText,
.innerText,
"", "",
"hides the emails" "hides the emails"
); );

View File

@ -20,7 +20,7 @@ acceptance("Group Members - Anonymous", function () {
1, 1,
"it displays the group's avatar flair" "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( assert.ok(
!exists(".group-member-dropdown"), !exists(".group-member-dropdown"),
@ -137,7 +137,7 @@ acceptance("Group Members", function (needs) {
); );
await click("button.bulk-select"); await click("button.bulk-select");
await click(".bulk-select-all"); await click(".bulk-select-buttons button:nth-child(1)");
assert.ok( assert.ok(
exists(".bulk-select-buttons-wrap details"), 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"); await visit("/g/discourse/manage/categories");
assert.ok( assert.ok(
exists(".group-members .group-member"), exists(".group-members tr"),
"it should redirect to members page for an anonymous user" "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"); await visit("/g/discourse/manage/profile");
assert.ok( assert.ok(
exists(".group-members .group-member"), exists(".group-members tr"),
"it should redirect to members page for an anonymous user" "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"); await visit("/g/discourse/manage/tags");
assert.ok( assert.ok(
exists(".group-members .group-member"), exists(".group-members tr"),
"it should redirect to members page for an anonymous user" "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) { test("Group Requests", async function (assert) {
await visit("/g/Macdonald/requests"); await visit("/g/Macdonald/requests");
assert.strictEqual(count(".group-members .group-member"), 2); assert.strictEqual(count(".group-members tr"), 2);
assert.strictEqual( assert.strictEqual(
query(".group-members .directory-table__row:first-child .user-detail") query(".group-members tr:first-child td:nth-child(1)")
.innerText.trim() .innerText.trim()
.replace(/\s+/g, " "), .replace(/\s+/g, " "),
"eviltrout Robin Ward" "eviltrout Robin Ward"
); );
assert.strictEqual( assert.strictEqual(
query( query(".group-members tr:first-child td:nth-child(3)").innerText.trim(),
".group-members .directory-table__row:first-child .directory-table__cell:nth-child(3)"
).innerText.trim(),
"Please accept my membership request." "Please accept my membership request."
); );
assert.strictEqual( assert.strictEqual(
query( query(".group-members tr:first-child .btn-primary").innerText.trim(),
".group-members .directory-table__row:first-child .btn-primary"
).innerText.trim(),
"Accept" "Accept"
); );
assert.strictEqual( assert.strictEqual(
query( query(".group-members tr:first-child .btn-danger").innerText.trim(),
".group-members .directory-table__row:first-child .btn-danger"
).innerText.trim(),
"Deny" "Deny"
); );
await click( await click(".group-members tr:first-child .btn-primary");
".group-members .directory-table__row:first-child .btn-primary"
);
assert.ok( assert.ok(
query( query(".group-members tr:first-child td:nth-child(4)")
".group-members .directory-table__row:first-child .directory-table__cell:nth-child(4)"
)
.innerText.trim() .innerText.trim()
.startsWith("accepted") .startsWith("accepted")
); );
assert.deepEqual(requests, [["19", "true"]]); 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( assert.strictEqual(
query( query(".group-members tr:last-child td:nth-child(4)").innerText.trim(),
".group-members .directory-table__row:last-child .directory-table__cell:nth-child(4)"
).innerText.trim(),
"denied" "denied"
); );
assert.deepEqual(requests, [ assert.deepEqual(requests, [

View File

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

View File

@ -14,10 +14,7 @@ acceptance("User Directory", function () {
document.body.classList.contains("users-page"), document.body.classList.contains("users-page"),
"has the body class" "has the body class"
); );
assert.ok( assert.ok(exists(".directory table tr"), "has a list of users");
exists(".directory .directory-table .directory-table__row"),
"has a list of users"
);
}); });
test("Visit All Time", async function (assert) { test("Visit All Time", async function (assert) {
@ -31,10 +28,7 @@ acceptance("User Directory", function () {
document.body.classList.contains("users-page"), document.body.classList.contains("users-page"),
"has the body class" "has the body class"
); );
assert.ok( assert.ok(exists(".directory table tr"), "has a list of users");
exists(".directory .directory-table .directory-table__row"),
"has a list of users"
);
}); });
test("Visit With Group Filter", async function (assert) { test("Visit With Group Filter", async function (assert) {
@ -43,27 +37,27 @@ acceptance("User Directory", function () {
document.body.classList.contains("users-page"), document.body.classList.contains("users-page"),
"has the body class" "has the body class"
); );
assert.ok( assert.ok(exists(".directory table tr"), "has a list of users");
exists(".directory .directory-table .directory-table__row"),
"has a list of users"
);
}); });
test("Custom user fields are present", async function (assert) { test("Custom user fields are present", async function (assert) {
await visit("/u"); await visit("/u");
const firstRowUserField = query( const firstRow = query(".users-directory table tr");
".directory .directory-table__body .directory-table__row:first-child .directory-table__value--user-field" 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) { test("Can sort table via keyboard", async function (assert) {
await visit("/u"); await visit("/u");
const secondHeading = 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"); await triggerKeyEvent(secondHeading, "keypress", "Enter");

View File

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

View File

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

View File

@ -1,11 +1,16 @@
.directory-table-top-scroll {
width: 100%;
overflow-x: auto;
}
.directory { .directory {
margin-bottom: 100px; margin-bottom: 100px;
.directory-table-container {
width: 100%;
overflow-x: auto;
}
.directory-table-top-scroll {
width: 100%;
overflow-x: auto;
}
&.users-directory { &.users-directory {
.directory-group-selector { .directory-group-selector {
vertical-align: top; vertical-align: top;
@ -34,6 +39,58 @@
color: var(--primary-medium); color: var(--primary-medium);
font-size: var(--font-down-1); 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 { .edit-user-directory-columns-modal {
@ -82,221 +139,3 @@
.edit-user-directory-columns-modal .modal-inner-container { .edit-user-directory-columns-modal .modal-inner-container {
min-width: 450px; 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 { #main-outlet {
grid-area: content; grid-area: content;
container-type: inline-size;
container-name: main-outlet;
} }
} }

View File

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

View File

@ -21,6 +21,35 @@
color: var(--primary-medium); color: var(--primary-medium);
padding: 5px; 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 { .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" make_all_primary_description: "Make this the primary group for all selected users"
remove_all_primary: "Remove as Primary" remove_all_primary: "Remove as Primary"
remove_all_primary_description: "Remove this group as primary" remove_all_primary_description: "Remove this group as primary"
status: "Status"
owner: "Owner" owner: "Owner"
primary: "Primary" primary: "Primary"
forbidden: "You're not allowed to view the members." 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." not_found: "Sorry, that username doesn't exist in our system."
id_not_found: "Sorry, that user id doesn't exist in our system." id_not_found: "Sorry, that user id doesn't exist in our system."
active: "Activated" active: "Activated"
status: "Status"
show_emails: "Show Emails" show_emails: "Show Emails"
hide_emails: "Hide Emails" hide_emails: "Hide Emails"
nav: nav: