Skip to content

Commit 1d58d9f

Browse files
etewiahclaude
andcommitted
Add theme availability management for tenant and website levels
Introduces a two-tier theme availability system where tenant admins can set default themes available to all new websites, and also override these settings for individual websites based on subscription plans. The default theme is always available to all websites. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8ba5a98 commit 1d58d9f

File tree

15 files changed

+394
-12
lines changed

15 files changed

+394
-12
lines changed

app/controllers/site_admin/onboarding_controller.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,9 @@ def property_params
232232
end
233233

234234
def available_themes
235-
# Return list of enabled theme names from Pwb::Theme
236-
Pwb::Theme.enabled.map(&:name)
235+
# Return list of accessible theme names for the current website
236+
# This respects per-website theme availability settings
237+
current_website.accessible_theme_names
237238
end
238239
end
239240
end

app/controllers/site_admin/website/settings_controller.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ def show
2020
@home_page = @website.pages.find_by(slug: 'home')
2121
@carousel_contents = @website.contents.where(tag: 'carousel')
2222
when 'appearance'
23-
@themes = Pwb::Theme.enabled
23+
# Only show themes that are accessible to this website
24+
@themes = @website.accessible_themes
2425
@style_variables = @website.style_variables
2526
when 'seo'
2627
@social_media = @website.social_media || {}
@@ -104,7 +105,7 @@ def update_appearance_settings
104105
if @website.update(appearance_settings_params)
105106
redirect_to site_admin_website_settings_tab_path('appearance'), notice: 'Appearance settings updated successfully'
106107
else
107-
@themes = Pwb::Theme.enabled
108+
@themes = @website.accessible_themes
108109
@style_variables = @website.style_variables
109110
flash.now[:alert] = 'Failed to update appearance settings'
110111
render :show, status: :unprocessable_entity
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
module TenantAdmin
4+
class SettingsController < TenantAdminController
5+
def show
6+
@tenant_settings = Pwb::TenantSettings.instance
7+
@all_themes = Pwb::Theme.enabled
8+
@selected_themes = @tenant_settings.effective_default_themes
9+
end
10+
11+
def update
12+
@tenant_settings = Pwb::TenantSettings.instance
13+
14+
# Get submitted theme names, ensuring we always keep default
15+
theme_names = params.dig(:tenant_settings, :default_available_themes) || []
16+
theme_names = theme_names.reject(&:blank?)
17+
18+
# Ensure 'default' theme is always included
19+
theme_names = (['default'] + theme_names).uniq
20+
21+
if @tenant_settings.update(default_available_themes: theme_names)
22+
redirect_to tenant_admin_settings_path, notice: 'Tenant settings updated successfully.'
23+
else
24+
@all_themes = Pwb::Theme.enabled
25+
@selected_themes = theme_names
26+
flash.now[:alert] = 'Failed to update settings.'
27+
render :show, status: :unprocessable_entity
28+
end
29+
end
30+
end
31+
end

app/controllers/tenant_admin/websites_controller.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ def show
3333

3434
def new
3535
@website = Pwb::Website.new
36+
@themes = Pwb::Theme.enabled
37+
@all_themes = @themes
3638
end
3739

3840
def create
@@ -74,12 +76,16 @@ def retry_provisioning
7476

7577
def edit
7678
# @website set by before_action
79+
@themes = Pwb::Theme.enabled
80+
@all_themes = @themes
7781
end
7882

7983
def update
8084
if @website.update(website_params)
8185
redirect_to tenant_admin_website_path(@website), notice: "Website updated successfully."
8286
else
87+
@themes = Pwb::Theme.enabled
88+
@all_themes = @themes
8389
render :edit, status: :unprocessable_entity
8490
end
8591
end
@@ -118,7 +124,8 @@ def website_params
118124
:landing_hide_for_sale,
119125
:landing_hide_search_bar,
120126
:available_currencies,
121-
supported_locales: []
127+
supported_locales: [],
128+
available_themes: []
122129
)
123130
end
124131
end
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# frozen_string_literal: true
2+
3+
module Pwb
4+
# Concern for managing theme availability on websites.
5+
#
6+
# Provides methods to determine which themes are available for a website,
7+
# based on:
8+
# 1. Website-specific available_themes (if set)
9+
# 2. Tenant default themes (from TenantSettings)
10+
# 3. The "default" theme is always available
11+
#
12+
# Usage:
13+
# website.accessible_themes # => [Theme, Theme, ...]
14+
# website.theme_accessible?('bologna') # => true/false
15+
# website.accessible_theme_names # => ['default', 'brisbane']
16+
#
17+
module WebsiteThemeable
18+
extend ActiveSupport::Concern
19+
20+
# The default theme name - always available
21+
DEFAULT_THEME = 'default'.freeze
22+
23+
included do
24+
# Validate that theme_name is accessible to this website
25+
validate :theme_must_be_accessible, if: :theme_name_changed?
26+
end
27+
28+
# Get all themes accessible to this website
29+
# Returns enabled themes filtered by website/tenant availability
30+
#
31+
# @return [Array<Theme>]
32+
#
33+
def accessible_themes
34+
theme_names = accessible_theme_names
35+
Theme.enabled.select { |theme| theme_names.include?(theme.name) }
36+
end
37+
38+
# Get names of themes accessible to this website
39+
# Uses website-specific list if set, otherwise tenant defaults
40+
#
41+
# @return [Array<String>]
42+
#
43+
def accessible_theme_names
44+
themes = if available_themes.present?
45+
available_themes
46+
else
47+
TenantSettings.instance.effective_default_themes
48+
end
49+
50+
# Always include default theme and ensure uniqueness
51+
([DEFAULT_THEME] + themes).uniq
52+
end
53+
54+
# Check if a specific theme is accessible to this website
55+
#
56+
# @param theme_name [String] The theme name to check
57+
# @return [Boolean]
58+
#
59+
def theme_accessible?(theme_name)
60+
return true if theme_name.to_s == DEFAULT_THEME
61+
62+
accessible_theme_names.include?(theme_name.to_s)
63+
end
64+
65+
# Check if this website has custom theme availability set
66+
#
67+
# @return [Boolean]
68+
#
69+
def custom_theme_availability?
70+
available_themes.present?
71+
end
72+
73+
# Update the available themes for this website
74+
#
75+
# @param themes [Array<String>] Array of theme names
76+
# @return [Boolean] true if saved successfully
77+
#
78+
def update_available_themes(themes)
79+
update(available_themes: Array(themes).reject(&:blank?))
80+
end
81+
82+
# Reset to use tenant default themes
83+
#
84+
# @return [Boolean] true if saved successfully
85+
#
86+
def reset_to_default_themes
87+
update(available_themes: nil)
88+
end
89+
90+
private
91+
92+
def theme_must_be_accessible
93+
return if theme_name.blank?
94+
return if theme_accessible?(theme_name)
95+
96+
errors.add(:theme_name, "is not available for this website")
97+
end
98+
end
99+
end

app/models/pwb/tenant_settings.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# frozen_string_literal: true
2+
3+
module Pwb
4+
# TenantSettings is a singleton model for tenant-wide configuration.
5+
# Only one record should exist, accessed via TenantSettings.instance
6+
#
7+
# Usage:
8+
# Pwb::TenantSettings.instance.default_available_themes
9+
# Pwb::TenantSettings.default_themes
10+
#
11+
# == Schema Information
12+
#
13+
# Table name: pwb_tenant_settings
14+
#
15+
# id :bigint not null, primary key
16+
# singleton_key :string default("default"), not null
17+
# default_available_themes :text default([]), is an Array
18+
# configuration :jsonb default({})
19+
# created_at :datetime not null
20+
# updated_at :datetime not null
21+
#
22+
# Indexes
23+
#
24+
# index_pwb_tenant_settings_on_singleton_key (singleton_key) UNIQUE
25+
#
26+
class TenantSettings < ApplicationRecord
27+
self.table_name = 'pwb_tenant_settings'
28+
29+
# Ensure only one record exists
30+
validates :singleton_key, uniqueness: true
31+
32+
# Default theme is always available
33+
DEFAULT_THEME = 'default'.freeze
34+
35+
class << self
36+
# Get the singleton instance, creating it if needed
37+
#
38+
# @return [TenantSettings]
39+
#
40+
def instance
41+
find_or_create_by!(singleton_key: 'default')
42+
end
43+
44+
# Shorthand to get default available themes
45+
#
46+
# @return [Array<String>]
47+
#
48+
def default_themes
49+
instance.default_available_themes || []
50+
end
51+
52+
# Update default available themes
53+
#
54+
# @param themes [Array<String>] Array of theme names
55+
# @return [Boolean] true if saved successfully
56+
#
57+
def update_default_themes(themes)
58+
instance.update(default_available_themes: Array(themes).reject(&:blank?))
59+
end
60+
end
61+
62+
# Get themes that are available by default
63+
# Always includes the default theme
64+
#
65+
# @return [Array<String>]
66+
#
67+
def effective_default_themes
68+
themes = default_available_themes || []
69+
themes = [DEFAULT_THEME] if themes.empty?
70+
themes.uniq
71+
end
72+
73+
# Check if a theme is available by default
74+
#
75+
# @param theme_name [String] Theme name to check
76+
# @return [Boolean]
77+
#
78+
def theme_available?(theme_name)
79+
effective_default_themes.include?(theme_name.to_s)
80+
end
81+
end
82+
end

app/models/pwb/website.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# admin_config :json
99
# analytics_id_type :integer
1010
# available_currencies :text default([]), is an Array
11+
# available_themes :text is an Array
1112
# company_display_name :string
1213
# configuration :json
1314
# custom_domain :string
@@ -96,6 +97,7 @@ class Website < ApplicationRecord
9697
include Pwb::WebsiteSubscribable
9798
include Pwb::WebsiteSocialLinkable
9899
include Pwb::WebsiteLocalizable
100+
include Pwb::WebsiteThemeable
99101
include FlagShihTzu
100102

101103
# Virtual attributes for form handling (avoid conflict with AASM events)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<div class="max-w-7xl mx-auto">
2+
<div class="mb-4 md:mb-6">
3+
<h1 class="text-2xl md:text-3xl font-bold text-gray-900">Tenant Settings</h1>
4+
<p class="mt-1 md:mt-2 text-sm md:text-base text-gray-600">Configure default settings for all websites</p>
5+
</div>
6+
7+
<%= form_with model: @tenant_settings, url: tenant_admin_settings_path, method: :patch, local: true, class: "space-y-6" do |f| %>
8+
<!-- Theme Availability Section -->
9+
<div class="bg-white rounded-lg shadow p-4 md:p-6">
10+
<h2 class="text-lg font-semibold text-gray-900 mb-4">Default Theme Availability</h2>
11+
<p class="text-sm text-gray-600 mb-4">
12+
Select which themes are available to new websites by default. The "Default Theme (Bristol)" is always available.
13+
You can override these settings for individual websites.
14+
</p>
15+
16+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
17+
<% @all_themes.each do |theme| %>
18+
<div class="relative flex items-start p-4 border rounded-lg <%= theme.name == 'default' ? 'bg-blue-50 border-blue-200' : 'bg-white border-gray-200' %>">
19+
<div class="flex items-center h-5">
20+
<% if theme.name == 'default' %>
21+
<!-- Default theme is always checked and disabled -->
22+
<input type="hidden" name="tenant_settings[default_available_themes][]" value="default">
23+
<input type="checkbox"
24+
id="theme_<%= theme.name %>"
25+
checked
26+
disabled
27+
class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 opacity-50 cursor-not-allowed">
28+
<% else %>
29+
<input type="checkbox"
30+
id="theme_<%= theme.name %>"
31+
name="tenant_settings[default_available_themes][]"
32+
value="<%= theme.name %>"
33+
<%= 'checked' if @selected_themes.include?(theme.name) %>
34+
class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 cursor-pointer">
35+
<% end %>
36+
</div>
37+
<div class="ml-3">
38+
<label for="theme_<%= theme.name %>" class="font-medium text-gray-900 <%= theme.name == 'default' ? 'cursor-not-allowed' : 'cursor-pointer' %>">
39+
<%= theme.friendly_name || theme.name.titleize %>
40+
</label>
41+
<% if theme.description.present? %>
42+
<p class="text-xs text-gray-500 mt-1"><%= truncate(theme.description, length: 80) %></p>
43+
<% end %>
44+
<% if theme.name == 'default' %>
45+
<span class="inline-flex items-center mt-1 px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
46+
Always Available
47+
</span>
48+
<% end %>
49+
</div>
50+
</div>
51+
<% end %>
52+
</div>
53+
54+
<% if @all_themes.empty? %>
55+
<div class="text-center py-8 text-gray-500">
56+
<p>No enabled themes found. Check your theme configuration.</p>
57+
</div>
58+
<% end %>
59+
</div>
60+
61+
<!-- Submit Button -->
62+
<div class="flex justify-end">
63+
<%= f.submit "Save Settings", class: "px-4 md:px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm cursor-pointer" %>
64+
</div>
65+
<% end %>
66+
</div>

0 commit comments

Comments
 (0)