panda_cms 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of panda_cms might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/README.md +72 -0
- data/Rakefile +8 -0
- data/app/assets/builds/panda_cms.css +1 -0
- data/app/assets/config/panda_cms_manifest.js +1 -0
- data/app/assets/stylesheets/panda_cms/application.tailwind.css +30 -0
- data/app/builders/panda_cms/form_builder.rb +118 -0
- data/app/components/panda_cms/admin/button_component.rb +65 -0
- data/app/components/panda_cms/admin/container_component.html.erb +13 -0
- data/app/components/panda_cms/admin/container_component.rb +11 -0
- data/app/components/panda_cms/admin/flash_message_component.html.erb +30 -0
- data/app/components/panda_cms/admin/flash_message_component.rb +44 -0
- data/app/components/panda_cms/admin/heading_component.rb +38 -0
- data/app/components/panda_cms/admin/slideover_component.html.erb +9 -0
- data/app/components/panda_cms/admin/slideover_component.rb +13 -0
- data/app/components/panda_cms/admin/tab_bar_component.html.erb +35 -0
- data/app/components/panda_cms/admin/tab_bar_component.rb +13 -0
- data/app/components/panda_cms/grid_component.html.erb +6 -0
- data/app/components/panda_cms/grid_component.rb +13 -0
- data/app/components/panda_cms/menu_component.html.erb +3 -0
- data/app/components/panda_cms/menu_component.rb +18 -0
- data/app/components/panda_cms/page_menu_component.html.erb +24 -0
- data/app/components/panda_cms/page_menu_component.rb +24 -0
- data/app/components/panda_cms/rich_text_component.html.erb +40 -0
- data/app/components/panda_cms/rich_text_component.rb +35 -0
- data/app/components/panda_cms/text_component.rb +63 -0
- data/app/constraints/panda_cms/admin_constraint.rb +16 -0
- data/app/controllers/panda_cms/admin/block_contents_controller.rb +42 -0
- data/app/controllers/panda_cms/admin/dashboard_controller.rb +15 -0
- data/app/controllers/panda_cms/admin/files_controller.rb +17 -0
- data/app/controllers/panda_cms/admin/menus_controller.rb +81 -0
- data/app/controllers/panda_cms/admin/pages_controller.rb +88 -0
- data/app/controllers/panda_cms/admin/sessions_controller.rb +72 -0
- data/app/controllers/panda_cms/application_controller.rb +51 -0
- data/app/controllers/panda_cms/errors_controller.rb +31 -0
- data/app/controllers/panda_cms/pages_controller.rb +33 -0
- data/app/helpers/panda_cms/admin/files_helper.rb +4 -0
- data/app/helpers/panda_cms/admin/pages_helper.rb +4 -0
- data/app/helpers/panda_cms/application_helper.rb +91 -0
- data/app/helpers/panda_cms/pages_helper.rb +4 -0
- data/app/helpers/panda_cms/theme_helper.rb +16 -0
- data/app/javascript/panda_cms/base.js +37 -0
- data/app/javascript/panda_cms/controllers/menu_controller.js +19 -0
- data/app/javascript/panda_cms/controllers/rich_text_editor_controller.js +59 -0
- data/app/javascript/panda_cms/controllers/text_field_update_controller.js +23 -0
- data/app/javascript/panda_cms/vendor/stimulus-components-rails-nested-form.js +2 -0
- data/app/javascript/panda_cms/vendor/tailwindcss-stimulus-components.js +2 -0
- data/app/jobs/panda_cms/application_job.rb +4 -0
- data/app/lib/panda_cms/demo_site_generator.rb +70 -0
- data/app/lib/panda_cms/slug.rb +21 -0
- data/app/mailers/panda_cms/application_mailer.rb +6 -0
- data/app/models/panda_cms/application_record.rb +5 -0
- data/app/models/panda_cms/block.rb +32 -0
- data/app/models/panda_cms/block_content.rb +16 -0
- data/app/models/panda_cms/block_content_version.rb +6 -0
- data/app/models/panda_cms/breadcrumb.rb +10 -0
- data/app/models/panda_cms/current.rb +15 -0
- data/app/models/panda_cms/menu.rb +50 -0
- data/app/models/panda_cms/menu_item.rb +56 -0
- data/app/models/panda_cms/page.rb +86 -0
- data/app/models/panda_cms/page_version.rb +6 -0
- data/app/models/panda_cms/redirect.rb +9 -0
- data/app/models/panda_cms/template.rb +44 -0
- data/app/models/panda_cms/template_version.rb +6 -0
- data/app/models/panda_cms/user.rb +11 -0
- data/app/models/panda_cms/version.rb +6 -0
- data/app/models/panda_cms/visit.rb +7 -0
- data/app/views/layouts/panda_cms/application.html.erb +68 -0
- data/app/views/layouts/panda_cms/public.html.erb +3 -0
- data/app/views/panda_cms/admin/dashboard/show.html.erb +8 -0
- data/app/views/panda_cms/admin/files/index.html.erb +124 -0
- data/app/views/panda_cms/admin/files/show.html.erb +2 -0
- data/app/views/panda_cms/admin/menus/_form.html.erb +21 -0
- data/app/views/panda_cms/admin/menus/_menu_item_fields.html.erb +7 -0
- data/app/views/panda_cms/admin/menus/edit.html.erb +58 -0
- data/app/views/panda_cms/admin/menus/index.html.erb +32 -0
- data/app/views/panda_cms/admin/menus/new.html.erb +5 -0
- data/app/views/panda_cms/admin/pages/edit.html.erb +35 -0
- data/app/views/panda_cms/admin/pages/index.html.erb +46 -0
- data/app/views/panda_cms/admin/pages/new.html.erb +16 -0
- data/app/views/panda_cms/admin/pages/show.html.erb +1 -0
- data/app/views/panda_cms/admin/sessions/new.html.erb +39 -0
- data/app/views/panda_cms/admin/shared/_breadcrumbs.html.erb +25 -0
- data/app/views/panda_cms/admin/shared/_flash.html.erb +5 -0
- data/app/views/panda_cms/admin/shared/_sidebar.html.erb +29 -0
- data/app/views/panda_cms/shared/_favicons.html.erb +9 -0
- data/app/views/panda_cms/shared/_footer.html.erb +2 -0
- data/app/views/panda_cms/shared/_header.html.erb +19 -0
- data/config/importmap.rb +7 -0
- data/config/initializers/panda_cms/form_errors.rb +38 -0
- data/config/initializers/panda_cms.rb +42 -0
- data/config/locales/en.yml +13 -0
- data/config/routes.rb +26 -0
- data/config/tailwind.config.js +31 -0
- data/config/tailwind.embed.config.js +20 -0
- data/db/migrate/20240205223709_create_panda_cms_pages.rb +9 -0
- data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +14 -0
- data/db/migrate/20240303002805_create_panda_cms_templates.rb +11 -0
- data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +14 -0
- data/db/migrate/20240303022441_create_panda_cms_blocks.rb +13 -0
- data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +10 -0
- data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +14 -0
- data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +10 -0
- data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +12 -0
- data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +5 -0
- data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +16 -0
- data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +6 -0
- data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +5 -0
- data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +24 -0
- data/db/migrate/20240317010532_create_panda_cms_users.rb +12 -0
- data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +7 -0
- data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +5 -0
- data/db/migrate/20240317214827_create_panda_cms_redirects.rb +15 -0
- data/db/migrate/20240317230622_create_panda_cms_visits.rb +13 -0
- data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +58 -0
- data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +5 -0
- data/db/seeds.rb +4 -0
- data/lib/generators/panda_cms/install_generator.rb +32 -0
- data/lib/panda_cms/engine.rb +172 -0
- data/lib/panda_cms/exceptions_app.rb +24 -0
- data/lib/panda_cms/version.rb +3 -0
- data/lib/panda_cms.rb +14 -0
- data/lib/tasks/panda_cms.rake +67 -0
- data/lib/templates/erb/scaffold/_form.html.erb.tt +43 -0
- data/lib/templates/erb/scaffold/edit.html.erb.tt +8 -0
- data/lib/templates/erb/scaffold/index.html.erb.tt +14 -0
- data/lib/templates/erb/scaffold/new.html.erb.tt +7 -0
- data/lib/templates/erb/scaffold/partial.html.erb.tt +22 -0
- data/lib/templates/erb/scaffold/show.html.erb.tt +15 -0
- data/public/panda-cms-assets/android-chrome-192x192.png +0 -0
- data/public/panda-cms-assets/android-chrome-512x512.png +0 -0
- data/public/panda-cms-assets/apple-touch-icon.png +0 -0
- data/public/panda-cms-assets/browserconfig.xml +9 -0
- data/public/panda-cms-assets/editable.js +212 -0
- data/public/panda-cms-assets/favicon-16x16.png +0 -0
- data/public/panda-cms-assets/favicon-32x32.png +0 -0
- data/public/panda-cms-assets/favicon.ico +0 -0
- data/public/panda-cms-assets/mstile-150x150.png +0 -0
- data/public/panda-cms-assets/panda-logo-screenprint.png +0 -0
- data/public/panda-cms-assets/panda-nav.png +0 -0
- data/public/panda-cms-assets/safari-pinned-tab.svg +61 -0
- data/public/panda-cms-assets/site.webmanifest +14 -0
- data/public/panda-cms-assets/stimulus-loading.js +113 -0
- metadata +845 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module Generators
|
2
|
+
module PandaCms
|
3
|
+
class InstallGenerator < ::Rails::Generators::Base
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
namespace "panda_cms:install"
|
7
|
+
desc "Adds the basic configuration for Panda CMS to your Rails app."
|
8
|
+
|
9
|
+
def create_initializer_file
|
10
|
+
# Add the initializer
|
11
|
+
initializer_path = "config/initializers/panda_cms.rb"
|
12
|
+
unless File.exist?("#{::Rails.root}/#{initializer_path}")
|
13
|
+
FileUtils.cp "#{::PandaCms::Engine.root}/#{initializer_path}", "#{::Rails.root}/#{initializer_path}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# # Add the JavaScript controllers
|
17
|
+
# unless File.exist?("#{::Rails.root}/app/javascript/panda_cms/controllers/editable_controller.js")
|
18
|
+
# `mkdir -p "#{::Rails.root}/app/javascript/panda_cms/controllers"`
|
19
|
+
# # FileUtils.cp "#{::PandaCms::Engine.root}/app/assets/javascript/panda_cms/controllers/editable_controller.js", "#{::Rails.root}/app/javascript/panda_cms/controllers/editable_controller.js"
|
20
|
+
# # `bin/importmap pin tailwindcss-stimulus-components`
|
21
|
+
# # `bin/importmap pin stimulus-rails-nested-form`
|
22
|
+
# end
|
23
|
+
|
24
|
+
# Add the seed loader to the seeds.rb file
|
25
|
+
unless File.read("#{::Rails.root}/db/seeds.rb")&.include?("PandaCms::Engine.load_seed")
|
26
|
+
existing_seeds = File.read("#{::Rails.root}/db/seeds.rb")
|
27
|
+
IO.write("#{::Rails.root}/db/seeds.rb", "PandaCms::Engine.load_seed\n\n#{existing_seeds}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require "importmap-rails"
|
2
|
+
require "paper_trail"
|
3
|
+
require "view_component"
|
4
|
+
|
5
|
+
require "omniauth"
|
6
|
+
require "omniauth/rails_csrf_protection"
|
7
|
+
require "omniauth/strategies/microsoft_graph"
|
8
|
+
require "omniauth/strategies/google_oauth2"
|
9
|
+
require "omniauth/strategies/github"
|
10
|
+
|
11
|
+
require "panda_cms/exceptions_app"
|
12
|
+
|
13
|
+
module PandaCms
|
14
|
+
class Engine < ::Rails::Engine
|
15
|
+
isolate_namespace PandaCms
|
16
|
+
engine_name "panda_cms"
|
17
|
+
|
18
|
+
attr_accessor :importmap
|
19
|
+
|
20
|
+
config.to_prepare do
|
21
|
+
ApplicationController.helper(::ApplicationHelper)
|
22
|
+
end
|
23
|
+
|
24
|
+
# We rely on ViewComponent here before we can continue
|
25
|
+
config.railties_order = [::ViewComponent::Engine, PandaCms::Engine, :main_app, :all]
|
26
|
+
|
27
|
+
# Set our generators
|
28
|
+
config.generators do |g|
|
29
|
+
g.orm :active_record, primary_key_type: :uuid
|
30
|
+
g.test_framework :rspec, fixture: true
|
31
|
+
g.fixture_replacement :factory_bot, dir: "spec/factories"
|
32
|
+
g.view_specs false
|
33
|
+
g.templates.unshift File.expand_path("../../templates", __FILE__)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Make files in public available to the main app (e.g. favicon.ico)
|
37
|
+
config.app_middleware.use(
|
38
|
+
Rack::Static,
|
39
|
+
urls: ["/panda-cms-assets"],
|
40
|
+
root: PandaCms::Engine.root.join("public")
|
41
|
+
)
|
42
|
+
|
43
|
+
config.exceptions_app = PandaCms::ExceptionsApp.new(exceptions_app: routes)
|
44
|
+
|
45
|
+
# Create an importmap for the engine's JS
|
46
|
+
initializer "panda_cms.importmap", before: "importmap" do |app|
|
47
|
+
map = Importmap::Map.new
|
48
|
+
map.draw(PandaCms::Engine.root.join("config/importmap.rb"))
|
49
|
+
map.cache_sweeper(watches: PandaCms::Engine.root.join("app/javascript"))
|
50
|
+
# TODO: Sort in production?
|
51
|
+
app.config.assets.paths << PandaCms::Engine.root.join("app/javascript") if Rails.env.development?
|
52
|
+
PandaCms::Engine.importmap = map
|
53
|
+
ActiveSupport.on_load(:action_controller_base, run_once: true) do
|
54
|
+
before_action { PandaCms::Engine.importmap.cache_sweeper.execute_if_updated }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Append routes to the routes file
|
59
|
+
config.after_initialize do |app|
|
60
|
+
app.routes.append do
|
61
|
+
mount PandaCms::Engine => "/", :as => "panda_cms"
|
62
|
+
get "/_maintenance", to: "panda_cms/errors#error_503", as: :panda_cms_maintenance
|
63
|
+
get "/*path", to: "panda_cms/pages#show", as: :panda_cms_page
|
64
|
+
root to: "panda_cms/pages#root"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Add the migrations to the main app
|
69
|
+
initializer "panda_cms.migrations" do |app|
|
70
|
+
unless app.root.to_s.match root.to_s
|
71
|
+
config.paths["db/migrate"].expanded.each do |expanded_path|
|
72
|
+
app.config.paths["db/migrate"] << expanded_path
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Set up authentication
|
78
|
+
initializer "panda_cms.omniauth", before: "omniauth" do |app|
|
79
|
+
app.config.session_store :cookie_store, key: "_panda_cms_session"
|
80
|
+
app.config.middleware.use ActionDispatch::Cookies
|
81
|
+
app.config.middleware.use ActionDispatch::Session::CookieStore, app.config.session_options
|
82
|
+
|
83
|
+
OmniAuth.config.logger = Rails.logger
|
84
|
+
auth_path = "#{PandaCms.admin_path}/auth"
|
85
|
+
callback_path = "/callback"
|
86
|
+
|
87
|
+
# TODO: Move this to somewhere more sensible
|
88
|
+
# Define the mapping of our provider "names" to the OmniAuth strategies and configuration
|
89
|
+
available_providers = {
|
90
|
+
microsoft: {
|
91
|
+
strategy: :microsoft_graph,
|
92
|
+
defaults: {
|
93
|
+
name: "microsoft",
|
94
|
+
# Setup at the following URL:
|
95
|
+
# https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade
|
96
|
+
client_id: Rails.application.credentials.dig(:microsoft, :client_id),
|
97
|
+
client_secret: Rails.application.credentials.dig(:microsoft, :client_secret),
|
98
|
+
# Don't change this or the sky will fall on your head
|
99
|
+
# https://github.com/synth/omniauth-microsoft_graph/tree/main?tab=readme-ov-file#domain-verification
|
100
|
+
skip_domain_verification: false,
|
101
|
+
# If your application is single-tenanted, replace "common" with your tenant (directory) ID
|
102
|
+
# from https://portal.azure.com/#settings/directory, otherwise you'll likely want to leave
|
103
|
+
# these settings unchanged
|
104
|
+
client_options: {
|
105
|
+
site: "https://login.microsoftonline.com/",
|
106
|
+
token_url: "common/oauth2/v2.0/token",
|
107
|
+
authorize_url: "common/oauth2/v2.0/authorize"
|
108
|
+
},
|
109
|
+
# If you assign specific users or groups, you will likely want to set this to
|
110
|
+
# true to enable auto-provisioning
|
111
|
+
create_account_on_first_login: false,
|
112
|
+
create_admin_account_on_first_login: false
|
113
|
+
}
|
114
|
+
},
|
115
|
+
google: {
|
116
|
+
strategy: :google_oauth2,
|
117
|
+
defaults: {
|
118
|
+
name: "google",
|
119
|
+
# Setup at the following URL: https://console.developers.google.com/
|
120
|
+
client_id: Rails.application.credentials.dig(:google, :client_id),
|
121
|
+
client_secret: Rails.application.credentials.dig(:google, :client_secret),
|
122
|
+
# If you assign specific users or groups, you will likely want to set this to
|
123
|
+
# true to enable auto-provisioning
|
124
|
+
create_account_on_first_login: false,
|
125
|
+
create_admin_account_on_first_login: false,
|
126
|
+
# Options we need
|
127
|
+
scope: "email, profile",
|
128
|
+
image_aspect_ratio: "square",
|
129
|
+
image_size: 150
|
130
|
+
}
|
131
|
+
},
|
132
|
+
github: {
|
133
|
+
strategy: :github,
|
134
|
+
defaults: {
|
135
|
+
# Setup at the following URL: https://github.com/settings/applications/new
|
136
|
+
# In the meantime, as long as you're set to /admin as your login path, you can
|
137
|
+
# use these for a first login
|
138
|
+
client_id: Rails.application.credentials.dig(:github, :client_id) || "Ov23li9k0LpMXtq8FShb",
|
139
|
+
client_secret: Rails.application.credentials.dig(:github, :client_secret) || "659f54cad324a00b19fc4e342cfecc6a0534fa55",
|
140
|
+
name: "github",
|
141
|
+
scope: "user:email,read:user",
|
142
|
+
create_account_on_first_login: false,
|
143
|
+
create_admin_account_on_first_login: false
|
144
|
+
}
|
145
|
+
}
|
146
|
+
}
|
147
|
+
|
148
|
+
available_providers.each do |provider, options|
|
149
|
+
if PandaCms.authentication.dig(provider, :enabled)
|
150
|
+
options[:defaults][:path_prefix] = auth_path
|
151
|
+
options[:defaults][:redirect_uri] = "#{PandaCms.url}#{auth_path}/#{provider}#{callback_path}"
|
152
|
+
|
153
|
+
provider_config = options[:defaults].merge(PandaCms.authentication[provider].except(
|
154
|
+
:enabled,
|
155
|
+
:client_id,
|
156
|
+
:client_secret,
|
157
|
+
:create_account_on_first_login,
|
158
|
+
:create_admin_account_on_first_login
|
159
|
+
))
|
160
|
+
|
161
|
+
Rails.logger.info("Configuring OmniAuth for #{provider} with #{provider_config}")
|
162
|
+
|
163
|
+
app.config.middleware.use OmniAuth::Builder do
|
164
|
+
provider options[:strategy], provider_config
|
165
|
+
end
|
166
|
+
|
167
|
+
OmniAuth.config.allowed_request_methods = %i[post get] if provider == :google
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# https://guides.rubyonrails.org/configuring.html#config-exceptions-app
|
2
|
+
module PandaCms
|
3
|
+
class ExceptionsApp
|
4
|
+
def initialize(exceptions_app:)
|
5
|
+
@exceptions_app = exceptions_app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
request = ActionDispatch::Request.new(env)
|
10
|
+
|
11
|
+
fallback_to_html_format_if_invalid_mime_type(request)
|
12
|
+
|
13
|
+
@exceptions_app.call(env)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def fallback_to_html_format_if_invalid_mime_type(request)
|
19
|
+
request.formats
|
20
|
+
rescue ActionDispatch::Http::MimeNegotiation::InvalidType
|
21
|
+
request.set_header "CONTENT_TYPE", "text/html"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/panda_cms.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "panda_cms/version"
|
2
|
+
require "panda_cms/engine"
|
3
|
+
|
4
|
+
module PandaCms
|
5
|
+
mattr_accessor :admin_path
|
6
|
+
mattr_accessor :authentication
|
7
|
+
mattr_accessor :require_login_to_view
|
8
|
+
mattr_accessor :title
|
9
|
+
mattr_accessor :url
|
10
|
+
|
11
|
+
def self.admin_path_symbol
|
12
|
+
@@admin_path.delete_prefix("/")
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "tailwindcss-rails"
|
2
|
+
require "shellwords"
|
3
|
+
|
4
|
+
ENV["TAILWIND_PATH"] ||= Tailwindcss::Engine.root.join("exe/tailwindcss").to_s
|
5
|
+
|
6
|
+
namespace :panda_cms do
|
7
|
+
namespace :assets do
|
8
|
+
desc "Build admin assets for Panda CMS"
|
9
|
+
task :admin do
|
10
|
+
run_tailwind(
|
11
|
+
root_path: PandaCms::Engine.root,
|
12
|
+
input_path: "app/assets/stylesheets/panda_cms/application.tailwind.css",
|
13
|
+
output_path: "app/assets/builds/panda_cms.css"
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Build dummy assets for Panda CMS"
|
18
|
+
task :dummy do
|
19
|
+
run_tailwind(
|
20
|
+
root_path: Rails.application.root,
|
21
|
+
input_path: "app/assets/stylesheets/application.tailwind.css",
|
22
|
+
output_path: "app/assets/builds/application.css",
|
23
|
+
config_path: "config/tailwind.config.js"
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "Watch admin assets for Panda CMS"
|
28
|
+
task :watch_admin do
|
29
|
+
run_tailwind(
|
30
|
+
root_path: PandaCms::Engine.root,
|
31
|
+
input_path: "app/assets/stylesheets/panda_cms/application.tailwind.css",
|
32
|
+
output_path: "app/assets/builds/panda_cms.css",
|
33
|
+
watch: true
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "Watch dummy assets for Panda CMS"
|
38
|
+
task :watch_dummy do
|
39
|
+
run_tailwind(
|
40
|
+
root_path: Rails.application.root,
|
41
|
+
input_path: "app/assets/stylesheets/application.tailwind.css",
|
42
|
+
output_path: "app/assets/builds/application.css",
|
43
|
+
config_path: "config/tailwind.config.js",
|
44
|
+
watch: true
|
45
|
+
)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
task default: [:spec, :panda_cms]
|
51
|
+
|
52
|
+
def run_tailwind(root_path:, input_path: nil, output_path: nil, config_path: nil, watch: false)
|
53
|
+
Rails.logger = Logger.new($stdout)
|
54
|
+
config_path ||= root_path.join("config/tailwind.config.js")
|
55
|
+
|
56
|
+
command = [
|
57
|
+
ENV["TAILWIND_PATH"],
|
58
|
+
"-i #{root_path.join(input_path)}",
|
59
|
+
"-o #{root_path.join(output_path)}",
|
60
|
+
"-c #{root_path.join(config_path)}",
|
61
|
+
"-m"
|
62
|
+
]
|
63
|
+
|
64
|
+
command << "-w" if watch
|
65
|
+
|
66
|
+
exec command.join(" ")
|
67
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
<%%= form_with(model: <%= model_resource_name %>, class: "contents") do |form| %>
|
2
|
+
<%% if <%= singular_table_name %>.errors.any? %>
|
3
|
+
<div id="error_explanation" class="py-2 px-3 mt-3 font-medium text-red-500 bg-red-50 rounded-lg">
|
4
|
+
<h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
|
5
|
+
|
6
|
+
<ul>
|
7
|
+
<%% <%= singular_table_name %>.errors.each do |error| %>
|
8
|
+
<li><%%= error.full_message %></li>
|
9
|
+
<%% end %>
|
10
|
+
</ul>
|
11
|
+
</div>
|
12
|
+
<%% end %>
|
13
|
+
|
14
|
+
<% attributes.each do |attribute| -%>
|
15
|
+
<div class="my-5">
|
16
|
+
<% if attribute.password_digest? -%>
|
17
|
+
<%%= form.label :password %>
|
18
|
+
<%%= form.password_field :password, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
|
19
|
+
</div>
|
20
|
+
|
21
|
+
<div class="my-5">
|
22
|
+
<%%= form.label :password_confirmation %>
|
23
|
+
<%%= form.password_field :password_confirmation, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
|
24
|
+
<% elsif attribute.attachments? -%>
|
25
|
+
<%%= form.label :<%= attribute.column_name %> %>
|
26
|
+
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, multiple: true, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
|
27
|
+
<% else -%>
|
28
|
+
<%%= form.label :<%= attribute.column_name %> %>
|
29
|
+
<% if attribute.field_type == :text_area -%>
|
30
|
+
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, rows: 4, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
|
31
|
+
<% elsif attribute.field_type == :check_box -%>
|
32
|
+
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, class: "block mt-2 h-5 w-5" %>
|
33
|
+
<% else -%>
|
34
|
+
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
|
35
|
+
<% end -%>
|
36
|
+
<% end -%>
|
37
|
+
</div>
|
38
|
+
|
39
|
+
<% end -%>
|
40
|
+
<div class="inline">
|
41
|
+
<%%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
|
42
|
+
</div>
|
43
|
+
<%% end %>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<div class="mx-auto w-full md:w-2/3">
|
2
|
+
<h1 class="text-4xl font-bold">Editing <%= human_name.downcase %></h1>
|
3
|
+
|
4
|
+
<%%= render "form", <%= singular_table_name %>: @<%= singular_table_name %> %>
|
5
|
+
|
6
|
+
<%%= link_to "Show this <%= human_name.downcase %>", @<%= singular_table_name %>, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
7
|
+
<%%= link_to "Back to <%= human_name.pluralize.downcase %>", <%= index_helper %>_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
8
|
+
</div>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<div class="w-full">
|
2
|
+
<%% if notice.present? %>
|
3
|
+
<p class="inline-block py-2 px-3 mb-5 font-medium text-green-500 bg-green-50 rounded-lg" id="notice"><%%= notice %></p>
|
4
|
+
<%% end %>
|
5
|
+
|
6
|
+
<div class="flex justify-between items-center">
|
7
|
+
<h1 class="text-4xl font-bold"><%= human_name.pluralize %></h1>
|
8
|
+
<%%= link_to "New <%= human_name.downcase %>", new_<%= singular_route_name %>_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<div id="<%= plural_table_name %>" class="min-w-full">
|
12
|
+
<%%= render @<%= plural_table_name %> %>
|
13
|
+
</div>
|
14
|
+
</div>
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<div class="mx-auto w-full md:w-2/3">
|
2
|
+
<h1 class="text-4xl font-bold">New <%= human_name.downcase %></h1>
|
3
|
+
|
4
|
+
<%%= render "form", <%= singular_table_name %>: @<%= singular_table_name %> %>
|
5
|
+
|
6
|
+
<%%= link_to "Back to <%= human_name.pluralize.downcase %>", <%= index_helper %>_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
7
|
+
</div>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<div id="<%%= dom_id <%= singular_name %> %>">
|
2
|
+
<% attributes.reject(&:password_digest?).each do |attribute| -%>
|
3
|
+
<p class="my-5">
|
4
|
+
<strong class="block mb-1 font-medium"><%= attribute.human_name %>:</strong>
|
5
|
+
<% if attribute.attachment? -%>
|
6
|
+
<%%= link_to <%= singular_name %>.<%= attribute.column_name %>.filename, <%= singular_name %>.<%= attribute.column_name %> if <%= singular_name %>.<%= attribute.column_name %>.attached? %>
|
7
|
+
<% elsif attribute.attachments? -%>
|
8
|
+
<%% <%= singular_name %>.<%= attribute.column_name %>.each do |<%= attribute.singular_name %>| %>
|
9
|
+
<div><%%= link_to <%= attribute.singular_name %>.filename, <%= attribute.singular_name %> %></div>
|
10
|
+
<%% end %>
|
11
|
+
<% else -%>
|
12
|
+
<%%= <%= singular_name %>.<%= attribute.column_name %> %>
|
13
|
+
<% end -%>
|
14
|
+
</p>
|
15
|
+
|
16
|
+
<% end -%>
|
17
|
+
<%% if action_name != "show" %>
|
18
|
+
<%%= link_to "Show this <%= human_name.downcase %>", <%= singular_name %>, class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
19
|
+
<%%= link_to "Edit this <%= human_name.downcase %>", edit_<%= singular_name %>_path(<%= singular_name %>), class: "rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium" %>
|
20
|
+
<hr class="mt-6">
|
21
|
+
<%% end %>
|
22
|
+
</div>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<div class="flex mx-auto w-full md:w-2/3">
|
2
|
+
<div class="mx-auto">
|
3
|
+
<%% if notice.present? %>
|
4
|
+
<p class="inline-block py-2 px-3 mb-5 font-medium text-green-500 bg-green-50 rounded-lg" id="notice"><%%= notice %></p>
|
5
|
+
<%% end %>
|
6
|
+
|
7
|
+
<%%= render @<%= singular_table_name %> %>
|
8
|
+
|
9
|
+
<%%= link_to "Edit this <%= singular_table_name %>", edit_<%= singular_table_name %>_path(@<%= singular_table_name %>), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
10
|
+
<div class="inline-block ml-2">
|
11
|
+
<%%= button_to "Destroy this <%= singular_table_name %>", <%= singular_table_name %>_path(@<%= singular_table_name %>), method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" %>
|
12
|
+
</div>
|
13
|
+
<%%= link_to "Back to <%= plural_table_name %>", <%= index_helper %>_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
14
|
+
</div>
|
15
|
+
</div>
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,212 @@
|
|
1
|
+
/**
|
2
|
+
* Represents a controller for managing editable content within an iframe.
|
3
|
+
* @class
|
4
|
+
*/
|
5
|
+
class EditableController {
|
6
|
+
/**
|
7
|
+
* Represents the constructor for the Editable class.
|
8
|
+
* @param {HTMLIFrameElement} frame - The iFrame element to be used for editing.
|
9
|
+
*/
|
10
|
+
constructor(pageId, frame) {
|
11
|
+
this.pageId = pageId;
|
12
|
+
this.frame = frame;
|
13
|
+
this.frame.style.display = "none";
|
14
|
+
this.csrfToken = document.querySelector('meta[name="csrf-token"]').content;
|
15
|
+
|
16
|
+
this.frame.addEventListener("load", () => {
|
17
|
+
this.frameDocument =
|
18
|
+
this.frame.contentDocument || this.frame.contentWindow.document;
|
19
|
+
this.body = this.frameDocument.body;
|
20
|
+
this.head = this.frameDocument.head;
|
21
|
+
this.loadEvents();
|
22
|
+
});
|
23
|
+
}
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Load events for the editable iFrame
|
27
|
+
*/
|
28
|
+
loadEvents() {
|
29
|
+
console.debug("[Panda CMS] iFrame loaded...");
|
30
|
+
this.embedExternalResources();
|
31
|
+
this.styleEditableElements();
|
32
|
+
}
|
33
|
+
|
34
|
+
embedExternalResources() {
|
35
|
+
this.addStylesheet(
|
36
|
+
this.frameDocument,
|
37
|
+
this.head,
|
38
|
+
"https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.bubble.css"
|
39
|
+
)
|
40
|
+
.then(() => {
|
41
|
+
return this.loadScript(
|
42
|
+
this.frameDocument,
|
43
|
+
this.head,
|
44
|
+
"https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js"
|
45
|
+
);
|
46
|
+
})
|
47
|
+
.then(() => {
|
48
|
+
return this.loadScript(
|
49
|
+
this.frameDocument,
|
50
|
+
this.head,
|
51
|
+
"https://unpkg.com/quill-image-compress@1.2.11/dist/quill.imageCompressor.min.js"
|
52
|
+
);
|
53
|
+
})
|
54
|
+
.then(() => {
|
55
|
+
return this.loadScript(
|
56
|
+
this.frameDocument,
|
57
|
+
this.head,
|
58
|
+
"https://unpkg.com/quill-magic-url@3.0.0/dist/index.js"
|
59
|
+
);
|
60
|
+
})
|
61
|
+
.then(() => {
|
62
|
+
return this.loadScript(
|
63
|
+
this.frameDocument,
|
64
|
+
this.head,
|
65
|
+
"https://cdn.jsdelivr.net/npm/quill-markdown-shortcuts@latest/dist/markdownShortcuts.js"
|
66
|
+
);
|
67
|
+
})
|
68
|
+
.then(() => {
|
69
|
+
console.debug(
|
70
|
+
"[Panda CMS] Dispatching event: pandaCmsExternalResourcesLoaded"
|
71
|
+
);
|
72
|
+
|
73
|
+
// Let the parent know that the external resources have been loaded
|
74
|
+
this.frameDocument.dispatchEvent(
|
75
|
+
new Event("pandaCmsExternalResourcesLoaded")
|
76
|
+
);
|
77
|
+
|
78
|
+
// Autosave the editable elements
|
79
|
+
this.autosaveElements();
|
80
|
+
|
81
|
+
// This prevents the flash of content before the iFrame is ready
|
82
|
+
this.frame.style.display = "";
|
83
|
+
});
|
84
|
+
}
|
85
|
+
|
86
|
+
/**
|
87
|
+
* Autosave the editable elements in the iFrame
|
88
|
+
* @todo #TODO Only do this when we have "autosave" mode enabled?
|
89
|
+
*/
|
90
|
+
autosaveElements() {
|
91
|
+
// Grab each element that's editable and append a save handler to it
|
92
|
+
var elements = this.frameDocument.querySelectorAll(".ql-editor");
|
93
|
+
elements.forEach((element) => {
|
94
|
+
element.addEventListener("blur", (event) => {
|
95
|
+
var target = event.target;
|
96
|
+
var blockContentId = target.parentElement.getAttribute(
|
97
|
+
"data-block-content-id"
|
98
|
+
);
|
99
|
+
|
100
|
+
this.bindSaveHandler(blockContentId, target.innerHTML);
|
101
|
+
});
|
102
|
+
});
|
103
|
+
}
|
104
|
+
|
105
|
+
/**
|
106
|
+
* Styles the editable elements in the iFrame
|
107
|
+
*/
|
108
|
+
styleEditableElements() {
|
109
|
+
var style = this.frameDocument.createElement("style");
|
110
|
+
style.innerHTML = `.ql-container,
|
111
|
+
.ql-editor, .ql-editor * {
|
112
|
+
margin: inherit !important;
|
113
|
+
padding: inherit !important;
|
114
|
+
font-size: inherit !important;
|
115
|
+
line-height: inherit !important;
|
116
|
+
font-family: inherit !important;
|
117
|
+
}
|
118
|
+
|
119
|
+
.ql-toolbar {
|
120
|
+
background-color: #282e3270;
|
121
|
+
border-radius: 10px;
|
122
|
+
}
|
123
|
+
|
124
|
+
.ql-editor:hover, .ql-container:hover {
|
125
|
+
cursor: pointer !important;
|
126
|
+
}
|
127
|
+
|
128
|
+
.ql-editor:hover, .ql-editor:focus, .ql-editor:active {
|
129
|
+
background-color: #e1effa;
|
130
|
+
cursor: pointer !important;
|
131
|
+
font-size: inherit !important;
|
132
|
+
-webkit-transition: background-color 500ms linear;
|
133
|
+
-ms-transition: background-color 500ms linear;
|
134
|
+
transition: background-color 500ms linear;
|
135
|
+
}
|
136
|
+
|
137
|
+
.ql-editor.success {
|
138
|
+
background-color: #66bd6a50 !important;
|
139
|
+
-webkit-transition: background-color 1000ms linear;
|
140
|
+
-ms-transition: background-color 1000ms linear;
|
141
|
+
transition: background-color 1000ms linear;
|
142
|
+
}
|
143
|
+
|
144
|
+
.ql-tooltip {
|
145
|
+
z-index: 999;
|
146
|
+
min-width: 90vw;
|
147
|
+
}`;
|
148
|
+
|
149
|
+
this.head.append(style);
|
150
|
+
}
|
151
|
+
|
152
|
+
bindSaveHandler(blockContentId, content) {
|
153
|
+
console.debug(`[Panda CMS] Calling save handler for ${blockContentId}...`);
|
154
|
+
fetch(`/admin/pages/${this.pageId}/block_contents/${blockContentId}`, {
|
155
|
+
method: "PATCH",
|
156
|
+
headers: {
|
157
|
+
"Content-Type": "application/json",
|
158
|
+
"X-CSRF-Token": this.csrfToken,
|
159
|
+
},
|
160
|
+
body: JSON.stringify({ content: content }),
|
161
|
+
})
|
162
|
+
.then((response) => response.json())
|
163
|
+
.then((data) => {
|
164
|
+
var editableBlock = this.frameDocument.querySelector(
|
165
|
+
`[data-block-content-id="${blockContentId}"] .ql-editor`
|
166
|
+
);
|
167
|
+
editableBlock.classList.add("success");
|
168
|
+
setTimeout(() => {
|
169
|
+
editableBlock.classList.remove("success");
|
170
|
+
}, 1500);
|
171
|
+
})
|
172
|
+
.catch((error) => {
|
173
|
+
console.log(error);
|
174
|
+
alert("Error updating. Please contact the support team!", error);
|
175
|
+
});
|
176
|
+
}
|
177
|
+
|
178
|
+
addStylesheet(frameDocument, head, href) {
|
179
|
+
return new Promise(function (resolve, reject) {
|
180
|
+
let link = frameDocument.createElement("link");
|
181
|
+
link.rel = "stylesheet";
|
182
|
+
link.href = href;
|
183
|
+
link.media = "none";
|
184
|
+
head.append(link);
|
185
|
+
|
186
|
+
link.onload = () => {
|
187
|
+
if (link.media != "all") {
|
188
|
+
link.media = "all";
|
189
|
+
}
|
190
|
+
console.debug(`[Panda CMS] Stylesheet loaded: ${href}`);
|
191
|
+
resolve(link);
|
192
|
+
};
|
193
|
+
link.onerror = () =>
|
194
|
+
reject(new Error(`[Panda CMS] Stylesheet load error for ${href}`));
|
195
|
+
});
|
196
|
+
}
|
197
|
+
|
198
|
+
loadScript(frameDocument, head, src) {
|
199
|
+
return new Promise(function (resolve, reject) {
|
200
|
+
let script = frameDocument.createElement("script");
|
201
|
+
script.src = src;
|
202
|
+
head.append(script);
|
203
|
+
|
204
|
+
script.onload = () => {
|
205
|
+
console.debug(`[Panda CMS] Script loaded: ${src}`);
|
206
|
+
resolve(script);
|
207
|
+
};
|
208
|
+
script.onerror = () =>
|
209
|
+
reject(new Error(`[Panda CMS] Script load error for ${src}`));
|
210
|
+
});
|
211
|
+
}
|
212
|
+
}
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|