mirror of
https://github.com/discourse/discourse.git
synced 2026-09-05 04:40:41 -05:00
FEATURE: add plugin API for homepage options
This commit is contained in:
@@ -10,7 +10,14 @@ class HomepageSiteSetting < EnumSiteSetting
|
||||
def self.values
|
||||
# A blank value means the homepage is derived from the first top_menu item.
|
||||
[{ name: "admin.homepage.top_menu_default", value: "" }] +
|
||||
TopMenu.homepage_choices.map { |f| { name: "filters.#{f}.title", value: f } }
|
||||
TopMenu.homepage_choices.map { |f| { name: "filters.#{f}.title", value: f } } +
|
||||
DiscoursePluginRegistry.homepage_options.map do |option|
|
||||
{ name: option[:name], value: option[:id] }
|
||||
end
|
||||
end
|
||||
|
||||
def self.choices
|
||||
values.filter_map { |entry| entry[:value].presence }
|
||||
end
|
||||
|
||||
def self.translate_names?
|
||||
|
||||
@@ -191,7 +191,7 @@ class SiteSetting < ActiveRecord::Base
|
||||
def self.homepage
|
||||
configured = default_homepage.presence
|
||||
|
||||
if configured && TopMenu.homepage_choices.include?(configured)
|
||||
if configured && HomepageSiteSetting.choices.include?(configured)
|
||||
configured
|
||||
else
|
||||
top_menu_items[0].name
|
||||
@@ -204,6 +204,11 @@ class SiteSetting < ActiveRecord::Base
|
||||
|
||||
def self.anonymous_homepage
|
||||
return homepage if anonymous_menu_items.include?(homepage)
|
||||
if DiscoursePluginRegistry.homepage_options.any? { |option|
|
||||
option[:id] == homepage && option[:anonymous]
|
||||
}
|
||||
return homepage
|
||||
end
|
||||
|
||||
top_menu_items
|
||||
.map { |item| item.name }
|
||||
|
||||
@@ -13,6 +13,7 @@ class SiteSerializer < ApplicationSerializer
|
||||
:filters,
|
||||
:anonymous_list_filters,
|
||||
:homepage_choices,
|
||||
:homepage_options,
|
||||
:periods,
|
||||
:top_menu_items,
|
||||
:anonymous_top_menu_items,
|
||||
@@ -230,7 +231,11 @@ class SiteSerializer < ApplicationSerializer
|
||||
end
|
||||
|
||||
def homepage_choices
|
||||
TopMenu.homepage_choices
|
||||
HomepageSiteSetting.choices
|
||||
end
|
||||
|
||||
def homepage_options
|
||||
DiscoursePluginRegistry.homepage_options.map { |option| option.slice(:id, :path, :server_side) }
|
||||
end
|
||||
|
||||
def periods
|
||||
|
||||
@@ -1896,6 +1896,11 @@ Discourse::Application.routes.draw do
|
||||
as: "list_#{filter}"
|
||||
end
|
||||
|
||||
DiscoursePluginRegistry._raw_homepage_options.each do |registration|
|
||||
option = registration[:value]
|
||||
get "/", to: option[:route], constraints: HomePageConstraint.new(option[:id])
|
||||
end
|
||||
|
||||
get "/t/:topic_id/view-stats.json" => "topic_view_stats#index"
|
||||
|
||||
# special case for categories
|
||||
|
||||
@@ -7,7 +7,7 @@ import GroupFlairVisibilityWarning from "discourse/components/group-flair-visibi
|
||||
import GroupDefaultNotificationsModal from "discourse/components/modal/group-default-notifications";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { GROUP_VISIBILITY_LEVELS } from "discourse/lib/constants";
|
||||
import { defaultHomepage } from "discourse/lib/utilities";
|
||||
import { homepageNavigationDestination } from "discourse/lib/homepage-router-overrides";
|
||||
import { or } from "discourse/truth-helpers";
|
||||
import DButton from "discourse/ui-kit/d-button";
|
||||
import { i18n } from "discourse-i18n";
|
||||
@@ -85,7 +85,7 @@ export default class GroupManageSaveButton extends Component {
|
||||
await group.save(opts);
|
||||
|
||||
if (lostAccess) {
|
||||
this.router.transitionTo(`discovery.${defaultHomepage()}`);
|
||||
this.router.transitionTo(homepageNavigationDestination());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defaultHomepage } from "discourse/lib/utilities";
|
||||
import Site from "discourse/models/site";
|
||||
|
||||
/**
|
||||
* We want / to display one of our discovery routes/controllers, but we don't
|
||||
@@ -28,13 +29,55 @@ export const homepageRewriteParam = "_discourse_homepage_rewrite";
|
||||
* We watch for this, and then perform the rewrite in the router.
|
||||
*/
|
||||
export function homepageDestination() {
|
||||
return `/${defaultHomepage()}?${homepageRewriteParam}=1`;
|
||||
if (serverSideHomepage()) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
return `${homepagePath()}?${homepageRewriteParam}=1`;
|
||||
}
|
||||
|
||||
export function homepageNavigationDestination() {
|
||||
const option = registeredHomepageOption();
|
||||
|
||||
if (!option) {
|
||||
return `discovery.${defaultHomepage()}`;
|
||||
}
|
||||
|
||||
return option.server_side ? "/" : option.path;
|
||||
}
|
||||
|
||||
export function homepagePreviewDestination() {
|
||||
const option = registeredHomepageOption();
|
||||
|
||||
if (!option) {
|
||||
return `discovery.${defaultHomepage()}`;
|
||||
}
|
||||
|
||||
return option.server_side ? "discovery.latest" : option.path;
|
||||
}
|
||||
|
||||
export function homepagePath() {
|
||||
const homepage = defaultHomepage();
|
||||
const option = registeredHomepageOption();
|
||||
|
||||
return option?.path || `/${homepage}`;
|
||||
}
|
||||
|
||||
export function serverSideHomepage() {
|
||||
return registeredHomepageOption()?.server_side === true;
|
||||
}
|
||||
|
||||
function registeredHomepageOption() {
|
||||
const homepage = defaultHomepage();
|
||||
|
||||
return Site.current()?.homepage_options?.find(({ id }) => id === homepage);
|
||||
}
|
||||
|
||||
function rewriteIfNeeded(url, transition) {
|
||||
const intentUrl = transition?.intent?.url;
|
||||
if (
|
||||
intentUrl?.startsWith(homepageDestination()) ||
|
||||
(homepageDestination() !== "/" &&
|
||||
intentUrl?.startsWith(homepageDestination())) ||
|
||||
intentUrl?.startsWith("/login-required") ||
|
||||
(transition?.intent.name === `discovery.${defaultHomepage()}` &&
|
||||
transition?.intent.queryParams[homepageRewriteParam])
|
||||
|
||||
@@ -13,6 +13,7 @@ import { applyValueTransformer } from "discourse/lib/transformer";
|
||||
import { defaultHomepage } from "discourse/lib/utilities";
|
||||
import Category from "discourse/models/category";
|
||||
import Session from "discourse/models/session";
|
||||
import Site from "discourse/models/site";
|
||||
|
||||
const rewrites = [];
|
||||
export const TOPIC_URL_REGEXP = /\/t\/([^\/]*[^\d\/][^\/]*)\/(\d+)\/?(\d+)?/;
|
||||
@@ -268,10 +269,17 @@ class DiscourseURL extends EmberObject {
|
||||
return this.redirectTo(path);
|
||||
}
|
||||
|
||||
const pathnameWithoutPrefix = withoutPrefix(pathname);
|
||||
const serverSide = SERVER_SIDE_ONLY.some((r) =>
|
||||
pathnameWithoutPrefix.match(r)
|
||||
);
|
||||
const pathnameWithoutPrefix = withoutPrefix(pathname).split(/[?#]/, 1)[0];
|
||||
const registeredServerSidePath = Site.current()
|
||||
?.homepage_options?.filter(({ server_side }) => server_side)
|
||||
.some(
|
||||
({ path: homepagePath }) =>
|
||||
pathnameWithoutPrefix === homepagePath ||
|
||||
pathnameWithoutPrefix.startsWith(`${homepagePath}/`)
|
||||
);
|
||||
const serverSide =
|
||||
registeredServerSidePath ||
|
||||
SERVER_SIDE_ONLY.some((r) => pathnameWithoutPrefix.match(r));
|
||||
if (serverSide) {
|
||||
this.redirectTo(path);
|
||||
return;
|
||||
|
||||
@@ -2,8 +2,10 @@ import { service } from "@ember/service";
|
||||
import {
|
||||
homepageDestination,
|
||||
homepageRewriteParam,
|
||||
serverSideHomepage,
|
||||
} from "discourse/lib/homepage-router-overrides";
|
||||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import DiscourseRoute from "../discourse";
|
||||
|
||||
@disableImplicitInjections
|
||||
@@ -13,6 +15,11 @@ export default class DiscoveryIndex extends DiscourseRoute {
|
||||
@service siteSettings;
|
||||
|
||||
beforeModel(transition) {
|
||||
if (serverSideHomepage()) {
|
||||
DiscourseURL.redirectTo("/");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = transition.intent.url;
|
||||
const params = url?.split("?", 2)[1];
|
||||
let destination = homepageDestination();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { next } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import ForgotPassword from "discourse/components/modal/forgot-password";
|
||||
import { defaultHomepage } from "discourse/lib/utilities";
|
||||
import { homepageNavigationDestination } from "discourse/lib/homepage-router-overrides";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class ForgotPasswordRoute extends DiscourseRoute {
|
||||
@@ -12,7 +12,7 @@ export default class ForgotPasswordRoute extends DiscourseRoute {
|
||||
const { loginRequired } = this.controllerFor("application");
|
||||
|
||||
await this.router.replaceWith(
|
||||
loginRequired ? "login" : `discovery.${defaultHomepage()}`
|
||||
loginRequired ? "login" : homepageNavigationDestination()
|
||||
);
|
||||
next(() => this.modal.show(ForgotPassword));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { service } from "@ember/service";
|
||||
import cookie from "discourse/lib/cookie";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import { homepageNavigationDestination } from "discourse/lib/homepage-router-overrides";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import {
|
||||
defaultHomepage,
|
||||
isValidDestinationUrl,
|
||||
postRNWebviewMessage,
|
||||
} from "discourse/lib/utilities";
|
||||
@@ -29,7 +29,9 @@ export default class extends DiscourseRoute {
|
||||
const { referrer } = document;
|
||||
const { isOnlyOneExternalLoginMethod, singleExternalLogin } = this.login;
|
||||
const redirect = auth_immediately || login_required || !from || wantsTo;
|
||||
const homepage = `discovery.${login_required ? "login-required" : defaultHomepage()}`;
|
||||
const homepage = login_required
|
||||
? "discovery.login-required"
|
||||
: homepageNavigationDestination();
|
||||
|
||||
// Regular users can't log in but staff can when the site is read-only
|
||||
if (isReadOnly && !isStaffWritesOnly) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { trackedArray } from "@ember/reactive/collections";
|
||||
import { next } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { homepageNavigationDestination } from "discourse/lib/homepage-router-overrides";
|
||||
import { showCreateInviteModal } from "discourse/lib/invite-modal";
|
||||
import { defaultHomepage } from "discourse/lib/utilities";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class extends DiscourseRoute {
|
||||
|
||||
// when landing on the route from a full page load
|
||||
this.router
|
||||
.replaceWith(`discovery.${defaultHomepage()}`)
|
||||
.replaceWith(homepageNavigationDestination())
|
||||
.followRedirects()
|
||||
.then(() => this.#openInviteModalIfAllowed());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { next } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { defaultHomepage } from "discourse/lib/utilities";
|
||||
import { homepageNavigationDestination } from "discourse/lib/homepage-router-overrides";
|
||||
import Group from "discourse/models/group";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import { i18n } from "discourse-i18n";
|
||||
@@ -27,7 +27,7 @@ export default class extends DiscourseRoute {
|
||||
|
||||
// when landing on the route from a full page load
|
||||
this.router
|
||||
.replaceWith(`discovery.${defaultHomepage()}`)
|
||||
.replaceWith(homepageNavigationDestination())
|
||||
.followRedirects()
|
||||
.then(() => this.#openComposerWithPrefilledValues(params));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { next } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { defaultHomepage } from "discourse/lib/utilities";
|
||||
import { homepageNavigationDestination } from "discourse/lib/homepage-router-overrides";
|
||||
import Category from "discourse/models/category";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
@@ -47,7 +47,7 @@ export default class extends DiscourseRoute {
|
||||
|
||||
// When landing on the route from a full page load
|
||||
this.router
|
||||
.replaceWith(`discovery.${defaultHomepage()}`)
|
||||
.replaceWith(homepageNavigationDestination())
|
||||
.followRedirects()
|
||||
.then(() => {
|
||||
if (this.currentUser.can_create_topic) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { service } from "@ember/service";
|
||||
import ShareTargetModal from "discourse/components/modal/share-target";
|
||||
import { defaultHomepage } from "discourse/lib/utilities";
|
||||
import { homepageNavigationDestination } from "discourse/lib/homepage-router-overrides";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class extends DiscourseRoute {
|
||||
@@ -31,7 +31,7 @@ export default class extends DiscourseRoute {
|
||||
|
||||
// The share-target route has no UI of its own — send the user to the
|
||||
// homepage; the modal (if any) opens once that page has rendered.
|
||||
this.router.replaceWith(`discovery.${defaultHomepage()}`);
|
||||
this.router.replaceWith(homepageNavigationDestination());
|
||||
}
|
||||
|
||||
#hasContent({ title, text, url, files }) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { service } from "@ember/service";
|
||||
import cookie from "discourse/lib/cookie";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import { homepageNavigationDestination } from "discourse/lib/homepage-router-overrides";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import {
|
||||
defaultHomepage,
|
||||
isValidDestinationUrl,
|
||||
postRNWebviewMessage,
|
||||
} from "discourse/lib/utilities";
|
||||
@@ -34,7 +34,9 @@ export default class extends DiscourseRoute {
|
||||
const { canSignUp } = this.controllerFor("application");
|
||||
const { isOnlyOneExternalLoginMethod, singleExternalLogin } = this.login;
|
||||
const redirect = auth_immediately || login_required || !from || wantsTo;
|
||||
const homepage = `discovery.${login_required ? "login-required" : defaultHomepage()}`;
|
||||
const homepage = login_required
|
||||
? "discovery.login-required"
|
||||
: homepageNavigationDestination();
|
||||
|
||||
// Can't sign up when the site is read-only
|
||||
if (isReadOnly) {
|
||||
|
||||
@@ -15,10 +15,10 @@ import {
|
||||
clearPreview,
|
||||
} from "discourse/lib/design-wizard-preview";
|
||||
import { isTesting } from "discourse/lib/environment";
|
||||
import { homepagePreviewDestination } from "discourse/lib/homepage-router-overrides";
|
||||
import discourseLater from "discourse/lib/later";
|
||||
import { HORIZON_THEME_ID, setLocalTheme } from "discourse/lib/theme-selector";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { defaultHomepage } from "discourse/lib/utilities";
|
||||
|
||||
const STATE_KEY = "design_wizard_panel_state";
|
||||
// site settings the wizard mutates locally to preview a selection, and which
|
||||
@@ -170,7 +170,7 @@ export default class DesignWizardService extends Service {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.transitionTo(`discovery.${defaultHomepage()}`);
|
||||
await this.router.transitionTo(homepagePreviewDestination());
|
||||
await this.start({ source: SOURCE_ADMIN, returnUrl });
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ const siteFixtures = {
|
||||
"hot",
|
||||
"unread"
|
||||
],
|
||||
homepage_options: [],
|
||||
periods: ["all", "yearly", "quarterly", "monthly", "weekly", "daily"],
|
||||
top_menu_items: [
|
||||
"latest",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { setupTest } from "ember-qunit";
|
||||
import { module, test } from "qunit";
|
||||
import {
|
||||
homepageDestination,
|
||||
homepageNavigationDestination,
|
||||
homepagePath,
|
||||
homepagePreviewDestination,
|
||||
} from "discourse/lib/homepage-router-overrides";
|
||||
import { setDefaultHomepage } from "discourse/lib/utilities";
|
||||
import Site from "discourse/models/site";
|
||||
|
||||
module("Unit | Lib | homepage-router-overrides", function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
hooks.afterEach(function () {
|
||||
Site.current().set("homepage_options", []);
|
||||
});
|
||||
|
||||
test("uses the conventional filter path for core homepages", function (assert) {
|
||||
setDefaultHomepage("latest");
|
||||
|
||||
assert.strictEqual(homepagePath(), "/latest");
|
||||
assert.strictEqual(homepageNavigationDestination(), "discovery.latest");
|
||||
assert.strictEqual(homepagePreviewDestination(), "discovery.latest");
|
||||
assert.strictEqual(
|
||||
homepageDestination(),
|
||||
"/latest?_discourse_homepage_rewrite=1"
|
||||
);
|
||||
});
|
||||
|
||||
test("uses the path supplied by a registered homepage", function (assert) {
|
||||
Site.current().set("homepage_options", [
|
||||
{ id: "directory", path: "/directory" },
|
||||
]);
|
||||
setDefaultHomepage("directory");
|
||||
|
||||
assert.strictEqual(homepagePath(), "/directory");
|
||||
assert.strictEqual(homepageNavigationDestination(), "/directory");
|
||||
assert.strictEqual(homepagePreviewDestination(), "/directory");
|
||||
assert.strictEqual(
|
||||
homepageDestination(),
|
||||
"/directory?_discourse_homepage_rewrite=1"
|
||||
);
|
||||
});
|
||||
|
||||
test("returns the site root for a server-rendered homepage", function (assert) {
|
||||
Site.current().set("homepage_options", [
|
||||
{ id: "directory", path: "/directory", server_side: true },
|
||||
]);
|
||||
setDefaultHomepage("directory");
|
||||
|
||||
assert.strictEqual(homepagePath(), "/directory");
|
||||
assert.strictEqual(homepageDestination(), "/");
|
||||
assert.strictEqual(homepageNavigationDestination(), "/");
|
||||
assert.strictEqual(homepagePreviewDestination(), "discovery.latest");
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import DiscourseURL, {
|
||||
userPath,
|
||||
} from "discourse/lib/url";
|
||||
import Session from "discourse/models/session";
|
||||
import Site from "discourse/models/site";
|
||||
import { logIn } from "discourse/tests/helpers/qunit-helpers";
|
||||
|
||||
module("Unit | Utility | url", function (hooks) {
|
||||
@@ -372,6 +373,33 @@ module("Unit | Utility | url", function (hooks) {
|
||||
}
|
||||
});
|
||||
|
||||
test("routeTo redirects paths registered as server-rendered homepages", function (assert) {
|
||||
const site = Site.current();
|
||||
const originalOptions = site.homepage_options;
|
||||
site.set("homepage_options", [
|
||||
{ id: "directory", path: "/directory", server_side: true },
|
||||
]);
|
||||
sinon.stub(DiscourseURL, "redirectTo");
|
||||
|
||||
try {
|
||||
for (const path of [
|
||||
"/directory/people/1/example",
|
||||
"/directory?neighborhood=mississippi",
|
||||
"/directory#shops",
|
||||
]) {
|
||||
DiscourseURL.redirectTo.resetHistory();
|
||||
DiscourseURL.routeTo(path);
|
||||
|
||||
assert.true(
|
||||
DiscourseURL.redirectTo.calledWith(path),
|
||||
`${path} is redirected to the server`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
site.set("homepage_options", originalOptions);
|
||||
}
|
||||
});
|
||||
|
||||
test("routeTo full page loads when a refresh is required", async function (assert) {
|
||||
sinon.stub(DiscourseURL, "isComposerOpen").get(() => false);
|
||||
sinon.stub(DiscourseURL, "redirectTo");
|
||||
|
||||
@@ -144,6 +144,8 @@ class DiscoursePluginRegistry
|
||||
|
||||
define_filtered_register :calendar_subscription_feeds
|
||||
|
||||
define_filtered_register :homepage_options
|
||||
|
||||
define_filtered_register :custom_filter_mappings
|
||||
|
||||
define_filtered_register :acl_target_classes
|
||||
|
||||
@@ -1106,6 +1106,56 @@ class Plugin::Instance
|
||||
)
|
||||
end
|
||||
|
||||
# Registers a plugin page as an option for the default_homepage site setting.
|
||||
# The route is also mounted at `/` when the option is selected, while `path`
|
||||
# remains the page's canonical URL for direct navigation.
|
||||
#
|
||||
# @param id [String, Symbol] stable identifier stored in the site setting
|
||||
# @param name [String] client-side translation key used in the admin setting
|
||||
# @param path [String] application path for the homepage
|
||||
# @param route [String] Rails controller action, in `controller#action` form
|
||||
# @param anonymous [Boolean] whether logged-out visitors may use this homepage
|
||||
# @param server_side [Boolean] whether navigation requires a full page request
|
||||
def register_homepage(id, name:, path:, route:, anonymous: false, server_side: false)
|
||||
id = id.to_s
|
||||
|
||||
if !id.match?(/\A[a-z0-9][a-z0-9_-]*\z/)
|
||||
raise ArgumentError,
|
||||
"homepage id must contain only lowercase letters, numbers, underscores, and hyphens"
|
||||
end
|
||||
raise ArgumentError, "homepage name must be present" if name.blank?
|
||||
raise ArgumentError, "homepage path must start with /" if !path.to_s.start_with?("/")
|
||||
if !route.to_s.match?(/\A[^#]+#[^#]+\z/)
|
||||
raise ArgumentError, "homepage route must use controller#action format"
|
||||
end
|
||||
if ![true, false].include?(anonymous)
|
||||
raise ArgumentError, "homepage anonymous must be true or false"
|
||||
end
|
||||
if ![true, false].include?(server_side)
|
||||
raise ArgumentError, "homepage server_side must be true or false"
|
||||
end
|
||||
|
||||
registered_ids =
|
||||
DiscoursePluginRegistry._raw_homepage_options.map { |entry| entry[:value][:id] }
|
||||
core_homepage_ids =
|
||||
Discourse.filters.map(&:to_s) + %w[categories custom blank finish_installation]
|
||||
if core_homepage_ids.include?(id) || registered_ids.include?(id)
|
||||
raise ArgumentError, "homepage id '#{id}' is already registered"
|
||||
end
|
||||
|
||||
DiscoursePluginRegistry.register_homepage_option(
|
||||
{
|
||||
id: id,
|
||||
name: name,
|
||||
path: path.to_s,
|
||||
route: route.to_s,
|
||||
anonymous: anonymous,
|
||||
server_side: server_side,
|
||||
},
|
||||
self,
|
||||
)
|
||||
end
|
||||
|
||||
# Register a new demon process to be forked by the Unicorn master.
|
||||
# The demon_class should inherit from Demon::Base.
|
||||
# With great power comes great responsibility - this method should
|
||||
|
||||
@@ -1178,6 +1178,100 @@ TEXT
|
||||
end
|
||||
end
|
||||
|
||||
describe "#register_homepage" do
|
||||
before { plugin_instance.stubs(:enabled?).returns(true) }
|
||||
|
||||
it "registers a homepage while the plugin is enabled" do
|
||||
plugin_instance.register_homepage(
|
||||
:sample_homepage,
|
||||
name: "sample_plugin.homepage.title",
|
||||
path: "/sample-homepage",
|
||||
route: "sample_plugin/homepage#index",
|
||||
anonymous: true,
|
||||
)
|
||||
|
||||
expect(DiscoursePluginRegistry.homepage_options).to contain_exactly(
|
||||
{
|
||||
id: "sample_homepage",
|
||||
name: "sample_plugin.homepage.title",
|
||||
path: "/sample-homepage",
|
||||
route: "sample_plugin/homepage#index",
|
||||
anonymous: true,
|
||||
server_side: false,
|
||||
},
|
||||
)
|
||||
|
||||
plugin_instance.stubs(:enabled?).returns(false)
|
||||
expect(DiscoursePluginRegistry.homepage_options).to be_empty
|
||||
end
|
||||
|
||||
it "rejects invalid and duplicate registrations" do
|
||||
expect do
|
||||
plugin_instance.register_homepage(
|
||||
"not valid",
|
||||
name: "plugin.homepage",
|
||||
path: "/plugin",
|
||||
route: "plugin#index",
|
||||
)
|
||||
end.to raise_error(ArgumentError, /homepage id/)
|
||||
|
||||
expect do
|
||||
plugin_instance.register_homepage(
|
||||
"latest",
|
||||
name: "plugin.latest",
|
||||
path: "/plugin-latest",
|
||||
route: "plugin#latest",
|
||||
)
|
||||
end.to raise_error(ArgumentError, /already registered/)
|
||||
|
||||
expect do
|
||||
plugin_instance.register_homepage(
|
||||
"other_homepage",
|
||||
name: "plugin.other_homepage",
|
||||
path: "/other",
|
||||
route: "plugin#other",
|
||||
server_side: nil,
|
||||
)
|
||||
end.to raise_error(ArgumentError, /server_side/)
|
||||
|
||||
plugin_instance.register_homepage(
|
||||
"sample_homepage",
|
||||
name: "plugin.homepage",
|
||||
path: "/sample-homepage",
|
||||
route: "plugin#index",
|
||||
)
|
||||
|
||||
expect do
|
||||
plugin_instance.register_homepage(
|
||||
"sample_homepage",
|
||||
name: "plugin.other_homepage",
|
||||
path: "/other",
|
||||
route: "plugin#other",
|
||||
)
|
||||
end.to raise_error(ArgumentError, /already registered/)
|
||||
end
|
||||
|
||||
it "allows distinct IDs that normalize to the same Rails helper name" do
|
||||
plugin_instance.register_homepage(
|
||||
"sample-homepage",
|
||||
name: "plugin.hyphenated",
|
||||
path: "/hyphenated",
|
||||
route: "plugin#hyphenated",
|
||||
)
|
||||
plugin_instance.register_homepage(
|
||||
"sample_homepage",
|
||||
name: "plugin.underscored",
|
||||
path: "/underscored",
|
||||
route: "plugin#underscored",
|
||||
)
|
||||
|
||||
expect(DiscoursePluginRegistry.homepage_options.pluck(:id)).to include(
|
||||
"sample-homepage",
|
||||
"sample_homepage",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#register_admin_dashboard_section" do
|
||||
let(:plugin) { Plugin::Instance.new }
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe HomepageSiteSetting do
|
||||
around do |example|
|
||||
registrations = DiscoursePluginRegistry._raw_homepage_options.dup
|
||||
example.run
|
||||
DiscoursePluginRegistry._raw_homepage_options.replace(registrations)
|
||||
end
|
||||
|
||||
it "offers the top_menu fallback and every homepage choice" do
|
||||
values = described_class.values
|
||||
|
||||
@@ -18,6 +24,25 @@ describe HomepageSiteSetting do
|
||||
)
|
||||
end
|
||||
|
||||
it "includes homepages registered by enabled plugins" do
|
||||
plugin = Plugin::Instance.new
|
||||
plugin.stubs(:enabled?).returns(true)
|
||||
plugin.register_homepage(
|
||||
"directory",
|
||||
name: "discourse_directory.navigation.title",
|
||||
path: "/directory",
|
||||
route: "discourse_directory/directory#index",
|
||||
anonymous: true,
|
||||
)
|
||||
|
||||
expect(described_class.values).to include(
|
||||
{ name: "discourse_directory.navigation.title", value: "directory" },
|
||||
)
|
||||
|
||||
plugin.stubs(:enabled?).returns(false)
|
||||
expect(described_class.values.map { |value| value[:value] }).not_to include("directory")
|
||||
end
|
||||
|
||||
it "does not offer unread when it is excluded from top menu choices" do
|
||||
TopMenu.stubs(:choices).returns(%w[latest new top categories])
|
||||
|
||||
|
||||
@@ -96,6 +96,12 @@ RSpec.describe SiteSetting do
|
||||
end
|
||||
|
||||
describe "homepage" do
|
||||
around do |example|
|
||||
registrations = DiscoursePluginRegistry._raw_homepage_options.dup
|
||||
example.run
|
||||
DiscoursePluginRegistry._raw_homepage_options.replace(registrations)
|
||||
end
|
||||
|
||||
it "uses default_homepage when set" do
|
||||
SiteSetting.default_homepage = "bookmarks"
|
||||
expect(SiteSetting.homepage).to eq("bookmarks")
|
||||
@@ -128,6 +134,43 @@ RSpec.describe SiteSetting do
|
||||
SiteSetting.enable_unified_new = true
|
||||
expect(SiteSetting.homepage).to eq("categories")
|
||||
end
|
||||
|
||||
it "uses a registered plugin homepage and falls back when the plugin is disabled" do
|
||||
plugin = Plugin::Instance.new
|
||||
plugin.stubs(:enabled?).returns(true)
|
||||
plugin.register_homepage(
|
||||
"directory",
|
||||
name: "discourse_directory.navigation.title",
|
||||
path: "/directory",
|
||||
route: "discourse_directory/directory#index",
|
||||
anonymous: true,
|
||||
)
|
||||
SiteSetting.top_menu = "categories|latest"
|
||||
SiteSetting.default_homepage = "directory"
|
||||
|
||||
expect(SiteSetting.homepage).to eq("directory")
|
||||
expect(SiteSetting.anonymous_homepage).to eq("directory")
|
||||
|
||||
plugin.stubs(:enabled?).returns(false)
|
||||
expect(SiteSetting.homepage).to eq("categories")
|
||||
expect(SiteSetting.anonymous_homepage).to eq("categories")
|
||||
end
|
||||
|
||||
it "does not use a private plugin homepage for anonymous visitors" do
|
||||
plugin = Plugin::Instance.new
|
||||
plugin.stubs(:enabled?).returns(true)
|
||||
plugin.register_homepage(
|
||||
"private_page",
|
||||
name: "plugin.private_page",
|
||||
path: "/private-page",
|
||||
route: "plugin/private_page#index",
|
||||
)
|
||||
SiteSetting.top_menu = "categories|latest"
|
||||
SiteSetting.default_homepage = "private_page"
|
||||
|
||||
expect(SiteSetting.homepage).to eq("private_page")
|
||||
expect(SiteSetting.anonymous_homepage).to eq("categories")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -340,6 +340,28 @@
|
||||
"type": "array",
|
||||
"items": {}
|
||||
},
|
||||
"homepage_options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"server_side": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"path",
|
||||
"server_side"
|
||||
]
|
||||
}
|
||||
},
|
||||
"periods": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
@@ -1038,6 +1060,7 @@
|
||||
"filters",
|
||||
"anonymous_list_filters",
|
||||
"homepage_choices",
|
||||
"homepage_options",
|
||||
"periods",
|
||||
"top_menu_items",
|
||||
"anonymous_top_menu_items",
|
||||
@@ -1067,4 +1090,4 @@
|
||||
"full_name_visible_in_signup",
|
||||
"email_configured"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,35 @@ RSpec.describe SiteSerializer do
|
||||
end
|
||||
|
||||
describe "#homepage_choices" do
|
||||
around do |example|
|
||||
registrations = DiscoursePluginRegistry._raw_homepage_options.dup
|
||||
example.run
|
||||
DiscoursePluginRegistry._raw_homepage_options.replace(registrations)
|
||||
end
|
||||
|
||||
it "exposes the eligible homepage choices" do
|
||||
serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
|
||||
expect(serialized[:homepage_choices]).to eq(TopMenu.homepage_choices)
|
||||
expect(serialized[:homepage_choices]).to eq(HomepageSiteSetting.choices)
|
||||
end
|
||||
|
||||
it "exposes registered homepage paths" do
|
||||
plugin = Plugin::Instance.new
|
||||
plugin.stubs(:enabled?).returns(true)
|
||||
plugin.register_homepage(
|
||||
"directory",
|
||||
name: "discourse_directory.navigation.title",
|
||||
path: "/directory",
|
||||
route: "discourse_directory/directory#index",
|
||||
)
|
||||
|
||||
serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
|
||||
|
||||
expect(serialized[:homepage_choices]).to include("directory")
|
||||
expect(serialized[:homepage_options]).to include(
|
||||
id: "directory",
|
||||
path: "/directory",
|
||||
server_side: false,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe "Plugin homepages" do
|
||||
before do
|
||||
Object.const_set(
|
||||
:PluginHomepageSystemSpecController,
|
||||
Class.new(ApplicationController) do
|
||||
layout "no_ember"
|
||||
skip_before_action :check_xhr, :preload_json
|
||||
|
||||
def public_page
|
||||
render html: '<main id="plugin-public-homepage">Public plugin homepage</main>'.html_safe
|
||||
end
|
||||
|
||||
def private_page
|
||||
render html: '<main id="plugin-private-homepage">Private plugin homepage</main>'.html_safe
|
||||
end
|
||||
|
||||
def alternate_page
|
||||
render html:
|
||||
'<main id="plugin-alternate-homepage">Alternate plugin homepage</main>'.html_safe
|
||||
end
|
||||
end,
|
||||
)
|
||||
@plugins = []
|
||||
end
|
||||
|
||||
after do
|
||||
DiscoursePluginRegistry._raw_homepage_options.reject! do |registration|
|
||||
@plugins.include?(registration[:plugin])
|
||||
end
|
||||
Rails.application.reload_routes!
|
||||
Site.clear_cache
|
||||
Object.send(:remove_const, :PluginHomepageSystemSpecController)
|
||||
end
|
||||
|
||||
it "serves an anonymous plugin homepage directly at the root" do
|
||||
register_homepage(
|
||||
"public-plugin",
|
||||
route: "plugin_homepage_system_spec#public_page",
|
||||
anonymous: true,
|
||||
server_side: true,
|
||||
)
|
||||
select_homepage("public-plugin")
|
||||
|
||||
visit "/"
|
||||
|
||||
expect(page).to have_current_path("/")
|
||||
expect(page).to have_css("#plugin-public-homepage", text: "Public plugin homepage")
|
||||
end
|
||||
|
||||
it "falls back for anonymous visitors but serves a private homepage to members" do
|
||||
register_homepage(
|
||||
"private-plugin",
|
||||
route: "plugin_homepage_system_spec#private_page",
|
||||
server_side: true,
|
||||
)
|
||||
select_homepage("private-plugin")
|
||||
|
||||
visit "/"
|
||||
expect(page).to have_css("#list-area")
|
||||
expect(page).not_to have_css("#plugin-private-homepage")
|
||||
|
||||
sign_in Fabricate(:user)
|
||||
visit "/"
|
||||
expect(page).to have_css("#plugin-private-homepage", text: "Private plugin homepage")
|
||||
end
|
||||
|
||||
it "supports multiple registrations and falls back when the selected plugin is disabled" do
|
||||
register_homepage(
|
||||
"sample-homepage",
|
||||
route: "plugin_homepage_system_spec#public_page",
|
||||
anonymous: true,
|
||||
server_side: true,
|
||||
)
|
||||
selected_plugin =
|
||||
register_homepage(
|
||||
"sample_homepage",
|
||||
route: "plugin_homepage_system_spec#alternate_page",
|
||||
anonymous: true,
|
||||
server_side: true,
|
||||
)
|
||||
select_homepage("sample_homepage")
|
||||
|
||||
visit "/"
|
||||
expect(page).to have_css("#plugin-alternate-homepage", text: "Alternate plugin homepage")
|
||||
|
||||
selected_plugin.enabled = false
|
||||
Site.clear_cache
|
||||
visit "/"
|
||||
|
||||
expect(page).to have_css("#list-area")
|
||||
expect(page).not_to have_css("#plugin-alternate-homepage")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def register_homepage(id, route:, anonymous: false, server_side: false)
|
||||
plugin = plugin_class.new
|
||||
plugin.enabled = true
|
||||
plugin.register_homepage(
|
||||
id,
|
||||
name: "plugin.#{id}",
|
||||
path: "/#{id}",
|
||||
route:,
|
||||
anonymous:,
|
||||
server_side:,
|
||||
)
|
||||
@plugins << plugin
|
||||
plugin
|
||||
end
|
||||
|
||||
def select_homepage(id)
|
||||
SiteSetting.default_homepage = id
|
||||
Rails.application.reload_routes!
|
||||
Site.clear_cache
|
||||
end
|
||||
|
||||
def plugin_class
|
||||
@plugin_class ||=
|
||||
Class.new(Plugin::Instance) do
|
||||
attr_accessor :enabled
|
||||
|
||||
def enabled?
|
||||
enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user