panda-cms 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +83 -4
- data/app/components/panda/cms/code_component.rb +117 -39
- data/app/components/panda/cms/grid_component.rb +26 -6
- data/app/components/panda/cms/menu_component.rb +66 -34
- data/app/components/panda/cms/page_menu_component.rb +94 -13
- data/app/components/panda/cms/rich_text_component.rb +198 -140
- data/app/components/panda/cms/text_component.rb +77 -44
- data/app/controllers/panda/cms/admin/base_controller.rb +19 -3
- data/app/controllers/panda/cms/admin/dashboard_controller.rb +3 -3
- data/app/controllers/panda/cms/admin/files_controller.rb +7 -0
- data/app/controllers/panda/cms/admin/menus_controller.rb +47 -3
- data/app/controllers/panda/cms/admin/pages_controller.rb +6 -1
- data/app/controllers/panda/cms/pages_controller.rb +2 -2
- data/app/helpers/panda/cms/application_helper.rb +15 -1
- data/app/helpers/panda/cms/asset_helper.rb +14 -3
- data/app/javascript/panda/cms/application_panda_cms.js +1 -1
- data/app/javascript/panda/cms/controllers/code_editor_controller.js +95 -0
- data/app/javascript/panda/cms/controllers/file_gallery_controller.js +128 -0
- data/app/javascript/panda/cms/controllers/index.js +48 -13
- data/app/javascript/panda/cms/controllers/inline_code_editor_controller.js +96 -0
- data/app/javascript/panda/cms/controllers/menu_form_controller.js +40 -0
- data/app/javascript/panda/cms/controllers/nested_form_controller.js +35 -0
- data/app/javascript/panda/cms/controllers/tree_controller.js +214 -0
- data/app/javascript/panda/cms/stimulus-loading.js +5 -7
- data/app/models/panda/cms/block_content.rb +9 -0
- data/app/models/panda/cms/page.rb +41 -0
- data/app/models/panda/cms/post.rb +1 -0
- data/app/views/panda/cms/admin/dashboard/show.html.erb +5 -5
- data/app/views/panda/cms/admin/files/_file_details.html.erb +45 -0
- data/app/views/panda/cms/admin/files/index.html.erb +11 -118
- data/app/views/panda/cms/admin/forms/index.html.erb +2 -2
- data/app/views/panda/cms/admin/forms/new.html.erb +1 -2
- data/app/views/panda/cms/admin/forms/show.html.erb +15 -30
- data/app/views/panda/cms/admin/menus/_menu_item_fields.html.erb +11 -0
- data/app/views/panda/cms/admin/menus/edit.html.erb +64 -0
- data/app/views/panda/cms/admin/menus/index.html.erb +3 -2
- data/app/views/panda/cms/admin/menus/new.html.erb +40 -0
- data/app/views/panda/cms/admin/pages/edit.html.erb +15 -9
- data/app/views/panda/cms/admin/pages/index.html.erb +49 -11
- data/app/views/panda/cms/admin/pages/new.html.erb +3 -11
- data/app/views/panda/cms/admin/posts/_form.html.erb +4 -14
- data/app/views/panda/cms/admin/posts/edit.html.erb +2 -2
- data/app/views/panda/cms/admin/posts/index.html.erb +3 -3
- data/app/views/panda/cms/admin/posts/new.html.erb +1 -1
- data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +1 -1
- data/app/views/panda/cms/admin/settings/index.html.erb +3 -3
- data/config/importmap.rb +4 -6
- data/config/initializers/panda/cms/healthcheck_log_silencer.rb.disabled +31 -0
- data/config/initializers/panda/cms.rb +52 -10
- data/config/routes.rb +4 -2
- data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +9 -2
- data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +6 -1
- data/db/migrate/20250809231125_migrate_users_to_panda_core.rb +23 -21
- data/db/migrate/20251104150640_add_cached_last_updated_at_to_panda_cms_pages.rb +22 -0
- data/db/migrate/20251104172242_add_page_type_to_panda_cms_pages.rb +6 -0
- data/db/migrate/20251104172638_set_page_types_for_existing_pages.rb +27 -0
- data/db/migrate/20251105000001_add_pending_review_status_to_pages_and_posts.panda_cms.rb +21 -0
- data/lib/generators/panda/cms/install_generator.rb +2 -5
- data/lib/panda/cms/asset_loader.rb +36 -16
- data/lib/panda/cms/debug.rb +29 -0
- data/lib/panda/cms/engine.rb +107 -48
- data/lib/panda/cms/features.rb +52 -0
- data/lib/panda-cms/version.rb +1 -1
- data/lib/panda-cms.rb +5 -6
- data/lib/tasks/assets.rake +5 -52
- data/lib/tasks/panda_cms_tasks.rake +16 -0
- metadata +22 -29
- data/app/components/panda/cms/admin/container_component.html.erb +0 -13
- data/app/components/panda/cms/admin/flash_message_component.html.erb +0 -31
- data/app/components/panda/cms/admin/panel_component.html.erb +0 -7
- data/app/components/panda/cms/admin/slideover_component.html.erb +0 -9
- data/app/components/panda/cms/admin/slideover_component.rb +0 -15
- data/app/components/panda/cms/admin/statistics_component.html.erb +0 -4
- data/app/components/panda/cms/admin/statistics_component.rb +0 -16
- data/app/components/panda/cms/admin/tab_bar_component.html.erb +0 -35
- data/app/components/panda/cms/admin/tab_bar_component.rb +0 -15
- data/app/components/panda/cms/admin/table_component.html.erb +0 -29
- data/app/components/panda/cms/admin/user_activity_component.html.erb +0 -7
- data/app/components/panda/cms/admin/user_activity_component.rb +0 -20
- data/app/components/panda/cms/admin/user_display_component.html.erb +0 -17
- data/app/components/panda/cms/admin/user_display_component.rb +0 -21
- data/app/components/panda/cms/grid_component.html.erb +0 -6
- data/app/components/panda/cms/menu_component.html.erb +0 -6
- data/app/components/panda/cms/page_menu_component.html.erb +0 -21
- data/app/components/panda/cms/rich_text_component.html.erb +0 -90
- data/app/views/layouts/panda/cms/application.html.erb +0 -42
- data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +0 -28
- data/app/views/panda/cms/admin/shared/_flash.html.erb +0 -5
- data/app/views/panda/cms/admin/shared/_sidebar.html.erb +0 -41
- data/app/views/panda/cms/shared/_footer.html.erb +0 -2
- data/app/views/panda/cms/shared/_header.html.erb +0 -25
- data/config/initializers/panda/cms/healthcheck_log_silencer.rb +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a5d7ab51a41583d6af2b9c03249894b81eb3301e8a88068bf512fea5ba10891a
|
|
4
|
+
data.tar.gz: 9cbbd69b5b6e6ce229fef244135e7abb1e12580be401524b054e94ff227280cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 84f46d5e374a05e046047e7e7751371f4b90d2e86a137df179c02e6e89c08169048e64c18ef38b0b78ab5137c22f55d0777451ab49bb32ab3aa3c67745b6aaed
|
|
7
|
+
data.tar.gz: 0b6ad4481db935ed61ca57d087582e8b370fe2ecb6b0e15dadeaa53042a834dbecaf9cd7d0122830788e10b071cce66290d0abd6a0b4649adf23465140854069
|
data/README.md
CHANGED
|
@@ -30,7 +30,17 @@ Then run `bin/dev`. You'll see a basic website has automatically been created fo
|
|
|
30
30
|
|
|
31
31
|
The easiest way for you to get started is to visit http://localhost:3000/admin and login with your GitHub credentials. As the first user, you'll automatically have an administrator account created.
|
|
32
32
|
|
|
33
|
-
When you're ready to configure further, you can set your own configuration in `config/initializers/panda
|
|
33
|
+
When you're ready to configure further, you can set your own configuration in `config/initializers/panda.rb`. Make sure to configure your authentication providers and update the domain restriction!
|
|
34
|
+
|
|
35
|
+
## Panda CMS Pro
|
|
36
|
+
|
|
37
|
+
Commercial features such as structured **Collections** live in the `panda-cms-pro` gem. Once the pro gem is installed you can:
|
|
38
|
+
|
|
39
|
+
- Model repeatable content with collections and items.
|
|
40
|
+
- Loop over entries inside layouts using helpers like `panda_cms_collection_items("trustees")`.
|
|
41
|
+
- Keep the feature hidden in open-source installs thanks to `Panda::CMS::Features`.
|
|
42
|
+
|
|
43
|
+
See `docs/collections.md` for the editor workflow and template examples, and `docs/private-gem-server.md` for hosting the private gem server.
|
|
34
44
|
|
|
35
45
|
### Existing applications
|
|
36
46
|
|
|
@@ -44,14 +54,83 @@ For initial setup, run:
|
|
|
44
54
|
|
|
45
55
|
```shell
|
|
46
56
|
bundle install
|
|
47
|
-
rails generate
|
|
48
|
-
rails
|
|
57
|
+
rails generate panda:cms:install
|
|
58
|
+
rails panda:cms:install:migrations
|
|
59
|
+
rails db:migrate
|
|
49
60
|
rails db:seed
|
|
50
61
|
```
|
|
51
62
|
|
|
52
63
|
You may want to check this does not re-run any of your existing seeds!
|
|
53
64
|
|
|
54
|
-
If you don't want to use GitHub to login (or are at a URL other than http://localhost:3000/), you'll need to configure
|
|
65
|
+
If you don't want to use GitHub to login (or are at a URL other than http://localhost:3000/), you'll need to configure authentication providers (in `config/initializers/panda.rb`), and then set your user's `admin` attribute to `true` once you've first tried to login.
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
All Panda configuration is managed in `config/initializers/panda.rb`. The generator creates this file with sensible defaults including Google OAuth authentication:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
# config/initializers/panda.rb
|
|
73
|
+
Panda::Core.configure do |config|
|
|
74
|
+
config.admin_path = "/admin"
|
|
75
|
+
|
|
76
|
+
config.login_page_title = "Panda Admin"
|
|
77
|
+
config.admin_title = "Panda Admin"
|
|
78
|
+
|
|
79
|
+
config.authentication_providers = {
|
|
80
|
+
google_oauth2: {
|
|
81
|
+
enabled: true,
|
|
82
|
+
name: "Google",
|
|
83
|
+
client_id: Rails.application.credentials.dig(:google, :client_id),
|
|
84
|
+
client_secret: Rails.application.credentials.dig(:google, :client_secret),
|
|
85
|
+
options: {
|
|
86
|
+
scope: "email,profile",
|
|
87
|
+
prompt: "select_account",
|
|
88
|
+
hd: "yourdomain.com" # Restrict to specific domain
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Core settings
|
|
94
|
+
config.session_token_cookie = :panda_session
|
|
95
|
+
config.user_class = "Panda::Core::User"
|
|
96
|
+
config.user_identity_class = "Panda::Core::UserIdentity"
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Important**: Update `hd: "yourdomain.com"` to your organization's domain to restrict admin access, or remove this line to allow any Google account.
|
|
101
|
+
|
|
102
|
+
See the [Configuration Documentation](docs/developers/configuration/) for detailed information on all available settings.
|
|
103
|
+
|
|
104
|
+
### Engine Mounting
|
|
105
|
+
|
|
106
|
+
**The Panda CMS engine automatically mounts itself** via an `after_initialize` hook. You do **not** need to manually add `mount Panda::CMS::Engine => "/"` to your routes file. The engine will:
|
|
107
|
+
|
|
108
|
+
- Mount itself at the root path
|
|
109
|
+
- Add admin routes under the configured admin path (e.g., `/admin/cms` or `/manage/cms`)
|
|
110
|
+
- Set up a catch-all route for CMS pages (excluding admin paths)
|
|
111
|
+
|
|
112
|
+
The admin interface structure will be:
|
|
113
|
+
- `{admin_path}` - Panda Core admin dashboard (authentication, profile)
|
|
114
|
+
- `{admin_path}/cms` - Panda CMS admin (pages, posts, menus, files)
|
|
115
|
+
|
|
116
|
+
## Styling
|
|
117
|
+
|
|
118
|
+
**Panda CMS does not compile or manage its own CSS.** All admin interface styling is provided by [Panda Core](https://github.com/tastybamboo/panda-core).
|
|
119
|
+
|
|
120
|
+
The CMS automatically loads Core's compiled stylesheet:
|
|
121
|
+
|
|
122
|
+
```erb
|
|
123
|
+
<link rel="stylesheet" href="/panda-core-assets/panda-core.css">
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Core's Rack middleware serves this file from the gem, so:
|
|
127
|
+
- ✅ No CSS copying or compilation needed
|
|
128
|
+
- ✅ Styles update automatically when Core updates
|
|
129
|
+
- ✅ Consistent design across all Panda gems
|
|
130
|
+
|
|
131
|
+
For details on customizing styles, development workflows, and troubleshooting, see [docs/STYLING.md](docs/STYLING.md).
|
|
132
|
+
|
|
133
|
+
For CSS compilation (when contributing to styling), see [Panda Core Asset Compilation Guide](https://github.com/tastybamboo/panda-core/blob/main/docs/ASSET_COMPILATION.md).
|
|
55
134
|
|
|
56
135
|
## Gotchas
|
|
57
136
|
|
|
@@ -2,68 +2,146 @@
|
|
|
2
2
|
|
|
3
3
|
module Panda
|
|
4
4
|
module CMS
|
|
5
|
-
#
|
|
6
|
-
# @param key [Symbol] The key to use for the
|
|
7
|
-
# @param text [String] The text to display
|
|
8
|
-
# @param editable [Boolean] If the
|
|
9
|
-
|
|
10
|
-
class CodeComponent < ViewComponent::Base
|
|
5
|
+
# Code component for editable HTML/code content
|
|
6
|
+
# @param key [Symbol] The key to use for the code component
|
|
7
|
+
# @param text [String] The default text to display
|
|
8
|
+
# @param editable [Boolean] If the code is editable or not (defaults to true)
|
|
9
|
+
class CodeComponent < Panda::Core::Base
|
|
11
10
|
KIND = "code"
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@options = options || {}
|
|
17
|
-
@options[:id] ||= "code-#{key.to_s.dasherize}"
|
|
18
|
-
@editable = editable
|
|
12
|
+
prop :key, Symbol, default: :text_component
|
|
13
|
+
prop :text, String, default: ""
|
|
14
|
+
prop :editable, _Boolean, default: true
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
def view_template
|
|
17
|
+
if @editable_state
|
|
18
|
+
render_editable_view
|
|
19
|
+
else
|
|
20
|
+
raw(@code_content.to_s.html_safe)
|
|
21
|
+
end
|
|
22
|
+
rescue => e
|
|
23
|
+
handle_error(e)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def before_template
|
|
27
|
+
raise BlockError, "Key 'code' is not allowed for CodeComponent" if @key == :code
|
|
28
|
+
prepare_content
|
|
21
29
|
end
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def prepare_content
|
|
34
|
+
@editable_state = component_is_editable?
|
|
35
|
+
|
|
36
|
+
block = find_block
|
|
37
|
+
return false if block.nil?
|
|
38
|
+
|
|
39
|
+
block_content = find_block_content(block)
|
|
40
|
+
@code_content = block_content&.content.to_s
|
|
41
|
+
@block_content_id = block_content&.id
|
|
42
|
+
end
|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
44
|
+
def find_block
|
|
45
|
+
Panda::CMS::Block.find_by(
|
|
46
|
+
kind: KIND,
|
|
47
|
+
key: @key,
|
|
48
|
+
panda_cms_template_id: Current.page.panda_cms_template_id
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def find_block_content(block)
|
|
53
|
+
block.block_contents.find_by(panda_cms_page_id: Current.page.id)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def render_editable_view
|
|
57
|
+
div(class: "code-component-wrapper mb-4", data: {controller: "inline-code-editor", inline_code_editor_page_id_value: Current.page.id, inline_code_editor_block_content_id_value: @block_content_id}) do
|
|
58
|
+
# Tab Navigation
|
|
59
|
+
div(class: "border-b border-gray-200 bg-white") do
|
|
60
|
+
nav(class: "-mb-px flex space-x-4 px-4", "aria-label": "Tabs") do
|
|
61
|
+
button(type: "button",
|
|
62
|
+
data: {inline_code_editor_target: "previewTab", action: "click->inline-code-editor#showPreview"},
|
|
63
|
+
class: "border-primary text-primary whitespace-nowrap border-b-2 py-2 px-1 text-sm font-medium") do
|
|
64
|
+
plain "Preview"
|
|
65
|
+
end
|
|
66
|
+
button(type: "button",
|
|
67
|
+
data: {inline_code_editor_target: "codeTab", action: "click->inline-code-editor#showCode"},
|
|
68
|
+
class: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap border-b-2 py-2 px-1 text-sm font-medium") do
|
|
69
|
+
plain "Code"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
31
72
|
end
|
|
32
73
|
|
|
33
|
-
|
|
74
|
+
# Preview View
|
|
75
|
+
div(data: {inline_code_editor_target: "previewView"}, class: "mt-2") do
|
|
76
|
+
div(**preview_attrs) { raw(@code_content.to_s.html_safe) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Code Editor View
|
|
80
|
+
div(data: {inline_code_editor_target: "codeView"}, class: "mt-2 hidden bg-white p-4 border border-gray-200") do
|
|
81
|
+
textarea(
|
|
82
|
+
data: {inline_code_editor_target: "codeInput"},
|
|
83
|
+
class: "w-full h-64 p-3 font-mono text-sm border border-gray-300 rounded focus:ring-primary focus:border-primary",
|
|
84
|
+
placeholder: "Enter your HTML/embed code here..."
|
|
85
|
+
) { raw(@code_content.to_s) }
|
|
86
|
+
|
|
87
|
+
div(class: "mt-3 flex justify-end space-x-2") do
|
|
88
|
+
button(type: "button",
|
|
89
|
+
data: {action: "click->inline-code-editor#saveCode"},
|
|
90
|
+
class: "inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500") do
|
|
91
|
+
plain "💾 Save Code"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
div(data: {inline_code_editor_target: "saveMessage"}, class: "hidden mt-2")
|
|
96
|
+
end
|
|
34
97
|
end
|
|
98
|
+
end
|
|
35
99
|
|
|
36
|
-
|
|
37
|
-
|
|
100
|
+
def preview_attrs
|
|
101
|
+
{
|
|
102
|
+
class: "p-4 border border-dashed border-gray-300 bg-gray-50 min-h-32"
|
|
103
|
+
}
|
|
104
|
+
end
|
|
38
105
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
106
|
+
def element_attrs
|
|
107
|
+
{
|
|
108
|
+
id: "editor-#{@block_content_id}",
|
|
109
|
+
contenteditable: "plaintext-only",
|
|
110
|
+
class: "block bg-yellow-50 font-mono text-xs p-2 border-2 border-yellow-700",
|
|
111
|
+
style: "white-space: pre-wrap;",
|
|
112
|
+
data: {
|
|
42
113
|
"editable-kind": "html",
|
|
43
114
|
"editable-page-id": Current.page.id,
|
|
44
|
-
"editable-block-content-id":
|
|
115
|
+
"editable-block-content-id": @block_content_id
|
|
45
116
|
}
|
|
46
|
-
|
|
47
|
-
@options[:style] = "white-space: pre-wrap;"
|
|
48
|
-
|
|
49
|
-
@options[:id] = "editor-#{block_content&.id}"
|
|
50
|
-
|
|
51
|
-
# TODO: Switch between the HTML and the preview?
|
|
52
|
-
content_tag(:div, code_content, @options, true)
|
|
53
|
-
else
|
|
54
|
-
code_content.html_safe
|
|
55
|
-
end
|
|
117
|
+
}.merge(@attrs)
|
|
56
118
|
end
|
|
57
119
|
|
|
58
120
|
def component_is_editable?
|
|
59
121
|
# TODO: Permissions
|
|
60
|
-
@editable && is_embedded? && Current.user&.admin
|
|
122
|
+
@editable && is_embedded? && Current.user&.admin?
|
|
61
123
|
end
|
|
62
124
|
|
|
63
125
|
def is_embedded?
|
|
64
126
|
# TODO: Check security on this - embed_id should match something?
|
|
65
|
-
request.params[:embed_id].present?
|
|
127
|
+
view_context.request.params[:embed_id].present?
|
|
66
128
|
end
|
|
129
|
+
|
|
130
|
+
def handle_error(error)
|
|
131
|
+
Rails.logger.error "CodeComponent error: #{error.message}"
|
|
132
|
+
Rails.logger.error error.backtrace.join("\n")
|
|
133
|
+
|
|
134
|
+
if Rails.env.production?
|
|
135
|
+
false
|
|
136
|
+
else
|
|
137
|
+
div(class: "p-4 bg-red-50 border border-red-200 rounded") do
|
|
138
|
+
p(class: "text-red-800 font-semibold") { "CodeComponent Error" }
|
|
139
|
+
p(class: "text-red-600 text-sm") { error.message }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
class BlockError < StandardError; end
|
|
67
145
|
end
|
|
68
146
|
end
|
|
69
147
|
end
|
|
@@ -2,14 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
module Panda
|
|
4
4
|
module CMS
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
# Grid layout component for creating column-based layouts
|
|
6
|
+
# @param columns [Integer] Number of grid columns
|
|
7
|
+
# @param spans [Array<Integer>] Array of column span values for each grid cell
|
|
8
|
+
class GridComponent < Panda::Core::Base
|
|
9
|
+
prop :columns, Integer, default: 1
|
|
10
|
+
prop :spans, Array, default: -> { [1].freeze }
|
|
11
|
+
|
|
12
|
+
def view_template
|
|
13
|
+
div(class: "w-full grid #{grid_columns_class} min-h-20") do
|
|
14
|
+
column_span_classes.each do |colspan|
|
|
15
|
+
div(
|
|
16
|
+
class: "border border-red-500 bg-red-50 #{colspan}",
|
|
17
|
+
onDragOver: "parent.onDragOver(event);",
|
|
18
|
+
onDrop: "parent.onDrop(event);"
|
|
19
|
+
)
|
|
20
|
+
end
|
|
11
21
|
end
|
|
12
22
|
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def grid_columns_class
|
|
27
|
+
"grid-cols-#{columns}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def column_span_classes
|
|
31
|
+
spans.map { |span| "col-span-#{span}" }
|
|
32
|
+
end
|
|
13
33
|
end
|
|
14
34
|
end
|
|
15
35
|
end
|
|
@@ -2,40 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
module Panda
|
|
4
4
|
module CMS
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
5
|
+
# Menu component for rendering navigational menus
|
|
6
|
+
# @param name [String] The name of the menu to render
|
|
7
|
+
# @param current_path [String] The current request path for highlighting active items
|
|
8
|
+
# @param styles [Hash] CSS classes for menu items (default, active, inactive)
|
|
9
|
+
# @param overrides [Hash] Menu item overrides (currently unused)
|
|
10
|
+
# @param render_page_menu [Boolean] Whether to render sub-page menus
|
|
11
|
+
# @param page_menu_styles [Hash] Styles for the page menu component
|
|
12
|
+
class MenuComponent < Panda::Core::Base
|
|
13
|
+
prop :name, String
|
|
14
|
+
prop :current_path, String, default: ""
|
|
15
|
+
prop :styles, Hash, default: -> { {}.freeze }
|
|
16
|
+
prop :overrides, Hash, default: -> { {}.freeze }
|
|
17
|
+
prop :render_page_menu, _Boolean, default: false
|
|
18
|
+
prop :page_menu_styles, Hash, default: -> { {}.freeze }
|
|
19
|
+
|
|
20
|
+
def view_template
|
|
21
|
+
return unless @menu
|
|
22
|
+
|
|
23
|
+
@processed_menu_items.each do |menu_item|
|
|
24
|
+
a(href: menu_item.resolved_link, class: menu_item.css_classes) { menu_item.text }
|
|
25
|
+
|
|
26
|
+
if @render_page_menu && menu_item.page
|
|
27
|
+
render Panda::CMS::PageMenuComponent.new(
|
|
28
|
+
page: menu_item.page,
|
|
29
|
+
start_depth: 1,
|
|
30
|
+
styles: @page_menu_styles,
|
|
31
|
+
show_heading: false
|
|
32
|
+
)
|
|
29
33
|
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def before_template
|
|
38
|
+
load_menu_items
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
30
42
|
|
|
43
|
+
def load_menu_items
|
|
44
|
+
@menu = Panda::CMS::Menu.find_by(name: @name)
|
|
45
|
+
return unless @menu
|
|
46
|
+
|
|
47
|
+
menu_items = @menu.menu_items
|
|
48
|
+
menu_items = menu_items.where("depth <= ?", @menu.depth) if @menu.depth
|
|
49
|
+
menu_items = menu_items.order(:lft)
|
|
50
|
+
|
|
51
|
+
@processed_menu_items = menu_items.map do |menu_item|
|
|
52
|
+
add_css_classes_to_item(menu_item)
|
|
31
53
|
menu_item
|
|
32
54
|
end
|
|
33
55
|
|
|
34
|
-
#
|
|
35
|
-
|
|
56
|
+
# Load current page for page menu rendering
|
|
57
|
+
if @render_page_menu
|
|
58
|
+
@current_page = Panda::CMS::Page.find_by(path: @current_path)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def add_css_classes_to_item(menu_item)
|
|
63
|
+
css_class = if is_active?(menu_item)
|
|
64
|
+
"#{@styles[:default]} #{@styles[:active]}"
|
|
65
|
+
else
|
|
66
|
+
"#{@styles[:default]} #{@styles[:inactive]}"
|
|
67
|
+
end
|
|
36
68
|
|
|
37
|
-
|
|
38
|
-
@page_menu_styles = page_menu_styles
|
|
69
|
+
menu_item.define_singleton_method(:css_classes) { css_class }
|
|
39
70
|
end
|
|
40
71
|
|
|
41
72
|
def is_active?(menu_item)
|
|
@@ -46,13 +77,14 @@ module Panda
|
|
|
46
77
|
end
|
|
47
78
|
|
|
48
79
|
def active_link?(path, match: :starts_with)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
80
|
+
case match
|
|
81
|
+
when :starts_with
|
|
82
|
+
@current_path.starts_with?(path)
|
|
83
|
+
when :exact
|
|
84
|
+
@current_path == path
|
|
85
|
+
else
|
|
86
|
+
false
|
|
53
87
|
end
|
|
54
|
-
|
|
55
|
-
false
|
|
56
88
|
end
|
|
57
89
|
end
|
|
58
90
|
end
|
|
@@ -2,35 +2,116 @@
|
|
|
2
2
|
|
|
3
3
|
module Panda
|
|
4
4
|
module CMS
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
# Page menu component for rendering hierarchical page navigation
|
|
6
|
+
# @param page [Panda::CMS::Page] The current page
|
|
7
|
+
# @param start_depth [Integer] The depth level to start the menu from
|
|
8
|
+
# @param styles [Hash] CSS classes for styling menu elements
|
|
9
|
+
# @param show_heading [Boolean] Whether to show the top-level heading
|
|
10
|
+
class PageMenuComponent < Panda::Core::Base
|
|
11
|
+
prop :page, Object
|
|
12
|
+
prop :start_depth, Integer
|
|
13
|
+
prop :styles, Hash, default: -> { {}.freeze }
|
|
14
|
+
prop :show_heading, _Boolean, default: true
|
|
7
15
|
|
|
8
|
-
def
|
|
9
|
-
|
|
16
|
+
def view_template
|
|
17
|
+
return unless should_render?
|
|
10
18
|
|
|
19
|
+
nav(class: @styles[:container]) do
|
|
20
|
+
ul(role: "list", class: "p-0 m-0") do
|
|
21
|
+
render_heading if @show_heading
|
|
22
|
+
render_menu_items
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def before_template
|
|
11
28
|
return if @page.nil?
|
|
12
29
|
|
|
13
|
-
start_page = if @page.depth == start_depth
|
|
30
|
+
@start_page = if @page.depth == @start_depth
|
|
14
31
|
@page
|
|
15
32
|
else
|
|
16
|
-
@page.ancestors.find { |anc| anc.depth == start_depth }
|
|
33
|
+
@page.ancestors.find { |anc| anc.depth == @start_depth }
|
|
17
34
|
end
|
|
18
35
|
|
|
19
|
-
menu = start_page&.page_menu
|
|
36
|
+
menu = @start_page&.page_menu
|
|
20
37
|
return if menu.nil?
|
|
21
38
|
|
|
22
39
|
@menu_item = menu.menu_items.order(:lft)&.first
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# Set some default styles for sanity
|
|
27
|
-
@styles = styles
|
|
28
|
-
@styles[:indent_with] ||= "pl-2"
|
|
41
|
+
# Set default styles if not already set
|
|
42
|
+
@styles[:indent_with] ||= "pl-2" if @styles
|
|
29
43
|
end
|
|
30
44
|
|
|
31
|
-
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def should_render?
|
|
32
48
|
@page&.path != "/" && @menu_item.present?
|
|
33
49
|
end
|
|
50
|
+
|
|
51
|
+
def render_heading
|
|
52
|
+
li do
|
|
53
|
+
a(
|
|
54
|
+
href: @menu_item.page.path,
|
|
55
|
+
class: heading_class
|
|
56
|
+
) { @menu_item.text }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def heading_class
|
|
61
|
+
if @menu_item.page == Panda::CMS::Current.page
|
|
62
|
+
@styles[:current_page_active]
|
|
63
|
+
else
|
|
64
|
+
@styles[:current_page_inactive]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def render_menu_items
|
|
69
|
+
ul do
|
|
70
|
+
Panda::CMS::MenuItem.includes(:page).each_with_level(@menu_item.descendants) do |submenu_item, level|
|
|
71
|
+
next if should_skip_item?(submenu_item, level)
|
|
72
|
+
|
|
73
|
+
render_menu_item(submenu_item, level)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def should_skip_item?(submenu_item, level)
|
|
79
|
+
# Skip if we're on the top menu item and level > 1
|
|
80
|
+
return true if Panda::CMS::Current.page == @menu_item.page && level > 1
|
|
81
|
+
|
|
82
|
+
# Skip if path contains parameter placeholder
|
|
83
|
+
return true if submenu_item.page&.path&.include?(":")
|
|
84
|
+
|
|
85
|
+
# Skip if page is nil or Current.page is nil
|
|
86
|
+
return true if submenu_item&.page.nil? || Panda::CMS::Current.page.nil?
|
|
87
|
+
|
|
88
|
+
# Skip if submenu page is deeper than current page and not an ancestor
|
|
89
|
+
(submenu_item.page&.depth&.to_i&.> Panda::CMS::Current.page&.depth&.to_i) &&
|
|
90
|
+
!Panda::CMS::Current.page&.in?(submenu_item.page.ancestors)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def render_menu_item(submenu_item, level)
|
|
94
|
+
li(
|
|
95
|
+
data: {
|
|
96
|
+
level: level,
|
|
97
|
+
page_id: submenu_item.page.id
|
|
98
|
+
},
|
|
99
|
+
class: menu_item_class(submenu_item)
|
|
100
|
+
) do
|
|
101
|
+
a(
|
|
102
|
+
href: submenu_item.page&.path,
|
|
103
|
+
class: view_context.menu_indent(submenu_item, indent_with: @styles[:indent_with])
|
|
104
|
+
) { submenu_item.page&.title }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def menu_item_class(submenu_item)
|
|
109
|
+
if submenu_item.page == Panda::CMS::Current.page
|
|
110
|
+
@styles[:active]
|
|
111
|
+
else
|
|
112
|
+
@styles[:inactive]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
34
115
|
end
|
|
35
116
|
end
|
|
36
117
|
end
|