DEV: Port the about page extra groups functionality into core (#32659)

We want to merge the theme component that allows admins to display extra groups on the about page. The settings for this are now under About your site.

All the code is lift-and-shift, with some minor adjustments, e.g. theme components can't use the group_list setting type, but it has been converted to that here.

Also the system tests for the admin controls are new.

This whole thing is gated behind a hidden site setting to avoid double rendering while we deprecate the theme component.
This commit is contained in:
Ted Johansson
2025-05-14 09:44:25 +08:00
committed by GitHub
parent 404e2598b3
commit a445e8cce5
15 changed files with 534 additions and 3 deletions

View File

@ -0,0 +1,126 @@
import Component from "@glimmer/component";
import { cached } from "@glimmer/tracking";
import { action } from "@ember/object";
import { service } from "@ember/service";
import Form from "discourse/components/form";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { i18n } from "discourse-i18n";
import GroupChooser from "select-kit/components/group-chooser";
export default class AdminConfigAreasAboutExtraGroups extends Component {
@service site;
@service toasts;
@cached
get data() {
return {
aboutPageExtraGroups:
this.args.extraGroups.aboutPageExtraGroups.value
.split("|")
.map(Number) || [],
aboutPageExtraGroupsInitialMembers:
this.args.extraGroups.aboutPageExtraGroupsInitialMembers.value,
aboutPageExtraGroupsOrder:
this.args.extraGroups.aboutPageExtraGroupsOrder.value,
aboutPageExtraGroupsShowDescription:
this.args.extraGroups.aboutPageExtraGroupsShowDescription.value ===
"true",
};
}
@action
async save(data) {
this.args.setGlobalSavingStatus(true);
try {
await ajax("/admin/config/about.json", {
type: "PUT",
data: {
extra_groups: {
groups: data.aboutPageExtraGroups.join("|"),
initial_members: data.aboutPageExtraGroupsInitialMembers,
order: data.aboutPageExtraGroupsOrder,
show_description: data.aboutPageExtraGroupsShowDescription,
},
},
});
this.toasts.success({
duration: 30000,
data: {
message: i18n("admin.config_areas.about.toasts.extra_groups_saved"),
},
});
} catch (err) {
popupAjaxError(err);
} finally {
this.args.setGlobalSavingStatus(false);
}
}
get orderings() {
return this.args.extraGroups.aboutPageExtraGroupsOrder.choices;
}
<template>
<Form @data={{this.data}} @onSubmit={{this.save}} as |form|>
<form.Field
@name="aboutPageExtraGroups"
@title={{i18n "admin.config_areas.about.extra_groups.groups"}}
@format="large"
as |field|
>
<field.Custom>
<GroupChooser
@content={{this.site.groups}}
@value={{field.value}}
@onChange={{field.set}}
/>
</field.Custom>
</form.Field>
<form.Field
@name="aboutPageExtraGroupsInitialMembers"
@title={{i18n "admin.config_areas.about.extra_groups.initial_members"}}
@description={{i18n
"admin.config_areas.about.extra_groups.initial_members_description"
}}
@validation="required"
@format="large"
as |field|
>
<field.Input @type="number" />
</form.Field>
<form.Field
@name="aboutPageExtraGroupsOrder"
@title={{i18n "admin.config_areas.about.extra_groups.order"}}
@validation="required"
@format="large"
as |field|
>
<field.Select as |select|>
{{#each this.orderings as |ordering|}}
<select.Option @value={{ordering}}>
{{ordering}}
</select.Option>
{{/each}}
</field.Select>
</form.Field>
<form.Field
@name="aboutPageExtraGroupsShowDescription"
@title={{i18n "admin.config_areas.about.extra_groups.show_description"}}
@validation="required"
@format="large"
as |field|
>
<field.Checkbox />
</form.Field>
<form.Submit
@label="admin.config_areas.about.update"
@disabled={{@globalSavingStatus}}
/>
</Form>
</template>
}

View File

@ -1,12 +1,16 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { service } from "@ember/service";
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
import AdminConfigAreasAboutContactInformation from "admin/components/admin-config-area-cards/about/contact-information";
import AdminConfigAreasAboutExtraGroups from "admin/components/admin-config-area-cards/about/extra-groups";
import AdminConfigAreasAboutGeneralSettings from "admin/components/admin-config-area-cards/about/general-settings";
import AdminConfigAreasAboutYourOrganization from "admin/components/admin-config-area-cards/about/your-organization";
export default class AdminConfigAreasAbout extends Component {
@service siteSettings;
@tracked saving = false;
get generalSettings() {
@ -39,6 +43,27 @@ export default class AdminConfigAreasAbout extends Component {
};
}
get extraGroups() {
return {
aboutPageExtraGroups: this.#lookupSettingFromData(
"about_page_extra_groups"
),
aboutPageExtraGroupsInitialMembers: this.#lookupSettingFromData(
"about_page_extra_groups_initial_members"
),
aboutPageExtraGroupsOrder: this.#lookupSettingFromData(
"about_page_extra_groups_order"
),
aboutPageExtraGroupsShowDescription: this.#lookupSettingFromData(
"about_page_extra_groups_show_description"
),
};
}
get showExtraGroups() {
return this.siteSettings.show_additional_about_groups === true;
}
@action
setSavingStatus(status) {
this.saving = status;
@ -91,6 +116,22 @@ export default class AdminConfigAreasAbout extends Component {
/>
</:content>
</AdminConfigAreaCard>
{{#if this.showExtraGroups}}
<AdminConfigAreaCard
@heading="admin.config_areas.about.extra_groups.heading"
@description="admin.config_areas.about.extra_groups.description"
@collapsable={{true}}
class="admin-config-area-about__extra-groups-section"
>
<:content>
<AdminConfigAreasAboutExtraGroups
@extraGroups={{this.extraGroups}}
@setGlobalSavingStatus={{this.setSavingStatus}}
@globalSavingStatus={{this.saving}}
/>
</:content>
</AdminConfigAreaCard>
{{/if}}
</div>
</div>
</template>

View File

@ -0,0 +1,129 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import AboutPageUsers from "discourse/components/about-page-users";
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
import { ajax } from "discourse/lib/ajax";
export default class AboutPageExtraGroups extends Component {
@service store;
@service site;
@service siteSettings;
@tracked groups = [];
@tracked loading = false;
constructor() {
super(...arguments);
this.loadGroups();
}
groupName(group) {
return group.full_name || group.name.replace(/[_-]/g, " ");
}
@action
async loadGroups() {
this.loading = true;
try {
const groupsSetting =
this.siteSettings.about_page_extra_groups?.split("|").map(Number) || [];
let groupsToFetch = this.site.groups.filter((group) =>
groupsSetting.includes(group.id)
);
// ordered alphabetically by default
if (
this.siteSettings.about_page_extra_groups_order === "order of creation"
) {
groupsToFetch.sort((a, b) => a.id - b.id);
}
const groupPromises = groupsToFetch.map(async (group) => {
try {
const groupDetails = await this.loadGroupDetails(group.name);
group.members = await this.loadGroupMembers(group.name);
Object.assign(group, groupDetails);
return group;
} catch (error) {
// eslint-disable-next-line no-console
console.error(
`Error loading members for group ${group.name}:`,
error
);
return null;
}
});
const groupsWithMembers = (await Promise.all(groupPromises)).filter(
(group) => group && group.members.length > 0
);
this.groups = groupsWithMembers;
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error loading groups:", error);
this.groups = [];
} finally {
this.loading = false;
}
}
async loadGroupDetails(groupName) {
try {
const response = await ajax(`/g/${groupName}`);
return response.group;
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Error loading details for group ${groupName}:`, error);
return "";
}
}
async loadGroupMembers(groupName) {
try {
const response = await ajax(`/g/${groupName}/members?asc=true`);
return response.members || [];
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Error loading members for group ${groupName}:`, error);
return [];
}
}
get showGroupDescription() {
return this.siteSettings.about_page_extra_groups_show_description;
}
get showInitialMembers() {
return this.siteSettings.about_page_extra_groups_initial_members;
}
<template>
<ConditionalLoadingSpinner @condition={{this.loading}}>
{{#if this.groups}}
{{#each this.groups as |group|}}
<section
class="about__{{group.name}}
--custom-group
{{if this.showGroupDescription '--has-description'}}"
>
<h3>
<a href="/g/{{group.name}}">{{this.groupName group}}</a>
</h3>
{{#if this.showGroupDescription}}
<p>{{htmlSafe group.bio_cooked}}</p>
{{/if}}
<AboutPageUsers
@users={{group.members}}
@truncateAt={{this.showInitialMembers}}
/>
</section>
{{/each}}
{{/if}}
</ConditionalLoadingSpinner>
</template>
}

View File

@ -3,6 +3,8 @@ import { hash } from "@ember/helper";
import { LinkTo } from "@ember/routing";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { isBlank } from "@ember/utils";
import AboutPageExtraGroups from "discourse/components/about-page-extra-groups";
import AboutPageUsers from "discourse/components/about-page-users";
import PluginOutlet from "discourse/components/plugin-outlet";
import icon from "discourse/helpers/d-icon";
@ -230,6 +232,13 @@ export default class AboutPage extends Component {
return configs;
}
get showExtraGroups() {
return (
this.siteSettings.show_additional_about_groups === true &&
!isBlank(this.siteSettings.about_page_extra_groups)
);
}
<template>
{{#if this.currentUser.admin}}
<p>
@ -294,6 +303,9 @@ export default class AboutPage extends Component {
@connectorTagName="section"
@outletArgs={{hash model=@model}}
/>
{{#if this.showExtraGroups}}
<AboutPageExtraGroups />
{{/if}}
</div>
<div class="about__right-side">

View File

@ -6,6 +6,36 @@
flex-wrap: wrap;
gap: 2em;
max-width: 1100px;
.--custom-group {
max-width: unset;
margin-top: 3em;
h3 {
a {
color: var(--primary);
&:hover {
color: var(--tertiary);
}
}
&::first-letter {
text-transform: capitalize;
}
}
p {
margin-top: 0;
color: var(--primary-high);
}
&.--has-description {
h3 {
margin-bottom: 0;
}
}
}
}
&__header {

View File

@ -57,6 +57,19 @@ class Admin::Config::AboutController < Admin::AdminController
}
end
if extra_groups = params[:extra_groups]
settings << { setting_name: "about_page_extra_groups", value: extra_groups[:groups] }
settings << {
setting_name: "about_page_extra_groups_initial_members",
value: extra_groups[:initial_members],
}
settings << { setting_name: "about_page_extra_groups_order", value: extra_groups[:order] }
settings << {
setting_name: "about_page_extra_groups_show_description",
value: extra_groups[:show_description],
}
end
SiteSetting::Update.call(
guardian:,
params: {

View File

@ -6228,12 +6228,21 @@ en:
city_for_disputes_placeholder: "City"
city_for_disputes_help: |
Specify the city for resolving legal disputes related to this forum.
extra_groups:
heading: "Group listing"
description: "Extra groups to show on the About page."
groups: "Groups"
order: "Sort order"
initial_members: "Expanded members"
initial_members_description: "Number of group members to display before adding a View more button."
show_description: "Include description"
optional: "(optional)"
update: "Update"
toasts:
general_settings_saved: "General settings saved"
contact_information_saved: "Contact information saved"
your_organization_saved: "Your organization saved"
extra_groups_saved: "Group listing saved"
saved: "saved!"
flags:
edit_header: "Edit Flag"

View File

@ -2765,6 +2765,10 @@ en:
page_loading_indicator: "Configure the loading indicator which appears during page navigations within Discourse. 'Spinner' is a full page indicator. 'Slider' shows a narrow bar at the top of the screen."
show_user_menu_avatars: "Show user avatars in the user menu"
about_page_hidden_groups: "Do not show members of specific groups on the /about page."
about_page_extra_groups: "Groups to show on the about page below the moderators."
about_page_extra_groups_initial_members: "Number of members to expand on each group on the about page."
about_page_extra_groups_show_description: "Whether to include the group descriptions on the about page."
about_page_extra_groups_order: "Ordering of the extra groups on the about page."
adobe_analytics_tags_url: "Adobe Analytics tags URL (`https://assets.adobedtm.com/...`)"
view_raw_email_allowed_groups: "Groups which can view the raw email content of a post if it was created by an incoming email. This includes email headers and other technical information."
experimental_content_localization: "Displays localized content for users based on their language preferences. Such content may include categories, tags, posts, and topics. This feature is under heavy development."

View File

@ -505,6 +505,29 @@ basic:
default: ""
type: group_list
area: "group_permissions"
about_page_extra_groups:
client: true
default: ""
type: group_list
area: "about"
about_page_extra_groups_initial_members:
client: true
default: 6
type: integer
area: "about"
about_page_extra_groups_order:
client: true
default: "alphabetically"
type: enum
choices:
- "alphabetically"
- "order of creation"
area: "about"
about_page_extra_groups_show_description:
client: true
default: false
type: bool
area: "about"
adobe_analytics_tags_url:
default: ""
regex: "assets.adobedtm.com"

View File

@ -374,6 +374,42 @@ describe "About page", type: :system do
end
end
describe "extra groups" do
let!(:extra_group) { Fabricate(:group, name: "illuminati") }
before do
SiteSetting.about_banner_image = nil
SiteSetting.show_additional_about_groups = true
SiteSetting.about_page_extra_groups = extra_groups_setting
extra_group.users << Fabricate(:user)
end
context "when extra groups are configured" do
let(:extra_groups_setting) { extra_group.id.to_s }
it "shows the extra groups" do
sign_in(admin)
about_page.visit
expect(about_page).to have_group_with_name("Illuminati")
end
end
context "when no extra groups are configured" do
let(:extra_groups_setting) { "" }
it "shows no extra groups" do
sign_in(admin)
about_page.visit
expect(about_page).to have_no_extra_groups
end
end
end
describe "the edit link" do
it "appears for admins" do
sign_in(admin)

View File

@ -6,7 +6,15 @@ describe "Admin About Config Area Page", type: :system do
let(:config_area) { PageObjects::Pages::AdminAboutConfigArea.new }
before { sign_in(admin) }
let!(:extra_group_1) { Fabricate(:group, name: "extra1") }
let!(:extra_group_2) { Fabricate(:group, name: "extra2") }
let!(:extra_group_3) { Fabricate(:group, name: "extra3") }
before do
SiteSetting.show_additional_about_groups = true
sign_in(admin)
end
context "when all fields have existing values" do
before do
@ -25,6 +33,11 @@ describe "Admin About Config Area Page", type: :system do
SiteSetting.company_name = "kitty company inc."
SiteSetting.governing_law = "kitty jurisdiction"
SiteSetting.city_for_disputes = "no disputes allowed"
SiteSetting.about_page_extra_groups = "#{extra_group_1.id}|#{extra_group_2.id}"
SiteSetting.about_page_extra_groups_initial_members = 5
SiteSetting.about_page_extra_groups_order = "order of creation"
SiteSetting.about_page_extra_groups_show_description = true
end
it "populates all input fields correctly" do
@ -67,6 +80,13 @@ describe "Admin About Config Area Page", type: :system do
expect(config_area.your_organization_section.city_for_disputes_input.value).to eq(
"no disputes allowed",
)
expect(config_area.group_listing_section.groups_input.value).to eq(
"#{extra_group_1.id},#{extra_group_2.id}",
)
expect(config_area.group_listing_section.initial_members_input.value).to eq("5")
expect(config_area.group_listing_section.order_input.value).to eq("order of creation")
expect(config_area.group_listing_section.show_description_input.value).to eq(true)
end
end
@ -215,4 +235,23 @@ describe "Admin About Config Area Page", type: :system do
expect(SiteSetting.city_for_disputes).to eq("teeb el shouq")
end
end
describe "the group listing card" do
it "can saves its fields to their corresponding site settings" do
config_area.visit
config_area.group_listing_section.groups_input.select("extra3")
config_area.group_listing_section.initial_members_input.fill_in("3")
config_area.group_listing_section.order_input.select("alphabetically")
config_area.group_listing_section.show_description_input.uncheck
config_area.group_listing_section.submit
expect(config_area.group_listing_section).to have_saved_successfully
expect(SiteSetting.about_page_extra_groups).to include(extra_group_3.id.to_s)
expect(SiteSetting.about_page_extra_groups_initial_members).to eq(3)
expect(SiteSetting.about_page_extra_groups_order).to eq("alphabetically")
expect(SiteSetting.about_page_extra_groups_show_description).to eq(false)
end
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module PageObjects
module Components
class AdminAboutConfigAreaGroupListingCard < PageObjects::Components::Base
def groups_input
form.field("aboutPageExtraGroups")
end
def initial_members_input
form.field("aboutPageExtraGroupsInitialMembers")
end
def order_input
form.field("aboutPageExtraGroupsOrder")
end
def show_description_input
form.field("aboutPageExtraGroupsShowDescription")
end
def submit
form.submit
end
def has_saved_successfully?
PageObjects::Components::Toasts.new.has_success?(
I18n.t("admin_js.admin.config_areas.about.toasts.extra_groups_saved"),
)
end
def form
PageObjects::Components::FormKit.new(
".admin-config-area-about__extra-groups-section .form-kit",
)
end
end
end
end

View File

@ -33,7 +33,7 @@ module PageObjects
case control_type
when /input-/, "password"
component.find("input").value
when "icon"
when "icon", "multi-select"
picker = PageObjects::Components::SelectKit.new(component)
picker.value
when "checkbox"
@ -84,7 +84,15 @@ module PageObjects
end
def control_type
component["data-control-type"]
type = component["data-control-type"]
return type if type != "custom"
if component.has_css?(".multi-select")
"multi-select"
else
raise "Unknown custom control"
end
end
def toggle
@ -121,6 +129,12 @@ module PageObjects
picker.expand
picker.search(value)
picker.select_row_by_value(value)
when "multi-select"
selector = component.find(".form-kit__control-custom > .multi-select")["id"]
picker = PageObjects::Components::SelectKit.new("#" + selector)
picker.expand
picker.search(value)
picker.select_row_by_name(value)
when "select"
PageObjects::Components::DSelect.new(component.find(".form-kit__control-select")).select(
value,

View File

@ -38,6 +38,18 @@ module PageObjects
element.has_text?(I18n.t("js.about.moderator_count", count:, formatted_number:))
end
def has_group_with_name?(name)
has_css?(".about__#{name.downcase} h3", text: name)
end
def has_no_group_with_name?(name)
has_no_css?(".about__#{name.downcase} h3", text: name)
end
def has_no_extra_groups?
has_no_css?("--custom-group")
end
def has_site_created_less_than_1_month_ago?
site_age_stat_element.has_text?(I18n.t("js.about.site_age.less_than_one_month"))
end

View File

@ -18,6 +18,10 @@ module PageObjects
def your_organization_section
PageObjects::Components::AdminAboutConfigAreaYourOrganizationCard.new
end
def group_listing_section
PageObjects::Components::AdminAboutConfigAreaGroupListingCard.new
end
end
end
end