From e206bd89072b68c8abe6087380223db9bcf68f3f Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 2 Mar 2023 09:20:38 -0500 Subject: [PATCH] REFACTOR: user directories without `` (#20316) --- .template-lintrc.js | 2 +- .../controllers/admin-users-list-show.js | 15 + .../admin/addon/templates/users-list-show.hbs | 334 +++++++++++------- .../app/components/directory-item.hbs | 40 ++- .../app/components/directory-item.js | 3 +- .../app/components/directory-table.hbs | 66 ++-- .../app/components/directory-table.js | 94 +---- .../app/components/responsive-table.hbs | 20 ++ .../app/components/responsive-table.js | 51 +++ .../app/components/table-header-toggle.hbs | 22 +- .../app/components/table-header-toggle.js | 4 +- .../app/helpers/directory-item-helpers.js | 10 +- .../discourse/app/templates/group-index.hbs | 272 ++++++++------ .../app/templates/group-requests.hbs | 61 ++-- .../mobile/components/directory-item.hbs | 37 -- .../discourse/app/templates/mobile/users.hbs | 80 ----- .../tests/acceptance/admin-users-list-test.js | 3 +- .../tests/acceptance/group-index-test.js | 4 +- .../group-manage-categories-test.js | 2 +- .../acceptance/group-manage-profile-test.js | 2 +- .../acceptance/group-manage-tags-test.js | 2 +- .../tests/acceptance/group-requests-test.js | 30 +- .../tests/acceptance/mobile-users-test.js | 5 +- .../discourse/tests/acceptance/users-test.js | 28 +- .../stylesheets/common/admin/admin_base.scss | 22 +- .../stylesheets/common/admin/users.scss | 76 ++-- .../stylesheets/common/base/directory.scss | 285 +++++++++++---- .../stylesheets/common/base/discourse.scss | 2 + app/assets/stylesheets/common/base/group.scss | 93 +++-- app/assets/stylesheets/mobile/directory.scss | 29 -- config/locales/client.en.yml | 2 + 31 files changed, 959 insertions(+), 737 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/components/responsive-table.hbs create mode 100644 app/assets/javascripts/discourse/app/components/responsive-table.js delete mode 100644 app/assets/javascripts/discourse/app/templates/mobile/components/directory-item.hbs delete mode 100644 app/assets/javascripts/discourse/app/templates/mobile/users.hbs diff --git a/.template-lintrc.js b/.template-lintrc.js index a5c4998a0ce..77a11b8f2cb 100644 --- a/.template-lintrc.js +++ b/.template-lintrc.js @@ -15,7 +15,7 @@ module.exports = { "directory-item-value", "directory-table-header-title", "loading-spinner", - "mobile-directory-item-label", + "directory-item-label", ], }, "no-implicit-this": { diff --git a/app/assets/javascripts/admin/addon/controllers/admin-users-list-show.js b/app/assets/javascripts/admin/addon/controllers/admin-users-list-show.js index e51e7c3a5cb..0976adec96a 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-users-list-show.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-users-list-show.js @@ -31,6 +31,21 @@ 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); diff --git a/app/assets/javascripts/admin/addon/templates/users-list-show.hbs b/app/assets/javascripts/admin/addon/templates/users-list-show.hbs index 95f9c8fbbbc..20ff75eb77c 100644 --- a/app/assets/javascripts/admin/addon/templates/users-list-show.hbs +++ b/app/assets/javascripts/admin/addon/templates/users-list-show.hbs @@ -26,140 +26,204 @@ @title={{this.searchHint}} /> - {{#if this.model}} -
- - - - - - - - - - - - {{#if this.siteSettings.must_approve_users}} - - {{/if}} - - - - + + + <:header> + + + + + + + + + + + {{#if this.siteSettings.must_approve_users}} +
{{i18n + "admin.users.approved" + }}
+ {{/if}} +
 
+ + + + <:body> {{#each this.model as |user|}} -
- - + +
+ + {{~user.email~}} + +
{{#if user.last_emailed_at}} - +
+ + {{i18n "admin.users.last_emailed"}} + + + {{format-duration user.last_emailed_age}} + +
{{else}} - +
+ + {{i18n "admin.users.last_emailed"}} + + + {{format-duration user.last_emailed_age}} + +
{{/if}} - - - - - - +
+ + {{i18n "last_seen"}} + + + {{format-duration user.last_seen_age}} + +
+
+ + {{i18n "admin.user.topics_entered"}} + + + {{number user.topics_entered}} + +
+
+ + {{i18n "admin.user.posts_read_count"}} + + + {{number user.posts_read_count}} + +
+
+ + {{i18n "admin.user.time_read"}} + + + {{format-duration user.time_read}} + +
+
+ + {{i18n "created"}} + + + {{format-duration user.created_at_age}} + +
{{#if this.siteSettings.must_approve_users}} - +
+ + {{i18n "admin.users.approved"}} + + + {{i18n-yes-no user.approved}} + +
{{/if}} - - + + {{/each}} - -
{{i18n "admin.users.approved"}} 
- +
+ {{avatar user imageSize="small"}} - {{user.username}} + + {{user.username}} + {{#if user.staged}} {{d-icon "far-envelope" title="user.staged"}} {{/if}} -
-
{{i18n "admin.users.last_emailed"}}
-
{{format-duration user.last_emailed_age}}
-
-
{{i18n "admin.users.last_emailed"}}
-
{{format-duration user.last_emailed_age}}
-
-
{{i18n "last_seen"}}
-
{{format-duration user.last_seen_age}}
-
-
{{i18n "admin.user.topics_entered"}}
-
{{number user.topics_entered}}
-
-
{{i18n "admin.user.posts_read_count"}}
-
{{number user.posts_read_count}}
-
-
{{i18n "admin.user.time_read"}}
-
{{format-duration user.time_read}}
-
-
{{i18n "created"}}
-
{{format-duration user.created_at_age}}
-
{{i18n-yes-no user.approved}} - {{#if user.admin}} - {{d-icon "shield-alt" title="admin.title"}} - {{/if}} - {{#if user.moderator}} - {{d-icon "shield-alt" title="admin.moderator"}} - {{/if}} - {{#if user.second_factor_enabled}} - {{d-icon "lock" title="admin.user.second_factor_enabled"}} - {{/if}} +
+ + {{i18n "admin.users.status"}} + + + {{#if user.admin}} + {{d-icon "shield-alt" title="admin.title"}} + {{/if}} + {{#if user.moderator}} + {{d-icon "shield-alt" title="admin.moderator"}} + {{/if}} + {{#if user.second_factor_enabled}} + {{d-icon "lock" title="admin.user.second_factor_enabled"}} + {{/if}} + -
+ + + + {{else}}

{{i18n "search.no_results"}}

diff --git a/app/assets/javascripts/discourse/app/components/directory-item.hbs b/app/assets/javascripts/discourse/app/components/directory-item.hbs index 4561d5ecb68..e05a5367c79 100644 --- a/app/assets/javascripts/discourse/app/components/directory-item.hbs +++ b/app/assets/javascripts/discourse/app/components/directory-item.hbs @@ -1,16 +1,38 @@ - +
+ +
+ {{#each this.columns as |column|}} - - {{#if (directory-column-is-user-field column=column)}} + {{#if (directory-column-is-user-field column=column)}} +
+ + {{column.name}} + {{directory-item-user-field-value item=this.item column=column}} - {{else}} +
+ {{else}} +
+ + + {{#if column.icon}} + {{d-icon column.icon}} + {{/if}} + {{directory-item-label item=this.item column=column}} + + {{directory-item-value item=this.item column=column}} - {{/if}} - +
+ {{/if}} + {{/each}} {{#if this.showTimeRead}} - {{format-duration - this.item.time_read - }} +
+ + {{i18n "directory.time_read"}} + + + {{format-duration this.item.time_read}} + +
{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/directory-item.js b/app/assets/javascripts/discourse/app/components/directory-item.js index 0b557d6672a..f1b4e15a829 100644 --- a/app/assets/javascripts/discourse/app/components/directory-item.js +++ b/app/assets/javascripts/discourse/app/components/directory-item.js @@ -2,7 +2,8 @@ import Component from "@ember/component"; import { propertyEqual } from "discourse/lib/computed"; export default Component.extend({ - tagName: "tr", + tagName: "div", + classNames: ["directory-table__row"], classNameBindings: ["me"], me: propertyEqual("item.user.id", "currentUser.id"), columns: null, diff --git a/app/assets/javascripts/discourse/app/components/directory-table.hbs b/app/assets/javascripts/discourse/app/components/directory-table.hbs index d8adc30add1..f9aa74ba30a 100644 --- a/app/assets/javascripts/discourse/app/components/directory-table.hbs +++ b/app/assets/javascripts/discourse/app/components/directory-table.hbs @@ -1,39 +1,37 @@ -
-
-
- -
- - + + <:header> + + {{#each this.columns as |column|}} - {{#each this.columns as |column|}} - - {{/each}} + {{/each}} - {{#if this.showTimeRead}} - - {{/if}} - - - {{#each this.items as |item|}} - - {{/each}} - -
{{i18n "directory.time_read"}}
-
\ No newline at end of file + {{#if this.showTimeRead}} +
+
+ {{i18n "directory.time_read"}} +
+
+ {{/if}} + + <:body> + {{#each this.items as |item|}} + + {{/each}} + + \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/directory-table.js b/app/assets/javascripts/discourse/app/components/directory-table.js index 08cb163562c..cf031759516 100644 --- a/app/assets/javascripts/discourse/app/components/directory-table.js +++ b/app/assets/javascripts/discourse/app/components/directory-table.js @@ -2,109 +2,31 @@ 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._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); + this._table.style.gridTemplateColumns = `minmax(13em, 3fr) repeat(${this._columnCount}, minmax(max-content, 1fr))`; }, @action setActiveHeader(header) { // After render, scroll table left to ensure the order by column is visible - if (!this._tableContainer) { - this.set( - "_tableContainer", - document.querySelector(".directory-table-container") - ); + if (!this._table) { + this.set("_table", document.querySelector(".directory-table")); } const scrollPixels = - header.offsetLeft + - header.offsetWidth + - 10 - - this._tableContainer.offsetWidth; + header.offsetLeft + header.offsetWidth + 10 - this._table.offsetWidth; if (scrollPixels > 0) { - this._tableContainer.scrollLeft = scrollPixels; + this._table.scrollLeft = scrollPixels; } }, }); diff --git a/app/assets/javascripts/discourse/app/components/responsive-table.hbs b/app/assets/javascripts/discourse/app/components/responsive-table.hbs new file mode 100644 index 00000000000..26c610e184c --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/responsive-table.hbs @@ -0,0 +1,20 @@ +
+
+
+
+
+ {{yield to="header"}} +
+
+ {{yield to="body"}} +
+
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/responsive-table.js b/app/assets/javascripts/discourse/app/components/responsive-table.js new file mode 100644 index 00000000000..aecf9c67c34 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/responsive-table.js @@ -0,0 +1,51 @@ +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); + } + } +} diff --git a/app/assets/javascripts/discourse/app/components/table-header-toggle.hbs b/app/assets/javascripts/discourse/app/components/table-header-toggle.hbs index 4a8aacf6721..dc32c5c91fb 100644 --- a/app/assets/javascripts/discourse/app/components/table-header-toggle.hbs +++ b/app/assets/javascripts/discourse/app/components/table-header-toggle.hbs @@ -1,4 +1,4 @@ - - {{directory-table-header-title - field=this.field - labelKey=this.labelKey - icon=this.icon - translated=this.translated - }} - {{this.chevronIcon}} - \ No newline at end of file + + {{yield}} + + {{directory-table-header-title + field=this.field + labelKey=this.labelKey + icon=this.icon + translated=this.translated + }} + {{this.chevronIcon}} + + \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/table-header-toggle.js b/app/assets/javascripts/discourse/app/components/table-header-toggle.js index 78dc01dffee..1b1bb07ce1a 100644 --- a/app/assets/javascripts/discourse/app/components/table-header-toggle.js +++ b/app/assets/javascripts/discourse/app/components/table-header-toggle.js @@ -6,8 +6,8 @@ import discourseComputed from "discourse-common/utils/decorators"; import I18n from "I18n"; export default Component.extend({ - tagName: "th", - classNames: ["sortable"], + tagName: "div", + classNames: ["directory-table__column-header", "sortable"], attributeBindings: ["title", "colspan", "ariaSort:aria-sort", "role"], role: "columnheader", labelKey: null, diff --git a/app/assets/javascripts/discourse/app/helpers/directory-item-helpers.js b/app/assets/javascripts/discourse/app/helpers/directory-item-helpers.js index 29339de185a..41ab101dcc2 100644 --- a/app/assets/javascripts/discourse/app/helpers/directory-item-helpers.js +++ b/app/assets/javascripts/discourse/app/helpers/directory-item-helpers.js @@ -3,7 +3,7 @@ import { number } from "discourse/lib/formatter"; import { registerUnbound } from "discourse-common/lib/helpers"; import I18n from "I18n"; -registerUnbound("mobile-directory-item-label", function (args) { +registerUnbound("directory-item-label", function (args) { // Args should include key/values { item, column } const count = args.item.get(args.column.name); const translationPrefix = @@ -14,7 +14,9 @@ registerUnbound("mobile-directory-item-label", function (args) { registerUnbound("directory-item-value", function (args) { // Args should include key/values { item, column } return htmlSafe( - `${number(args.item.get(args.column.name))}` + `${number( + args.item.get(args.column.name) + )}` ); }); @@ -25,7 +27,9 @@ registerUnbound("directory-item-user-field-value", function (args) { ? args.item.user.user_fields[args.column.user_field_id] : null; const content = value || "-"; - return htmlSafe(`${content}`); + return htmlSafe( + `${content}` + ); }); registerUnbound("directory-column-is-automatic", function (args) { diff --git a/app/assets/javascripts/discourse/app/templates/group-index.hbs b/app/assets/javascripts/discourse/app/templates/group-index.hbs index dc0dfa4809b..ec769263a0c 100644 --- a/app/assets/javascripts/discourse/app/templates/group-index.hbs +++ b/app/assets/javascripts/discourse/app/templates/group-index.hbs @@ -1,5 +1,15 @@
+ + {{#if this.canManageGroup}} + + {{/if}} + {{#if this.model.can_see_members}} + {{#if this.bulkSelection}} + + + + {{/if}} + + + + + {{/if}} +
{{#if this.hasMembers}} - - + + - - - - {{#if this.isBulk}} - - {{/if}} - - - - - - - - - - {{#each this.model.members as |m|}} - - {{#if this.isBulk}} - - {{/if}} - - + - - - - + {{#if this.canManageGroup}} +
+ {{#if (or m.owner m.primary)}} + + {{i18n "groups.members.status"}} + + {{/if}} + + {{#if m.owner}} + {{d-icon "shield-alt"}} + {{i18n "groups.members.owner"}}
+ {{/if}} + {{#if m.primary}} + {{i18n "groups.members.primary"}} + {{/if}} +
-
- + {{! group parameter is used by plugins }} + + {{/if}} + {{/each}} - -
- {{#if this.canManageGroup}} - - {{/if}} - - - {{#if this.bulkSelection}} - + + + {{#if this.canManageGroup}} +
+ {{/if}} + + + + + + {{#if this.canManageGroup}} +
+ {{/if}} + + + <:body> + {{#each this.model.members as |m|}} +
+ +
+ {{#if this.canManageGroup}} + {{#if this.isBulk}} + {{/if}} - - - -
- - + {{/if}} - - {{#if m.owner}} - {{d-icon "shield-alt"}} - {{i18n "groups.members.owner"}}
- {{/if}} - {{#if m.primary}} - {{i18n "groups.members.primary"}} - {{/if}} -
- {{bound-date m.added_at}} - - {{bound-date m.last_posted_at}} - - {{bound-date m.last_seen_at}} - - {{#if this.canManageGroup}} + + {{/if}} +
+ + {{i18n "groups.member_added"}} + + + {{bound-date m.added_at}} + +
+
+ {{#if m.last_posted_at}} + + {{i18n "last_post"}} + + {{/if}} + + {{bound-date m.last_posted_at}} + +
+
+ {{#if m.last_seen_at}} + + {{i18n "last_seen"}} + + {{/if}} + + {{bound-date m.last_seen_at}} + +
+ {{#if this.canManageGroup}} +
- {{/if}} - {{! group parameter is used by plugins }} -
+ + + +
diff --git a/app/assets/javascripts/discourse/app/templates/group-requests.hbs b/app/assets/javascripts/discourse/app/templates/group-requests.hbs index 6ea7de3ea1a..2f03497cb6c 100644 --- a/app/assets/javascripts/discourse/app/templates/group-requests.hbs +++ b/app/assets/javascripts/discourse/app/templates/group-requests.hbs @@ -9,10 +9,14 @@
{{#if this.hasRequesters}} - - - + + + <:header> - - - - - - +
{{i18n + "groups.requests.reason" + }}
+
+ + <:body> {{#each this.model.requesters as |m|}} - - - - - - - + + {{/each}} - -
{{i18n "groups.requests.reason"}}
+
+
-
- {{bound-date m.requested_at}} - {{m.reason}} + +
+ + {{i18n "groups.member_requested"}} + + + {{bound-date m.requested_at}} + +
+
+ + {{i18n "groups.requests.reason"}} + + + {{m.reason}} + +
+
{{#if m.request_undone}} {{i18n "groups.requests.undone"}} {{else if m.request_accepted}} @@ -67,17 +83,14 @@ @class="btn-danger" /> {{/if}} -
+ +
- {{else}}
{{i18n "groups.empty.requests"}}
{{/if}} -
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/templates/mobile/components/directory-item.hbs b/app/assets/javascripts/discourse/app/templates/mobile/components/directory-item.hbs deleted file mode 100644 index 6f8d45048ec..00000000000 --- a/app/assets/javascripts/discourse/app/templates/mobile/components/directory-item.hbs +++ /dev/null @@ -1,37 +0,0 @@ - - -{{#each this.columns as |column|}} - {{#if (directory-column-is-user-field column=column)}} - {{#if (get this.item.user.user_fields column.user_field_id)}} -
- - {{directory-item-user-field-value item=this.item column=column}} - - - {{column.name}} - -
- {{/if}} - - {{else}} -
- - {{directory-item-value item=this.item column=column}} - - - {{#if column.icon}} - {{d-icon column.icon}} - {{/if}} - {{mobile-directory-item-label item=this.item column=column}} - -
- {{/if}} -{{/each}} - -{{#if this.showTimeRead}} - -{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/templates/mobile/users.hbs b/app/assets/javascripts/discourse/app/templates/mobile/users.hbs deleted file mode 100644 index 8f023d3f8fb..00000000000 --- a/app/assets/javascripts/discourse/app/templates/mobile/users.hbs +++ /dev/null @@ -1,80 +0,0 @@ - -
-
- - - - -
- - {{#if this.lastUpdatedAt}} -
- {{i18n "directory.last_updated"}} - {{this.lastUpdatedAt}} -
- {{/if}} -
- - - {{#if this.currentUser.staff}} - - {{/if}} -
- -
- - - {{#if this.model.length}} -
{{i18n - "directory.total_rows" - count=this.model.totalRows - }}
- {{#each this.model as |item|}} - - {{/each}} - - - {{else}} -
-

{{i18n "directory.no_results"}}

- {{/if}} -
- -
-
-
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-users-list-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-users-list-test.js index 6e051abe6be..83badf83d05 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/admin-users-list-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/admin-users-list-test.js @@ -68,7 +68,8 @@ acceptance("Admin - Users List", function (needs) { await click(".hide-emails"); assert.strictEqual( - query(".users-list .user:nth-child(1) .email").innerText, + query(".users-list .user:nth-child(1) .email .directory-table__value") + .innerText, "", "hides the emails" ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-index-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-index-test.js index 2707adf91d9..475b6baf7f8 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/group-index-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/group-index-test.js @@ -20,7 +20,7 @@ acceptance("Group Members - Anonymous", function () { 1, "it displays the group's avatar flair" ); - assert.ok(exists(".group-members tr"), "it lists group members"); + assert.ok(exists(".group-members .group-member"), "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-buttons button:nth-child(1)"); + await click(".bulk-select-all"); assert.ok( exists(".bulk-select-buttons-wrap details"), diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-manage-categories-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-manage-categories-test.js index 1e36361608c..38dcd87512d 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/group-manage-categories-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/group-manage-categories-test.js @@ -12,7 +12,7 @@ acceptance("Managing Group Category Notification Defaults", function () { await visit("/g/discourse/manage/categories"); assert.ok( - exists(".group-members tr"), + exists(".group-members .group-member"), "it should redirect to members page for an anonymous user" ); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-manage-profile-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-manage-profile-test.js index aa436364a53..c3de67d7f55 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/group-manage-profile-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/group-manage-profile-test.js @@ -12,7 +12,7 @@ acceptance("Managing Group Profile", function () { await visit("/g/discourse/manage/profile"); assert.ok( - exists(".group-members tr"), + exists(".group-members .group-member"), "it should redirect to members page for an anonymous user" ); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-manage-tags-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-manage-tags-test.js index 2d75056e83d..e3d500831c9 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/group-manage-tags-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/group-manage-tags-test.js @@ -12,7 +12,7 @@ acceptance("Managing Group Tag Notification Defaults", function () { await visit("/g/discourse/manage/tags"); assert.ok( - exists(".group-members tr"), + exists(".group-members .group-member"), "it should redirect to members page for an anonymous user" ); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-requests-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-requests-test.js index abbde3f6038..2a9441dfb11 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/group-requests-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/group-requests-test.js @@ -89,37 +89,49 @@ acceptance("Group Requests", function (needs) { test("Group Requests", async function (assert) { await visit("/g/Macdonald/requests"); - assert.strictEqual(count(".group-members tr"), 2); + assert.strictEqual(count(".group-members .group-member"), 2); assert.strictEqual( - query(".group-members tr:first-child td:nth-child(1)") + query(".group-members .directory-table__row:first-child .user-detail") .innerText.trim() .replace(/\s+/g, " "), "eviltrout Robin Ward" ); assert.strictEqual( - query(".group-members tr:first-child td:nth-child(3)").innerText.trim(), + query( + ".group-members .directory-table__row:first-child .directory-table__cell:nth-child(3)" + ).innerText.trim(), "Please accept my membership request." ); assert.strictEqual( - query(".group-members tr:first-child .btn-primary").innerText.trim(), + query( + ".group-members .directory-table__row:first-child .btn-primary" + ).innerText.trim(), "Accept" ); assert.strictEqual( - query(".group-members tr:first-child .btn-danger").innerText.trim(), + query( + ".group-members .directory-table__row:first-child .btn-danger" + ).innerText.trim(), "Deny" ); - await click(".group-members tr:first-child .btn-primary"); + await click( + ".group-members .directory-table__row:first-child .btn-primary" + ); assert.ok( - query(".group-members tr:first-child td:nth-child(4)") + query( + ".group-members .directory-table__row:first-child .directory-table__cell:nth-child(4)" + ) .innerText.trim() .startsWith("accepted") ); assert.deepEqual(requests, [["19", "true"]]); - await click(".group-members tr:last-child .btn-danger"); + await click(".group-members .directory-table__row:last-child .btn-danger"); assert.strictEqual( - query(".group-members tr:last-child td:nth-child(4)").innerText.trim(), + query( + ".group-members .directory-table__row:last-child .directory-table__cell:nth-child(4)" + ).innerText.trim(), "denied" ); assert.deepEqual(requests, [ diff --git a/app/assets/javascripts/discourse/tests/acceptance/mobile-users-test.js b/app/assets/javascripts/discourse/tests/acceptance/mobile-users-test.js index 9913b12f4e2..4f46e025547 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/mobile-users-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/mobile-users-test.js @@ -7,6 +7,9 @@ acceptance("User Directory - Mobile", function (needs) { test("Visit Page", async function (assert) { await visit("/u"); - assert.ok(exists(".directory .user"), "has a list of users"); + assert.ok( + exists(".directory .directory-table__row"), + "has a list of users" + ); }); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/users-test.js b/app/assets/javascripts/discourse/tests/acceptance/users-test.js index d6ffec8bc65..941bc4e0d1c 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/users-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/users-test.js @@ -14,7 +14,10 @@ acceptance("User Directory", function () { document.body.classList.contains("users-page"), "has the body class" ); - assert.ok(exists(".directory table tr"), "has a list of users"); + assert.ok( + exists(".directory .directory-table .directory-table__row"), + "has a list of users" + ); }); test("Visit All Time", async function (assert) { @@ -28,7 +31,10 @@ acceptance("User Directory", function () { document.body.classList.contains("users-page"), "has the body class" ); - assert.ok(exists(".directory table tr"), "has a list of users"); + assert.ok( + exists(".directory .directory-table .directory-table__row"), + "has a list of users" + ); }); test("Visit With Group Filter", async function (assert) { @@ -37,27 +43,27 @@ acceptance("User Directory", function () { document.body.classList.contains("users-page"), "has the body class" ); - assert.ok(exists(".directory table tr"), "has a list of users"); + assert.ok( + exists(".directory .directory-table .directory-table__row"), + "has a list of users" + ); }); test("Custom user fields are present", async function (assert) { await visit("/u"); - const firstRow = query(".users-directory table tr"); - const columnData = firstRow.querySelectorAll("td"); - const favoriteColorTd = columnData[columnData.length - 1]; - - assert.strictEqual( - favoriteColorTd.querySelector("span").textContent, - "Blue" + const firstRowUserField = query( + ".directory .directory-table__body .directory-table__row:first-child .directory-table__value--user-field" ); + + assert.strictEqual(firstRowUserField.textContent, "Blue"); }); test("Can sort table via keyboard", async function (assert) { await visit("/u"); const secondHeading = - ".users-directory table th:nth-child(2) .header-contents"; + ".users-directory .directory-table__header div:nth-child(2) .header-contents"; await triggerKeyEvent(secondHeading, "keypress", "Enter"); diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 3a2514f6cec..28044352472 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -802,19 +802,15 @@ section.details { } } -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; +.directory-table { + .not-activated { + .directory-table__cell { + &, + a, + a:visited { + color: #bbb; + } + } } } diff --git a/app/assets/stylesheets/common/admin/users.scss b/app/assets/stylesheets/common/admin/users.scss index 9dff0bd83b3..e824dbf9a9f 100644 --- a/app/assets/stylesheets/common/admin/users.scss +++ b/app/assets/stylesheets/common/admin/users.scss @@ -99,49 +99,55 @@ } .admin-users-list { - td.username { - @include ellipsis; - 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 + .directory-table__cell { + &.username { + justify-content: start; } - td.email { - max-width: 28vw; // Prevents horizontal scroll down to 768px - overflow-wrap: break-word; + &.email { + justify-content: start; + span { + display: flex; + min-width: 17em; + word-break: break-all; + } } } - @media screen and (max-width: 767px) { - tr { - td.username { - grid-column-start: 1; - grid-column-end: -2; - font-weight: bold; - } - td.user-status { - text-align: right; - grid-row: 1; - grid-column-end: -1; - .d-icon { - margin-left: 0.25em; - } - } - td.email { - grid-column-start: 1; - grid-column-end: -1; - word-wrap: break-word; - overflow-wrap: break-word; - overflow: hidden; - min-width: 0; - margin: 0.5em 0 0 0; - &:empty { - display: none; + .directory-table { + margin-top: 1em; + &__column-header--username, + &__column-header--email { + .header-contents { + text-align: left; + } + } + + &__cell.username { + align-items: center; + } + + &__cell.email { + @include breakpoint("tablet") { + grid-column-start: 1; + grid-column-end: -1; + span { + max-width: 100%; } } } } + + .directory-table__cell { + padding: 0.5em 0.25em; + } + + .user-status span { + gap: 0.15em; + } + + .avatar { + margin-right: 0.25em; + } } // mobile styles diff --git a/app/assets/stylesheets/common/base/directory.scss b/app/assets/stylesheets/common/base/directory.scss index f64f1b1cec4..c8a120c1ff8 100644 --- a/app/assets/stylesheets/common/base/directory.scss +++ b/app/assets/stylesheets/common/base/directory.scss @@ -1,16 +1,11 @@ +.directory-table-top-scroll { + width: 100%; + overflow-x: auto; +} + .directory { margin-bottom: 100px; - .directory-table-container { - width: 100%; - overflow-x: auto; - } - - .directory-table-top-scroll { - width: 100%; - overflow-x: auto; - } - &.users-directory { .directory-group-selector { vertical-align: top; @@ -39,58 +34,6 @@ 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 { @@ -139,3 +82,221 @@ .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; + } + } + } + } +} diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 0d1e38e3e9c..25d767cb552 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -596,6 +596,8 @@ table { #main-outlet { grid-area: content; + container-type: inline-size; + container-name: main-outlet; } } diff --git a/app/assets/stylesheets/common/base/group.scss b/app/assets/stylesheets/common/base/group.scss index 5ef4362f9bf..d961bedb60e 100644 --- a/app/assets/stylesheets/common/base/group.scss +++ b/app/assets/stylesheets/common/base/group.scss @@ -25,15 +25,27 @@ display: flex; flex-wrap: wrap; width: 100%; + gap: 0.5em 0; - input + .group-members-manage { - margin-left: auto; + .bulk-select + input { + margin-left: 0.5em; } - .group-username-filter { - margin: 0 0 5px 0; - vertical-align: middle; + input { + margin: 0 auto 0 0; } + + .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 { @@ -118,58 +130,45 @@ table.group-manage-logs { } } -table.group-members { - width: 100%; +.group-members { + grid-template-columns: 3fr repeat(3, minmax(min-content, 1fr)); - th { - text-align: center; - - &.bulk-select { - height: 30px; - width: 30px; - } - - &.bulk-select-buttons { - text-align: left; - white-space: nowrap; - width: 1%; - - .bulk-select-buttons-wrap { - display: flex; + &--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; } - - .btn { - margin-right: 0.25em; + .member-settings { + margin-left: auto; + } + .user-info { + padding-right: 3.5em; } - } - - &.username { - text-align: left; } } - td { - color: var(--primary-medium); - padding: 0.8em 0; - text-align: center; - - &.group-member { - text-align: left; - } + &.group-members__requests { + grid-template-columns: 3fr repeat(3, minmax(min-content, 1fr)); } - .user-info { - display: block; + .directory-table__value { + font-size: var(--font-0); + color: var(--primary); + } - .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; } } } diff --git a/app/assets/stylesheets/mobile/directory.scss b/app/assets/stylesheets/mobile/directory.scss index 8965198565c..b1de306686f 100644 --- a/app/assets/stylesheets/mobile/directory.scss +++ b/app/assets/stylesheets/mobile/directory.scss @@ -21,35 +21,6 @@ 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 { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 2fcce585138..3bb954ef481 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -917,6 +917,7 @@ 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." @@ -5623,6 +5624,7 @@ 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: