mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
DEV: allow themes to render their own custom homepage (#26291)
This PR adds a theme modifier and route so that custom themes can opt to show their own homepage. See PR description for example usage.
This commit is contained in:
parent
7eec13375d
commit
1eb70973a2
|
@ -186,6 +186,11 @@ export default Controller.extend({
|
|||
|
||||
homeChanged() {
|
||||
const siteHome = this.siteSettings.top_menu.split("|")[0].split(",")[0];
|
||||
|
||||
if (this.model.canPickThemeWithCustomHomepage) {
|
||||
USER_HOMES[-1] = "custom";
|
||||
}
|
||||
|
||||
const userHome = USER_HOMES[this.get("model.user_option.homepage_id")];
|
||||
|
||||
setDefaultHomepage(userHome || siteHome);
|
||||
|
@ -200,6 +205,14 @@ export default Controller.extend({
|
|||
});
|
||||
|
||||
let result = [];
|
||||
|
||||
if (this.model.canPickThemeWithCustomHomepage) {
|
||||
result.push({
|
||||
name: I18n.t("user.homepage.default"),
|
||||
value: -1,
|
||||
});
|
||||
}
|
||||
|
||||
this.siteSettings.top_menu.split("|").forEach((m) => {
|
||||
let id = homeValues[m];
|
||||
if (id) {
|
||||
|
|
|
@ -209,6 +209,7 @@ export default class User extends RestModel.extend(Evented) {
|
|||
@alias("sidebar_sections") sidebarSections;
|
||||
@mapBy("sidebarTags", "name") sidebarTagNames;
|
||||
@filterBy("groups", "has_messages", true) groupsWithMessages;
|
||||
@alias("can_pick_theme_with_custom_homepage") canPickThemeWithCustomHomepage;
|
||||
|
||||
numGroupsToDisplay = 2;
|
||||
|
||||
|
|
|
@ -63,6 +63,8 @@ export default function () {
|
|||
this.route("categoryNone", { path: "/c/*category_slug_path_with_id/none" });
|
||||
this.route("categoryAll", { path: "/c/*category_slug_path_with_id/all" });
|
||||
this.route("category", { path: "/c/*category_slug_path_with_id" });
|
||||
|
||||
this.route("custom");
|
||||
});
|
||||
|
||||
this.route("groups", { resetNamespace: true, path: "/g" }, function () {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<PluginOutlet @name="custom-homepage">
|
||||
{{#if this.currentUser.admin}}
|
||||
<p class="alert alert-info">
|
||||
{{i18n "custom_homepage.admin_message"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
</PluginOutlet>
|
|
@ -519,7 +519,7 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
def current_homepage
|
||||
current_user&.user_option&.homepage || SiteSetting.anonymous_homepage
|
||||
current_user&.user_option&.homepage || HomepageHelper.resolve(request, current_user)
|
||||
end
|
||||
|
||||
def serialize_data(obj, serializer, opts = nil)
|
||||
|
|
7
app/controllers/custom_homepage_controller.rb
Normal file
7
app/controllers/custom_homepage_controller.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CustomHomepageController < ApplicationController
|
||||
def index
|
||||
render "default/custom"
|
||||
end
|
||||
end
|
|
@ -559,7 +559,7 @@ module ApplicationHelper
|
|||
end
|
||||
|
||||
def current_homepage
|
||||
current_user&.user_option&.homepage || SiteSetting.anonymous_homepage
|
||||
current_user&.user_option&.homepage || HomepageHelper.resolve(request, current_user)
|
||||
end
|
||||
|
||||
def build_plugin_html(name)
|
||||
|
@ -758,6 +758,10 @@ module ApplicationHelper
|
|||
user&.display_name
|
||||
end
|
||||
|
||||
def anonymous_top_menu_items
|
||||
Discourse.anonymous_top_menu_items.map(&:to_s)
|
||||
end
|
||||
|
||||
def authentication_data
|
||||
return @authentication_data if defined?(@authentication_data)
|
||||
|
||||
|
|
|
@ -111,6 +111,7 @@ end
|
|||
# csp_extensions :string is an Array
|
||||
# svg_icons :string is an Array
|
||||
# topic_thumbnail_sizes :string is an Array
|
||||
# custom_homepage :boolean
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class UserOption < ActiveRecord::Base
|
||||
HOMEPAGES = {
|
||||
# -1 => reserved for "custom homepage"
|
||||
1 => "latest",
|
||||
2 => "categories",
|
||||
3 => "unread",
|
||||
|
@ -9,6 +10,7 @@ class UserOption < ActiveRecord::Base
|
|||
5 => "top",
|
||||
6 => "bookmarks",
|
||||
7 => "unseen",
|
||||
# 8 => reserved for "hot"
|
||||
}
|
||||
|
||||
self.ignored_columns = [
|
||||
|
@ -182,11 +184,7 @@ class UserOption < ActiveRecord::Base
|
|||
def homepage
|
||||
return HOMEPAGES[homepage_id] if HOMEPAGES.keys.include?(homepage_id)
|
||||
|
||||
if homepage_id == 8 && SiteSetting.top_menu_map.include?("hot")
|
||||
"hot"
|
||||
else
|
||||
SiteSetting.homepage
|
||||
end
|
||||
"hot" if homepage_id == 8 && SiteSetting.top_menu_map.include?("hot")
|
||||
end
|
||||
|
||||
def text_size
|
||||
|
|
|
@ -65,7 +65,8 @@ class UserSerializer < UserCardSerializer
|
|||
:use_logo_small_as_avatar,
|
||||
:sidebar_tags,
|
||||
:sidebar_category_ids,
|
||||
:display_sidebar_tags
|
||||
:display_sidebar_tags,
|
||||
:can_pick_theme_with_custom_homepage
|
||||
|
||||
untrusted_attributes :bio_raw, :bio_cooked, :profile_background_upload_url
|
||||
|
||||
|
@ -322,6 +323,10 @@ class UserSerializer < UserCardSerializer
|
|||
SiteSetting.use_site_small_logo_as_system_avatar
|
||||
end
|
||||
|
||||
def can_pick_theme_with_custom_homepage
|
||||
ThemeModifierHelper.new(theme_ids: Theme.user_theme_ids).custom_homepage
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def custom_field_keys
|
||||
|
|
|
@ -42,6 +42,7 @@ class WebHookUserSerializer < UserSerializer
|
|||
display_sidebar_tags
|
||||
sidebar_category_ids
|
||||
sidebar_tags
|
||||
can_pick_theme_with_custom_homepage
|
||||
].each { |attr| define_method("include_#{attr}?") { false } }
|
||||
|
||||
def include_email?
|
||||
|
|
|
@ -129,6 +129,8 @@ class UserUpdater
|
|||
user.primary_group_id = nil
|
||||
end
|
||||
|
||||
attributes[:homepage_id] = nil if attributes[:homepage_id] == "-1"
|
||||
|
||||
if attributes[:flair_group_id] && attributes[:flair_group_id] != user.flair_group_id &&
|
||||
(
|
||||
attributes[:flair_group_id].blank? ||
|
||||
|
|
9
app/views/default/custom.html.erb
Normal file
9
app/views/default/custom.html.erb
Normal file
|
@ -0,0 +1,9 @@
|
|||
<ul>
|
||||
<% anonymous_top_menu_items.each_with_index do |item, index| %>
|
||||
<li>
|
||||
<a href='<%= Discourse.base_url %>/<%= item %>'>
|
||||
<%= I18n.t("js.filters.#{item}.title") %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
|
@ -1672,6 +1672,9 @@ en:
|
|||
default: "(default)"
|
||||
any: "any"
|
||||
|
||||
homepage:
|
||||
default: "(default)"
|
||||
|
||||
password_confirmation:
|
||||
title: "Password Again"
|
||||
|
||||
|
@ -4183,6 +4186,9 @@ en:
|
|||
this_week: "Week"
|
||||
today: "Today"
|
||||
|
||||
custom_homepage:
|
||||
admin_message: 'One of your themes has enabled the "custom_homepage" modifier but it does not output anything in the [custom-homepage] connector. (This message is only shown to site administrators.)'
|
||||
|
||||
browser_update: 'Unfortunately, <a href="https://www.discourse.org/faq/#browser">your browser is unsupported</a>. Please <a href="https://browsehappy.com">switch to a supported browser</a> to view rich content, log in and reply.'
|
||||
|
||||
permission_types:
|
||||
|
|
|
@ -1586,6 +1586,8 @@ Discourse::Application.routes.draw do
|
|||
constraints: HomePageConstraint.new("finish_installation"),
|
||||
as: "installation_redirect"
|
||||
|
||||
root to: "custom#index", constraints: HomePageConstraint.new("custom"), as: "custom_index"
|
||||
|
||||
get "/user-api-key/new" => "user_api_keys#new"
|
||||
post "/user-api-key" => "user_api_keys#create"
|
||||
post "/user-api-key/revoke" => "user_api_keys#revoke"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddCustomHomepageThemeModifiers < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :theme_modifier_sets, :custom_homepage, :boolean, null: true
|
||||
end
|
||||
end
|
|
@ -9,7 +9,7 @@ class HomePageConstraint
|
|||
return @filter == "finish_installation" if SiteSetting.has_login_hint?
|
||||
|
||||
current_user = CurrentUser.lookup_from_env(request.env)
|
||||
homepage = current_user&.user_option&.homepage || SiteSetting.anonymous_homepage
|
||||
homepage = current_user&.user_option&.homepage || HomepageHelper.resolve(request, current_user)
|
||||
homepage == @filter
|
||||
rescue Discourse::InvalidAccess, Discourse::ReadOnly
|
||||
false
|
||||
|
|
9
lib/homepage_helper.rb
Normal file
9
lib/homepage_helper.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class HomepageHelper
|
||||
def self.resolve(request = nil, current_user = nil)
|
||||
return "custom" if ThemeModifierHelper.new(request: request).custom_homepage
|
||||
|
||||
current_user ? SiteSetting.homepage : SiteSetting.anonymous_homepage
|
||||
end
|
||||
end
|
28
spec/lib/homepage_helper_spec.rb
Normal file
28
spec/lib/homepage_helper_spec.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe HomepageHelper do
|
||||
describe "resolver" do
|
||||
fab!(:user)
|
||||
|
||||
it "returns latest by default" do
|
||||
expect(HomepageHelper.resolve).to eq("latest")
|
||||
end
|
||||
|
||||
it "returns custom when theme has a custom homepage" do
|
||||
ThemeModifierHelper.any_instance.expects(:custom_homepage).returns(true)
|
||||
|
||||
expect(HomepageHelper.resolve).to eq("custom")
|
||||
end
|
||||
|
||||
context "when first item in top menu is no valid for anons" do
|
||||
it "distinguishes between auth homepage and anon homepage" do
|
||||
SiteSetting.top_menu = "new|top|latest|unread"
|
||||
|
||||
expect(HomepageHelper.resolve(nil, user)).to eq("new")
|
||||
# new is not a valid route for anon users, anon homepage is next item, top
|
||||
expect(HomepageHelper.resolve).to eq(SiteSetting.anonymous_homepage)
|
||||
expect(HomepageHelper.resolve).to eq("top")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -336,6 +336,9 @@
|
|||
"display_sidebar_tags": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_pick_theme_with_custom_homepage": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"user_auth_tokens": {
|
||||
"type": "array",
|
||||
"items":
|
||||
|
|
|
@ -653,5 +653,10 @@ RSpec.describe UserUpdater do
|
|||
|
||||
expect(UserHistory.last.action).to eq(UserHistory.actions[:change_name])
|
||||
end
|
||||
|
||||
it "clears the homepage_id when the special 'custom' id is chosen" do
|
||||
UserUpdater.new(user, user).update(homepage_id: "-1")
|
||||
expect(user.user_option.homepage_id).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
152
spec/system/homepage_spec.rb
Normal file
152
spec/system/homepage_spec.rb
Normal file
|
@ -0,0 +1,152 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
describe "Homepage", type: :system do
|
||||
fab!(:admin)
|
||||
fab!(:user)
|
||||
fab!(:topics) { Fabricate.times(5, :post).map(&:topic) }
|
||||
let(:discovery) { PageObjects::Pages::Discovery.new }
|
||||
let!(:theme) { Fabricate(:theme) }
|
||||
|
||||
before do
|
||||
# A workaround to avoid the global notice from interfering with the tests
|
||||
# It is coming from the ensure_login_hint.rb initializer and it gets
|
||||
# evaluated before the tests run (and it wrongly counts 0 admins defined)
|
||||
SiteSetting.global_notice = nil
|
||||
end
|
||||
|
||||
it "shows a list of topics by default" do
|
||||
visit "/"
|
||||
expect(discovery.topic_list).to have_topics(count: 5)
|
||||
end
|
||||
|
||||
it "allows users to pick their homepage" do
|
||||
sign_in user
|
||||
visit "/"
|
||||
|
||||
expect(page).to have_css(".navigation-container .latest.active", text: "Latest")
|
||||
|
||||
visit "u/#{user.username}/preferences/interface"
|
||||
|
||||
homepage_picker = PageObjects::Components::SelectKit.new("#home-selector")
|
||||
homepage_picker.expand
|
||||
homepage_picker.select_row_by_name("Top")
|
||||
page.find(".btn-primary.save-changes").click
|
||||
|
||||
# Wait for the save to complete
|
||||
find(".btn-primary.save-changes:not([disabled])", wait: 5)
|
||||
|
||||
visit "/"
|
||||
|
||||
expect(page).to have_css(".navigation-container .top.active", text: "Top")
|
||||
expect(page).to have_css(".top-lists")
|
||||
end
|
||||
|
||||
it "defaults to first top_menu item as anonymous homepage" do
|
||||
SiteSetting.top_menu = "categories|latest|new|unread"
|
||||
visit "/"
|
||||
|
||||
expect(page).to have_css(".navigation-container .categories.active", text: "Categories")
|
||||
|
||||
sign_in user
|
||||
visit "/"
|
||||
|
||||
expect(page).to have_css(".navigation-container .categories.active", text: "Categories")
|
||||
end
|
||||
|
||||
context "when default theme uses a custom_homepage modifier" do
|
||||
before do
|
||||
theme.theme_modifier_set.custom_homepage = true
|
||||
theme.theme_modifier_set.save!
|
||||
theme.set_default!
|
||||
end
|
||||
|
||||
it "shows empty state to regular users" do
|
||||
sign_in user
|
||||
visit "/"
|
||||
|
||||
expect(page).to have_no_css(".list-container")
|
||||
expect(page).to have_no_css(".alert-info")
|
||||
end
|
||||
|
||||
it "shows empty state and notice to admins" do
|
||||
sign_in admin
|
||||
visit "/"
|
||||
|
||||
expect(page).to have_no_css(".list-container")
|
||||
expect(page).to have_css(".alert-info")
|
||||
end
|
||||
|
||||
context "when the theme adds content to the [custom-homepage] connector" do
|
||||
let!(:basic_html_field) do
|
||||
Fabricate(
|
||||
:theme_field,
|
||||
theme: theme,
|
||||
type_id: ThemeField.types[:html],
|
||||
target_id: Theme.targets[:common],
|
||||
name: "head_tag",
|
||||
value: <<~HTML,
|
||||
<script type="text/x-handlebars" data-template-name="/connectors/custom-homepage/new-home">
|
||||
<div class="new-home">Hi friends!</div>
|
||||
</script>
|
||||
HTML
|
||||
)
|
||||
end
|
||||
|
||||
it "shows the custom homepage from the theme on the homepage" do
|
||||
visit "/"
|
||||
|
||||
expect(page).to have_css(".new-home", text: "Hi friends!")
|
||||
expect(page).to have_no_css(".list-container")
|
||||
|
||||
find("#sidebar-section-content-community .sidebar-section-link:first-child").click
|
||||
expect(page).to have_css(".list-container")
|
||||
|
||||
find("#site-logo").click
|
||||
|
||||
expect(page).to have_no_css(".list-container")
|
||||
# ensure clicking on logo brings user back to the custom homepage
|
||||
expect(page).to have_css(".new-home", text: "Hi friends!")
|
||||
end
|
||||
|
||||
it "respects the user's homepage choice" do
|
||||
visit "/"
|
||||
|
||||
expect(page).not_to have_css(".list-container")
|
||||
expect(page).to have_css(".new-home", text: "Hi friends!")
|
||||
|
||||
sign_in user
|
||||
|
||||
visit "/u/#{user.username}/preferences/interface"
|
||||
|
||||
homepage_picker = PageObjects::Components::SelectKit.new("#home-selector")
|
||||
homepage_picker.expand
|
||||
# user overrides theme custom homepage
|
||||
homepage_picker.select_row_by_name("Top")
|
||||
page.find(".btn-primary.save-changes").click
|
||||
|
||||
# Wait for the save to complete
|
||||
find(".btn-primary.save-changes:not([disabled])", wait: 5)
|
||||
|
||||
find("#site-logo").click
|
||||
|
||||
expect(page).to have_css(".navigation-container .top.active", text: "Top")
|
||||
expect(page).to have_css(".top-lists")
|
||||
|
||||
visit "/u/#{user.username}/preferences/interface"
|
||||
|
||||
homepage_picker = PageObjects::Components::SelectKit.new("#home-selector")
|
||||
homepage_picker.expand
|
||||
# user selects theme custom homepage again
|
||||
homepage_picker.select_row_by_name("(default)")
|
||||
page.find(".btn-primary.save-changes").click
|
||||
|
||||
# Wait for the save to complete
|
||||
find(".btn-primary.save-changes:not([disabled])", wait: 5)
|
||||
find("#site-logo").click
|
||||
|
||||
expect(page).not_to have_css(".list-container")
|
||||
expect(page).to have_css(".new-home", text: "Hi friends!")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user