diff --git a/app/assets/javascripts/discourse/app/components/about-page-user.gjs b/app/assets/javascripts/discourse/app/components/about-page-user.gjs new file mode 100644 index 00000000000..10f9907eb81 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/about-page-user.gjs @@ -0,0 +1,47 @@ +import avatar from "discourse/helpers/avatar"; +import { prioritizeNameInUx } from "discourse/lib/settings"; +import { userPath } from "discourse/lib/url"; +import i18n from "discourse-common/helpers/i18n"; + +const AboutPageUser = <template> + <div data-username={{@user.username}} class="user-info small"> + <div class="user-image"> + <div class="user-image-inner"> + <a + href={{userPath @user.username}} + data-user-card={{@user.username}} + aria-hidden="true" + > + {{avatar @user imageSize="large"}} + </a> + </div> + </div> + <div class="user-detail"> + <div class="name-line"> + <a + href={{userPath @user.username}} + data-user-card={{@user.username}} + aria-label={{i18n "user.profile_possessive" username=@user.username}} + > + <span class="username"> + {{#if (prioritizeNameInUx @user.name)}} + {{@user.name}} + {{else}} + {{@user.username}} + {{/if}} + </span> + <span class="name"> + {{#if (prioritizeNameInUx @user.name)}} + {{@user.username}} + {{else}} + {{@user.name}} + {{/if}} + </span> + </a> + </div> + <div class="title">{{@user.title}}</div> + </div> + </div> +</template>; + +export default AboutPageUser; diff --git a/app/assets/javascripts/discourse/app/components/about-page-users.gjs b/app/assets/javascripts/discourse/app/components/about-page-users.gjs index 015b6480fea..af760c61648 100644 --- a/app/assets/javascripts/discourse/app/components/about-page-users.gjs +++ b/app/assets/javascripts/discourse/app/components/about-page-users.gjs @@ -1,71 +1,49 @@ import Component from "@glimmer/component"; -import { service } from "@ember/service"; -import { htmlSafe } from "@ember/template"; -import { renderAvatar } from "discourse/helpers/user-avatar"; -import { prioritizeNameInUx } from "discourse/lib/settings"; -import { userPath } from "discourse/lib/url"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; +import AboutPageUser from "discourse/components/about-page-user"; +import DButton from "discourse/components/d-button"; import i18n from "discourse-common/helpers/i18n"; export default class AboutPageUsers extends Component { - @service siteSettings; + @tracked expanded = false; - get usersTemplates() { - return (this.args.users || []).map((user) => ({ - name: user.name, - username: user.username, - userPath: userPath(user.username), - avatar: renderAvatar(user, { - imageSize: "large", - siteSettings: this.siteSettings, - }), - title: user.title || "", - prioritizeName: prioritizeNameInUx(user.name), - })); + get users() { + let users = this.args.users; + if (this.showViewMoreButton && !this.expanded) { + users = users.slice(0, this.args.truncateAt); + } + return users; + } + + get showViewMoreButton() { + return ( + this.args.truncateAt > 0 && this.args.users.length > this.args.truncateAt + ); + } + + @action + toggleExpanded() { + this.expanded = !this.expanded; } <template> - {{#each this.usersTemplates as |template|}} - <div data-username={{template.username}} class="user-info small"> - <div class="user-image"> - <div class="user-image-inner"> - <a - href={{template.userPath}} - data-user-card={{template.username}} - aria-hidden="true" - > - {{htmlSafe template.avatar}} - </a> - </div> - </div> - <div class="user-detail"> - <div class="name-line"> - <a - href={{template.userPath}} - data-user-card={{template.username}} - aria-label={{i18n - "user.profile_possessive" - username=template.username - }} - > - <span class="username"> - {{#if template.prioritizeName}} - {{template.name}} - {{else}} - {{template.username}} - {{/if}} - </span> - <span class="name"> - {{#if template.prioritizeName}} - {{template.username}} - {{else}} - {{template.name}} - {{/if}} - </span> - </a> - </div> - <div class="title">{{template.title}}</div> - </div> - </div> - {{/each}} + <div class="about-page-users-list"> + {{#each this.users as |user|}} + <AboutPageUser @user={{user}} /> + {{/each}} + </div> + {{#if this.showViewMoreButton}} + <DButton + class="btn-flat about-page-users-list__expand-button" + @action={{this.toggleExpanded}} + @icon={{if this.expanded "chevron-up" "chevron-down"}} + @translatedLabel={{if + this.expanded + (i18n "about.view_less") + (i18n "about.view_more") + }} + /> + {{/if}} </template> } diff --git a/app/assets/javascripts/discourse/app/components/about-page.gjs b/app/assets/javascripts/discourse/app/components/about-page.gjs index ecf252ae5a1..73a1e726318 100644 --- a/app/assets/javascripts/discourse/app/components/about-page.gjs +++ b/app/assets/javascripts/discourse/app/components/about-page.gjs @@ -1,6 +1,7 @@ import Component from "@glimmer/component"; import { hash } from "@ember/helper"; import { htmlSafe } from "@ember/template"; +import AboutPageUsers from "discourse/components/about-page-users"; import PluginOutlet from "discourse/components/plugin-outlet"; import { number } from "discourse/lib/formatter"; import dIcon from "discourse-common/helpers/d-icon"; @@ -161,6 +162,20 @@ export default class AboutPage extends Component { </div> <h3>{{i18n "about.simple_title"}}</h3> <div>{{htmlSafe @model.extended_site_description}}</div> + + {{#if @model.admins.length}} + <section class="about__admins"> + <h3>{{dIcon "users"}} {{i18n "about.our_admins"}}</h3> + <AboutPageUsers @users={{@model.admins}} @truncateAt={{10}} /> + </section> + {{/if}} + + {{#if @model.moderators.length}} + <section class="about__moderators"> + <h3>{{dIcon "users"}} {{i18n "about.our_moderators"}}</h3> + <AboutPageUsers @users={{@model.moderators}} @truncateAt={{10}} /> + </section> + {{/if}} </section> <section class="about__right-side"> <h3>{{i18n "about.contact"}}</h3> diff --git a/app/assets/javascripts/discourse/app/components/legacy-about-page-users.gjs b/app/assets/javascripts/discourse/app/components/legacy-about-page-users.gjs new file mode 100644 index 00000000000..10bbb49d431 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/legacy-about-page-users.gjs @@ -0,0 +1,14 @@ +import Component from "@glimmer/component"; +import AboutPageUser from "discourse/components/about-page-user"; + +export default class LegacyAboutPageUsers extends Component { + get users() { + return this.args.users || []; + } + + <template> + {{#each this.users as |user|}} + <AboutPageUser @user={{user}} /> + {{/each}} + </template> +} diff --git a/app/assets/javascripts/discourse/app/templates/about.hbs b/app/assets/javascripts/discourse/app/templates/about.hbs index 25bfe465e22..023ebf79d27 100644 --- a/app/assets/javascripts/discourse/app/templates/about.hbs +++ b/app/assets/javascripts/discourse/app/templates/about.hbs @@ -53,7 +53,7 @@ <section class="about admins"> <h3>{{d-icon "users"}} {{i18n "about.our_admins"}}</h3> <div class="users"> - <AboutPageUsers @users={{this.model.admins}} /> + <LegacyAboutPageUsers @users={{this.model.admins}} /> </div> </section> {{/if}} @@ -70,7 +70,7 @@ <section class="about moderators"> <h3>{{d-icon "users"}} {{i18n "about.our_moderators"}}</h3> <div class="users"> - <AboutPageUsers @users={{this.model.moderators}} /> + <LegacyAboutPageUsers @users={{this.model.moderators}} /> </div> </section> {{/if}} @@ -90,7 +90,7 @@ > <h3>{{category-link cm.category}}{{i18n "about.moderators"}}</h3> <div class="users"> - <AboutPageUsers @users={{cm.moderators}} /> + <LegacyAboutPageUsers @users={{cm.moderators}} /> </div> <div class="clearfix"></div> </section> diff --git a/app/assets/stylesheets/common/base/about.scss b/app/assets/stylesheets/common/base/about.scss index f6b878dfa91..47bc7663e10 100644 --- a/app/assets/stylesheets/common/base/about.scss +++ b/app/assets/stylesheets/common/base/about.scss @@ -37,6 +37,25 @@ &__activities-item-period { font-size: var(--font-down-2); } + + &__admins, + &__moderators { + margin-top: 3em; + + h3 { + margin-bottom: 1em; + } + } +} + +.about-page-users-list { + display: grid; + gap: 1em; + grid-template-columns: repeat(auto-fit, minmax(20em, 1fr)); + + &__expand-button { + width: 100%; + } } section.about { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a30b91bb6ae..5b927aaf0b1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -346,6 +346,8 @@ en: contact: "Contact us" contact_info: "In the event of a critical issue or urgent matter affecting this site, please contact us at %{contact_info}." site_activity: "Site activity" + view_more: "View more" + view_less: "View less" activities: topics: one: "%{formatted_number} topic" diff --git a/spec/system/about_page_spec.rb b/spec/system/about_page_spec.rb index a92553e27fd..50ff0aefe5e 100644 --- a/spec/system/about_page_spec.rb +++ b/spec/system/about_page_spec.rb @@ -171,5 +171,187 @@ describe "About page", type: :system do end end end + + describe "our admins section" do + before { User.update_all(last_seen_at: 1.month.ago) } + + fab!(:admins) { Fabricate.times(12, :admin) } + + it "displays only the 10 most recently seen admins when there are more than 10 admins" do + admins[0].update!(last_seen_at: 4.minutes.ago) + admins[1].update!(last_seen_at: 1.minutes.ago) + admins[2].update!(last_seen_at: 10.minutes.ago) + + about_page.visit + expect(about_page.admins_list).to have_expand_button + + displayed_admins = about_page.admins_list.users + expect(displayed_admins.size).to eq(10) + expect(displayed_admins.map { |u| u[:username] }.first(3)).to eq( + [admins[1].username, admins[0].username, admins[2].username], + ) + end + + it "allows expanding and collapsing the list of admins" do + about_page.visit + + displayed_admins = about_page.admins_list.users + expect(displayed_admins.size).to eq(10) + + expect(about_page.admins_list).to be_expandable + + about_page.admins_list.expand + + expect(about_page.admins_list).to be_collapsible + + displayed_admins = about_page.admins_list.users + expect(displayed_admins.size).to eq(13) # 12 fabricated for this spec group and 1 global + + about_page.admins_list.collapse + + expect(about_page.admins_list).to be_expandable + + displayed_admins = about_page.admins_list.users + expect(displayed_admins.size).to eq(10) + end + + it "doesn't show an expand/collapse button when there are fewer than 10 admins" do + User.where(id: admins.first(7).map(&:id)).destroy_all + + about_page.visit + + displayed_admins = about_page.admins_list.users + expect(displayed_admins.size).to eq(6) + expect(about_page.admins_list).to have_no_expand_button + end + + it "prioritizes names when prioritize_username_in_ux is false" do + SiteSetting.prioritize_username_in_ux = false + + about_page.visit + + displayed_admins = about_page.admins_list.users + admins = User.where(username: displayed_admins.map { |u| u[:username] }) + expect(displayed_admins.map { |u| u[:displayed_username] }).to contain_exactly( + *admins.pluck(:name), + ) + expect(displayed_admins.map { |u| u[:displayed_name] }).to contain_exactly( + *admins.pluck(:username), + ) + end + + it "prioritizes usernames when prioritize_username_in_ux is true" do + SiteSetting.prioritize_username_in_ux = true + + about_page.visit + + displayed_admins = about_page.admins_list.users + admins = User.where(username: displayed_admins.map { |u| u[:username] }) + expect(displayed_admins.map { |u| u[:displayed_username] }).to contain_exactly( + *admins.pluck(:username), + ) + expect(displayed_admins.map { |u| u[:displayed_name] }).to contain_exactly( + *admins.pluck(:name), + ) + end + + it "opens the user card when a user is clicked" do + about_page.visit + + about_page.admins_list.users.first[:node].click + expect(about_page).to have_css("#user-card") + end + end + + describe "our moderators section" do + before { User.update_all(last_seen_at: 1.month.ago) } + + fab!(:moderators) { Fabricate.times(13, :moderator) } + + it "displays only the 10 most recently seen moderators when there are more than 10 moderators" do + moderators[10].update!(last_seen_at: 5.hours.ago) + moderators[3].update!(last_seen_at: 2.hours.ago) + moderators[5].update!(last_seen_at: 13.hours.ago) + + about_page.visit + expect(about_page.moderators_list).to have_expand_button + + displayed_mods = about_page.moderators_list.users + expect(displayed_mods.size).to eq(10) + expect(displayed_mods.map { |u| u[:username] }.first(3)).to eq( + [moderators[3].username, moderators[10].username, moderators[5].username], + ) + end + + it "allows expanding and collapsing the list of moderators" do + about_page.visit + + displayed_mods = about_page.moderators_list.users + expect(displayed_mods.size).to eq(10) + + expect(about_page.moderators_list).to be_expandable + + about_page.moderators_list.expand + + expect(about_page.moderators_list).to be_collapsible + + displayed_mods = about_page.moderators_list.users + expect(displayed_mods.size).to eq(14) # 13 fabricated for this spec group and 1 global + + about_page.moderators_list.collapse + + expect(about_page.moderators_list).to be_expandable + + displayed_mods = about_page.moderators_list.users + expect(displayed_mods.size).to eq(10) + end + + it "doesn't show an expand/collapse button when there are fewer than 10 moderators" do + User.where(id: moderators.first(10).map(&:id)).destroy_all + + about_page.visit + + displayed_mods = about_page.moderators_list.users + expect(displayed_mods.size).to eq(4) + expect(about_page.moderators_list).to have_no_expand_button + end + + it "prioritizes names when prioritize_username_in_ux is false" do + SiteSetting.prioritize_username_in_ux = false + + about_page.visit + + displayed_mods = about_page.moderators_list.users + moderators = User.where(username: displayed_mods.map { |u| u[:username] }) + expect(displayed_mods.map { |u| u[:displayed_username] }).to contain_exactly( + *moderators.pluck(:name), + ) + expect(displayed_mods.map { |u| u[:displayed_name] }).to contain_exactly( + *moderators.pluck(:username), + ) + end + + it "prioritizes usernames when prioritize_username_in_ux is true" do + SiteSetting.prioritize_username_in_ux = true + + about_page.visit + + displayed_mods = about_page.moderators_list.users + moderators = User.where(username: displayed_mods.map { |u| u[:username] }) + expect(displayed_mods.map { |u| u[:displayed_username] }).to contain_exactly( + *moderators.pluck(:username), + ) + expect(displayed_mods.map { |u| u[:displayed_name] }).to contain_exactly( + *moderators.pluck(:name), + ) + end + + it "opens the user card when a user is clicked" do + about_page.visit + + about_page.moderators_list.users.last[:node].click + expect(about_page).to have_css("#user-card") + end + end end end diff --git a/spec/system/page_objects/components/about_page_users_list.rb b/spec/system/page_objects/components/about_page_users_list.rb new file mode 100644 index 00000000000..d0e17cc8da7 --- /dev/null +++ b/spec/system/page_objects/components/about_page_users_list.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module PageObjects + module Components + class AboutPageUsersList < PageObjects::Components::Base + attr_reader :container + + def initialize(container) + @container = container + end + + def has_expand_button? + container.has_css?(".about-page-users-list__expand-button") + end + + def has_no_expand_button? + container.has_no_css?(".about-page-users-list__expand-button") + end + + def expandable? + container.find(".about-page-users-list__expand-button").has_text?( + I18n.t("js.about.view_more"), + ) + end + + def collapsible? + container.find(".about-page-users-list__expand-button").has_text?( + I18n.t("js.about.view_less"), + ) + end + + def expand + container.find( + ".about-page-users-list__expand-button", + text: I18n.t("js.about.view_more"), + ).click + end + + def collapse + container.find( + ".about-page-users-list__expand-button", + text: I18n.t("js.about.view_less"), + ).click + end + + def users + container + .all(".user-info") + .map do |node| + { + username: node["data-username"], + displayed_username: node.find(".name-line .username").text, + displayed_name: node.find(".name-line .name").text, + node:, + } + end + end + end + end +end diff --git a/spec/system/page_objects/pages/about.rb b/spec/system/page_objects/pages/about.rb index fe59e6e89e5..9cd23ccdab2 100644 --- a/spec/system/page_objects/pages/about.rb +++ b/spec/system/page_objects/pages/about.rb @@ -50,6 +50,14 @@ module PageObjects PageObjects::Components::AboutPageSiteActivity.new(find(".about__activities")) end + def admins_list + PageObjects::Components::AboutPageUsersList.new(find(".about__admins")) + end + + def moderators_list + PageObjects::Components::AboutPageUsersList.new(find(".about__moderators")) + end + private def site_age_stat_element