DEV: Allow custom site activity items in the new /about page (#28400)

This commit introduces a new frontend API to add custom items to the "Site activity" section in the new /about page. The new API is called `addAboutPageActivity` and it works along side the `register_stat` serve-side API which serializes the data that the frontend API consumes. More details of how the two APIs work together is in the JSDoc comment above the API function definition.

Internal topic: t/128545/9.
This commit is contained in:
Osama Sayegh 2024-08-20 16:16:05 +03:00 committed by GitHub
parent ccb1861ada
commit db6eff7be9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 223 additions and 2 deletions

View File

@ -9,6 +9,16 @@ import i18n from "discourse-common/helpers/i18n";
import escape from "discourse-common/lib/escape";
import I18n from "discourse-i18n";
const pluginActivitiesFuncs = [];
export function addAboutPageActivity(name, func) {
pluginActivitiesFuncs.push({ name, func });
}
export function clearAboutPageActivities() {
pluginActivitiesFuncs.clear();
}
export default class AboutPage extends Component {
get moderatorsCount() {
return this.args.model.moderators.length;
@ -57,7 +67,7 @@ export default class AboutPage extends Component {
}
get siteActivities() {
return [
const list = [
{
icon: "scroll",
class: "topics",
@ -104,6 +114,8 @@ export default class AboutPage extends Component {
period: I18n.t("about.activities.periods.all_time"),
},
];
return list.concat(this.siteActivitiesFromPlugins());
}
get contactInfo() {
@ -139,6 +151,33 @@ export default class AboutPage extends Component {
}
}
siteActivitiesFromPlugins() {
const stats = this.args.model.stats;
const statKeys = Object.keys(stats);
const configs = [];
for (const { name, func } of pluginActivitiesFuncs) {
let present = false;
const periods = {};
for (const stat of statKeys) {
const prefix = `${name}_`;
if (stat.startsWith(prefix)) {
present = true;
const period = stat.replace(prefix, "");
periods[period] = stats[stat];
}
}
if (!present) {
continue;
}
const config = func(periods);
if (config) {
configs.push(config);
}
}
return configs;
}
<template>
<section class="about__header">
{{#if @model.banner_image}}

View File

@ -3,10 +3,11 @@
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
// using the format described at https://keepachangelog.com/en/1.0.0/.
export const PLUGIN_API_VERSION = "1.36.0";
export const PLUGIN_API_VERSION = "1.37.0";
import $ from "jquery";
import { h } from "virtual-dom";
import { addAboutPageActivity } from "discourse/components/about-page";
import { addBulkDropdownButton } from "discourse/components/bulk-select-topics-dropdown";
import {
addApiImageWrapperButtonClickEvent,
@ -3238,6 +3239,45 @@ class PluginApi {
registerAdminPluginConfigNav(pluginId, mode, links);
}
/**
* Adds a custom site activity item in the new /about page. Requires using
* the `register_stat` server-side API to serialize the needed data to the
* frontend.
*
* ```
* api.addAboutPageActivity("released_songs", (periods) => {
* return {
* icon: "guitar",
* class: "released-songs",
* activityText: `${periods["last_year"]} released songs`,
* period: "in the last year",
* };
* });
* ```
*
* The above example would require the `register_stat` server-side API to be
* used like this:
*
* ```ruby
* register_stat("released_songs", expose_via_api: true) do
* {
* last_year: Songs.where("released_at > ?", 1.year.ago).count,
* last_month: Songs.where("released_at > ?", 1.month.ago).count,
* }
* end
* ```
*
* @callback activityItemConfig
* @param {Object} periods - an object containing the periods that the block given to the `register_stat` server-side API returns.
* @returns {Object} - configuration object for the site activity item. The object must contain the following properties: `icon`, `class`, `activityText` and `period`.
*
* @param {string} name - a string that matches the string given to the `register_stat` server-side API.
* @param {activityItemConfig} func - a callback that returns an object containing properties for the custom site activity item.
*/
addAboutPageActivity(name, func) {
addAboutPageActivity(name, func);
}
#deprecatedHeaderWidgetOverride(widgetName, override) {
if (DEPRECATED_HEADER_WIDGETS.includes(widgetName)) {
this.container.lookup("service:header").anyWidgetHeaderOverrides = true;

View File

@ -12,6 +12,7 @@ import MessageBus from "message-bus-client";
import { resetCache as resetOneboxCache } from "pretty-text/oneboxer";
import QUnit, { module, skip, test } from "qunit";
import sinon from "sinon";
import { clearAboutPageActivities } from "discourse/components/about-page";
import {
cleanUpComposerUploadHandler,
cleanUpComposerUploadMarkdownResolver,
@ -251,6 +252,7 @@ export function testCleanup(container, app) {
resetAdminPluginConfigNav();
resetTransformers();
rollbackAllPrepends();
clearAboutPageActivities();
}
function cleanupCssGeneratorTags() {

View File

@ -0,0 +1,64 @@
import { render } from "@ember/test-helpers";
import { module, test } from "qunit";
import AboutPage from "discourse/components/about-page";
import { withPluginApi } from "discourse/lib/plugin-api";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
function createModelObject({
title = "My Forums",
admins = [],
moderators = [],
stats = {},
}) {
return {
title,
admins,
moderators,
stats,
};
}
module("Integration | Component | about-page", function (hooks) {
setupRenderingTest(hooks);
test("custom site activities registered via the plugin API", async function (assert) {
withPluginApi("1.37.0", (api) => {
api.addAboutPageActivity("my_custom_activity", (periods) => {
return {
icon: "eye",
class: "custom-activity",
activityText: `${periods["3_weeks"]} my custom activity`,
period: "in the last 3 weeks",
};
});
api.addAboutPageActivity("another_custom_activity", () => null);
});
const model = createModelObject({
stats: {
my_custom_activity_3_weeks: 342,
my_custom_activity_1_year: 123,
another_custom_activity_1_day: 994,
},
});
await render(<template><AboutPage @model={{model}} /></template>);
assert
.dom(".about__activities-item.custom-activity")
.exists("my_custom_activity is rendered");
assert
.dom(".about__activities-item.custom-activity .d-icon-eye")
.exists("icon for my_custom_activity is rendered");
assert
.dom(
".about__activities-item.custom-activity .about__activities-item-count"
)
.hasText("342 my custom activity");
assert
.dom(
".about__activities-item.custom-activity .about__activities-item-period"
)
.hasText("in the last 3 weeks");
});
});

View File

@ -7,6 +7,10 @@ in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.37.0] - 2024-08-19
- Added `addAboutPageActivity` which allows plugins/TCs to register a custom site activity item in the new /about page. Requires the server-side `register_stat` plugin API.
## [1.36.0] - 2024-08-06
- Added `addLogSearchLinkClickedCallbacks` which allows plugins/TCs to register a callback when a search link is clicked and before a search log is created

View File

@ -1121,6 +1121,9 @@ class Plugin::Instance
# group of stats is shown on the site About page in the Site Statistics
# table. Some stats may be needed purely for reporting purposes and thus
# do not need to be shown in the UI to admins/users.
#
# TODO(osama): deprecate show_in_ui when experimental_redesigned_about_page_groups
# is removed
def register_stat(name, show_in_ui: false, expose_via_api: false, &block)
# We do not want to register and display the same group multiple times.
return if DiscoursePluginRegistry.stats.any? { |stat| stat.name == name }

View File

@ -1,3 +1,4 @@
import { number } from "discourse/lib/formatter";
import { withPluginApi } from "discourse/lib/plugin-api";
import { getOwnerWithFallback } from "discourse-common/lib/get-owner";
import { replaceIcon } from "discourse-common/lib/icon-library";
@ -163,6 +164,21 @@ export default {
document.body.classList.remove("chat-drawer-active");
}
});
api.addAboutPageActivity("chat_messages", (periods) => {
const count = periods["7_days"];
if (count) {
return {
icon: "comment-dots",
class: "chat-messages",
activityText: I18n.t("about.activities.chat_messages", {
count,
formatted_number: number(count),
}),
period: I18n.t("about.activities.periods.last_7_days"),
};
}
});
});
},

View File

@ -33,6 +33,10 @@ en:
chat_messages_count: "Chat messages"
chat_channels_count: "Chat channels"
chat_users_count: "Chat users"
activities:
chat_messages:
one: "%{formatted_number} chat message"
other: "%{formatted_number} chat messages"
chat:
text_copied: Text copied to clipboard

View File

@ -18,6 +18,7 @@ register_asset "stylesheets/mobile/index.scss", :mobile
register_svg_icon "comments"
register_svg_icon "comment-slash"
register_svg_icon "comment-dots"
register_svg_icon "lock"
register_svg_icon "clipboard"
register_svg_icon "file-audio"

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
describe "Chat messages site activity in the about page", type: :system do
fab!(:current_user) { Fabricate(:user) }
fab!(:group) { Fabricate(:group, users: [current_user]) }
let(:about_page) { PageObjects::Pages::About.new }
before do
chat_system_bootstrap
SiteSetting.experimental_redesigned_about_page_groups = group.id.to_s
sign_in(current_user)
Fabricate(:chat_message, created_at: 5.hours.ago)
Fabricate(:chat_message, created_at: 2.days.ago)
Fabricate(:chat_message, created_at: 6.days.ago)
Fabricate(:chat_message, created_at: 9.days.ago)
end
it "displays the number of chat messages in the last 7 days" do
about_page.visit
expect(about_page.site_activities.custom("chat-messages")).to have_custom_count(
I18n.t("js.about.activities.chat_messages", count: 3, formatted_number: "3"),
)
expect(about_page.site_activities.custom("chat-messages")).to have_custom_period(
I18n.t("js.about.activities.periods.last_7_days"),
)
end
end

View File

@ -43,6 +43,14 @@ module PageObjects
translation_key: "about.activities.likes",
)
end
# used by plugins
def custom(name, translation_key: nil)
AboutPageSiteActivityItem.new(
container.find(".about__activities-item.#{name}"),
translation_key:,
)
end
end
end
end

View File

@ -28,6 +28,16 @@ module PageObjects
period_element.has_text?(I18n.t("js.about.activities.periods.all_time"))
end
# used by plugins
def has_custom_count?(text)
container.find(".about__activities-item-count").has_text?(text)
end
# used by plugins
def has_custom_period?(text)
period_element.has_text?(text)
end
private
def period_element