diff --git a/app/assets/javascripts/admin/addon/components/admin-config-area-cards/about/extra-groups.gjs b/app/assets/javascripts/admin/addon/components/admin-config-area-cards/about/extra-groups.gjs new file mode 100644 index 00000000000..c36398133a4 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-config-area-cards/about/extra-groups.gjs @@ -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; + } + + + + + + + + + + + + + + + + {{#each this.orderings as |ordering|}} + + {{ordering}} + + {{/each}} + + + + + + + + + + +} diff --git a/app/assets/javascripts/admin/addon/components/admin-config-areas/about.gjs b/app/assets/javascripts/admin/addon/components/admin-config-areas/about.gjs index 02be84f8da1..7254a6d7e7e 100644 --- a/app/assets/javascripts/admin/addon/components/admin-config-areas/about.gjs +++ b/app/assets/javascripts/admin/addon/components/admin-config-areas/about.gjs @@ -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 { /> + {{#if this.showExtraGroups}} + + <:content> + + + + {{/if}} diff --git a/app/assets/javascripts/discourse/app/components/about-page-extra-groups.gjs b/app/assets/javascripts/discourse/app/components/about-page-extra-groups.gjs new file mode 100644 index 00000000000..662445f1741 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/about-page-extra-groups.gjs @@ -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; + } + + + + {{#if this.groups}} + {{#each this.groups as |group|}} + + + {{this.groupName group}} + + {{#if this.showGroupDescription}} + {{htmlSafe group.bio_cooked}} + {{/if}} + + + {{/each}} + {{/if}} + + +} diff --git a/app/assets/javascripts/discourse/app/components/about-page.gjs b/app/assets/javascripts/discourse/app/components/about-page.gjs index 22bc68054da..bcbd56f3621 100644 --- a/app/assets/javascripts/discourse/app/components/about-page.gjs +++ b/app/assets/javascripts/discourse/app/components/about-page.gjs @@ -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) + ); + } + {{#if this.currentUser.admin}} @@ -294,6 +303,9 @@ export default class AboutPage extends Component { @connectorTagName="section" @outletArgs={{hash model=@model}} /> + {{#if this.showExtraGroups}} + + {{/if}} diff --git a/app/assets/stylesheets/common/base/about.scss b/app/assets/stylesheets/common/base/about.scss index 9ae8946de6a..dddcbbb4714 100644 --- a/app/assets/stylesheets/common/base/about.scss +++ b/app/assets/stylesheets/common/base/about.scss @@ -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 { diff --git a/app/controllers/admin/config/about_controller.rb b/app/controllers/admin/config/about_controller.rb index e760333f0b7..e81fcf49618 100644 --- a/app/controllers/admin/config/about_controller.rb +++ b/app/controllers/admin/config/about_controller.rb @@ -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: { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 764086b2a5c..60893327597 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 46fcf2ee011..ee76fc80b07 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -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." diff --git a/config/site_settings.yml b/config/site_settings.yml index 05c8f7495b7..61b10f2682c 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -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" diff --git a/spec/system/about_page_spec.rb b/spec/system/about_page_spec.rb index ccd7decf4c7..0d33a15c544 100644 --- a/spec/system/about_page_spec.rb +++ b/spec/system/about_page_spec.rb @@ -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) diff --git a/spec/system/admin_about_config_area_spec.rb b/spec/system/admin_about_config_area_spec.rb index b5dea1e7cff..8a0ae57ee63 100644 --- a/spec/system/admin_about_config_area_spec.rb +++ b/spec/system/admin_about_config_area_spec.rb @@ -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 diff --git a/spec/system/page_objects/components/admin_about_config_area_group_listing_card.rb b/spec/system/page_objects/components/admin_about_config_area_group_listing_card.rb new file mode 100644 index 00000000000..0c160df9443 --- /dev/null +++ b/spec/system/page_objects/components/admin_about_config_area_group_listing_card.rb @@ -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 diff --git a/spec/system/page_objects/components/form_kit.rb b/spec/system/page_objects/components/form_kit.rb index e5d6bc00e22..32f79aa9014 100644 --- a/spec/system/page_objects/components/form_kit.rb +++ b/spec/system/page_objects/components/form_kit.rb @@ -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, diff --git a/spec/system/page_objects/pages/about.rb b/spec/system/page_objects/pages/about.rb index 63f16ad6625..9176132ca93 100644 --- a/spec/system/page_objects/pages/about.rb +++ b/spec/system/page_objects/pages/about.rb @@ -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 diff --git a/spec/system/page_objects/pages/admin_about_config_area.rb b/spec/system/page_objects/pages/admin_about_config_area.rb index 5ace2733125..2bf83bb7def 100644 --- a/spec/system/page_objects/pages/admin_about_config_area.rb +++ b/spec/system/page_objects/pages/admin_about_config_area.rb @@ -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
{{htmlSafe group.bio_cooked}}
@@ -294,6 +303,9 @@ export default class AboutPage extends Component { @connectorTagName="section" @outletArgs={{hash model=@model}} /> + {{#if this.showExtraGroups}} + + {{/if}}