panda-core 0.12.5 → 1.1.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/LICENSE +1 -1
- data/README.md +53 -0
- data/app/assets/builds/panda_core_admin.css +2 -0
- data/app/assets/tailwind/application.css +236 -36
- data/app/builders/panda/core/form_builder.rb +120 -18
- data/app/components/panda/core/UI/badge.html.erb +10 -0
- data/app/components/panda/core/UI/badge.rb +14 -30
- data/app/components/panda/core/UI/button.html.erb +5 -0
- data/app/components/panda/core/UI/button.rb +11 -15
- data/app/components/panda/core/UI/card.html.erb +3 -0
- data/app/components/panda/core/UI/card.rb +7 -6
- data/app/components/panda/core/admin/attribute_diff_component.html.erb +21 -0
- data/app/components/panda/core/admin/attribute_diff_component.rb +29 -0
- data/app/components/panda/core/admin/breadcrumb_component.html.erb +36 -0
- data/app/components/panda/core/admin/breadcrumb_component.rb +12 -64
- data/app/components/panda/core/admin/breadcrumbs_component.html.erb +16 -0
- data/app/components/panda/core/admin/breadcrumbs_component.rb +26 -0
- data/app/components/panda/core/admin/button_component.html.erb +9 -0
- data/app/components/panda/core/admin/button_component.rb +36 -31
- data/app/components/panda/core/admin/callout_component.html.erb +17 -0
- data/app/components/panda/core/admin/callout_component.rb +57 -0
- data/app/components/panda/core/admin/code_block_component.html.erb +9 -0
- data/app/components/panda/core/admin/code_block_component.rb +23 -0
- data/app/components/panda/core/admin/container_component.html.erb +27 -0
- data/app/components/panda/core/admin/container_component.rb +11 -77
- data/app/components/panda/core/admin/contributors_summary_component.html.erb +15 -0
- data/app/components/panda/core/admin/contributors_summary_component.rb +23 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.html.erb +70 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.rb +32 -0
- data/app/components/panda/core/admin/delete_button_component.html.erb +4 -0
- data/app/components/panda/core/admin/delete_button_component.rb +41 -0
- data/app/components/panda/core/admin/dropdown_button_component.html.erb +28 -0
- data/app/components/panda/core/admin/dropdown_button_component.rb +100 -0
- data/app/components/panda/core/admin/dropdown_component.html.erb +32 -0
- data/app/components/panda/core/admin/dropdown_component.rb +65 -0
- data/app/components/panda/core/admin/empty_state_component.html.erb +16 -0
- data/app/components/panda/core/admin/empty_state_component.rb +25 -0
- data/app/components/panda/core/admin/file_gallery_component.html.erb +58 -0
- data/app/components/panda/core/admin/file_gallery_component.rb +26 -98
- data/app/components/panda/core/admin/filter_pills_component.html.erb +6 -0
- data/app/components/panda/core/admin/filter_pills_component.rb +29 -0
- data/app/components/panda/core/admin/flash_message_component.html.erb +11 -0
- data/app/components/panda/core/admin/flash_message_component.rb +33 -60
- data/app/components/panda/core/admin/form_error_component.html.erb +7 -0
- data/app/components/panda/core/admin/form_error_component.rb +8 -16
- data/app/components/panda/core/admin/form_footer_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_footer_component.rb +65 -0
- data/app/components/panda/core/admin/form_input_component.html.erb +1 -0
- data/app/components/panda/core/admin/form_input_component.rb +21 -13
- data/app/components/panda/core/admin/form_section_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_section_component.rb +64 -0
- data/app/components/panda/core/admin/form_select_component.html.erb +5 -0
- data/app/components/panda/core/admin/form_select_component.rb +25 -26
- data/app/components/panda/core/admin/heading_component.html.erb +26 -0
- data/app/components/panda/core/admin/heading_component.rb +23 -60
- data/app/components/panda/core/admin/login_form_component.html.erb +38 -0
- data/app/components/panda/core/admin/login_form_component.rb +28 -0
- data/app/components/panda/core/admin/main_layout_component.html.erb +88 -0
- data/app/components/panda/core/admin/main_layout_component.rb +19 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.html.erb +41 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.rb +27 -0
- data/app/components/panda/core/admin/navigation/item_component.html.erb +34 -0
- data/app/components/panda/core/admin/navigation/item_component.rb +69 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.html.erb +9 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.rb +44 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.html.erb +30 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.rb +36 -0
- data/app/components/panda/core/admin/page_header_component.html.erb +28 -0
- data/app/components/panda/core/admin/page_header_component.rb +19 -35
- data/app/components/panda/core/admin/pagination_component.html.erb +33 -0
- data/app/components/panda/core/admin/pagination_component.rb +56 -0
- data/app/components/panda/core/admin/panel_component.html.erb +19 -0
- data/app/components/panda/core/admin/panel_component.rb +4 -29
- data/app/components/panda/core/admin/prose_panel_component.html.erb +6 -0
- data/app/components/panda/core/admin/prose_panel_component.rb +43 -0
- data/app/components/panda/core/admin/search_bar_component.html.erb +19 -0
- data/app/components/panda/core/admin/search_bar_component.rb +31 -0
- data/app/components/panda/core/admin/search_filter_bar_component.html.erb +22 -0
- data/app/components/panda/core/admin/search_filter_bar_component.rb +27 -0
- data/app/components/panda/core/admin/secret_field_component.html.erb +24 -0
- data/app/components/panda/core/admin/secret_field_component.rb +31 -0
- data/app/components/panda/core/admin/sidebar_component.html.erb +160 -0
- data/app/components/panda/core/admin/sidebar_component.rb +55 -0
- data/app/components/panda/core/admin/slideover_component.html.erb +67 -0
- data/app/components/panda/core/admin/slideover_component.rb +31 -70
- data/app/components/panda/core/admin/statistics_component.html.erb +4 -0
- data/app/components/panda/core/admin/statistics_component.rb +6 -8
- data/app/components/panda/core/admin/tab_bar_component.html.erb +48 -0
- data/app/components/panda/core/admin/tab_bar_component.rb +32 -67
- data/app/components/panda/core/admin/table_component.html.erb +82 -0
- data/app/components/panda/core/admin/table_component.rb +54 -87
- data/app/components/panda/core/admin/tag_badge_component.html.erb +7 -0
- data/app/components/panda/core/admin/tag_badge_component.rb +21 -0
- data/app/components/panda/core/admin/tag_component.html.erb +1 -0
- data/app/components/panda/core/admin/tag_component.rb +51 -20
- data/app/components/panda/core/admin/tag_input_component.html.erb +37 -0
- data/app/components/panda/core/admin/tag_input_component.rb +22 -0
- data/app/components/panda/core/admin/tags_panel_component.html.erb +8 -0
- data/app/components/panda/core/admin/tags_panel_component.rb +33 -0
- data/app/components/panda/core/admin/user_activity_component.html.erb +15 -0
- data/app/components/panda/core/admin/user_activity_component.rb +9 -22
- data/app/components/panda/core/admin/user_display_component.html.erb +8 -0
- data/app/components/panda/core/admin/user_display_component.rb +19 -22
- data/app/components/panda/core/base.rb +35 -42
- data/app/components/panda/core/shared/footer_component.html.erb +2 -0
- data/app/components/panda/core/shared/footer_component.rb +11 -0
- data/app/components/panda/core/shared/header_component.html.erb +32 -0
- data/app/components/panda/core/shared/header_component.rb +19 -0
- data/app/constraints/panda/core/admin_constraint.rb +2 -2
- data/app/controllers/concerns/panda/core/admin_authorization.rb +58 -0
- data/app/controllers/concerns/panda/core/authorizable.rb +107 -0
- data/app/controllers/panda/core/admin/base_controller.rb +62 -3
- data/app/controllers/panda/core/admin/editor/search_controller.rb +17 -0
- data/app/controllers/panda/core/admin/feature_flags_controller.rb +35 -0
- data/app/controllers/panda/core/admin/file_categories_controller.rb +84 -0
- data/app/controllers/panda/core/admin/files_controller.rb +230 -0
- data/app/controllers/panda/core/admin/import_sessions_controller.rb +120 -0
- data/app/controllers/panda/core/admin/my_profile/logins_controller.rb +24 -0
- data/app/controllers/panda/core/admin/my_profile_controller.rb +31 -0
- data/app/controllers/panda/core/admin/search_controller.rb +15 -0
- data/app/controllers/panda/core/admin/sessions_controller.rb +84 -10
- data/app/controllers/panda/core/admin/tags_controller.rb +77 -0
- data/app/controllers/panda/core/admin/test_sessions_controller.rb +5 -6
- data/app/controllers/panda/core/admin/users_controller.rb +184 -0
- data/app/controllers/panda/core/application_controller.rb +13 -1
- data/app/helpers/panda/core/active_storage_helper.rb +24 -0
- data/app/helpers/panda/core/feature_flag_helper.rb +11 -0
- data/app/helpers/panda/core/form_helper.rb +15 -0
- data/app/javascript/panda/core/controllers/avatar_upload_controller.js +73 -0
- data/app/javascript/panda/core/controllers/clipboard_controller.js +119 -0
- data/app/javascript/panda/core/controllers/collapsible_item_controller.js +45 -0
- data/app/javascript/panda/core/controllers/custom_select_controller.js +256 -0
- data/app/javascript/panda/core/controllers/datepicker_controller.js +121 -0
- data/app/javascript/panda/core/controllers/daterange_controller.js +193 -0
- data/app/javascript/panda/core/controllers/file_gallery_controller.js +134 -0
- data/app/javascript/panda/core/controllers/file_upload_controller.js +165 -0
- data/app/javascript/panda/core/controllers/global_search_controller.js +184 -0
- data/app/javascript/panda/core/controllers/index.js +36 -0
- data/app/javascript/panda/core/controllers/mobile_sidebar_controller.js +58 -0
- data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +49 -24
- data/app/javascript/panda/core/controllers/row_link_controller.js +27 -0
- data/app/javascript/panda/core/controllers/tag_input_controller.js +183 -0
- data/app/javascript/panda/core/tailwindcss-stimulus-components.js +2 -3
- data/app/javascript/panda/core/vendor/@fortawesome--fontawesome-free@7.2.0.js +8129 -0
- data/app/javascript/panda/core/vendor/@hotwired--turbo.js +7197 -100
- data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +511 -2
- data/app/javascript/panda/core/vendor/@tailwindplus--elements@1.0.22.js +65 -0
- data/app/javascript/panda/core/vendor/cropperjs@2.1.0.js +3 -0
- data/app/javascript/panda/core/vendor/vanilla-calendar-pro.js +2 -0
- data/app/models/concerns/panda/core/has_metadata.rb +164 -0
- data/app/models/concerns/panda/core/importable.rb +79 -0
- data/app/models/concerns/panda/core/searchable.rb +66 -0
- data/app/models/concerns/panda/core/taggable.rb +37 -0
- data/app/models/panda/core/feature_flag.rb +56 -0
- data/app/models/panda/core/file_categorization.rb +14 -0
- data/app/models/panda/core/file_category.rb +45 -0
- data/app/models/panda/core/import_session.rb +79 -0
- data/app/models/panda/core/presence.rb +24 -0
- data/app/models/panda/core/tag.rb +33 -0
- data/app/models/panda/core/tagging.rb +14 -0
- data/app/models/panda/core/user.rb +94 -16
- data/app/models/panda/core/user_activity.rb +42 -0
- data/app/models/panda/core/user_session.rb +35 -0
- data/app/services/panda/core/attach_avatar_service.rb +15 -3
- data/app/services/panda/core/csv_import_service.rb +65 -0
- data/app/services/panda/core/file_categorizer.rb +52 -0
- data/app/services/panda/core/file_parser.rb +76 -0
- data/app/services/panda/core/invite_user_service.rb +44 -0
- data/app/services/panda/core/presence_service.rb +49 -0
- data/app/views/layouts/panda/core/admin.html.erb +6 -76
- data/app/views/layouts/panda/core/admin_simple.html.erb +2 -2
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +1 -73
- data/app/views/panda/core/admin/dashboard/show.html.erb +12 -15
- data/app/views/panda/core/admin/feature_flags/index.html.erb +56 -0
- data/app/views/panda/core/admin/file_categories/_form.html.erb +20 -0
- data/app/views/panda/core/admin/file_categories/edit.html.erb +5 -0
- data/app/views/panda/core/admin/file_categories/index.html.erb +55 -0
- data/app/views/panda/core/admin/file_categories/new.html.erb +5 -0
- data/app/views/panda/core/admin/files/_file_details.html.erb +156 -0
- data/app/views/panda/core/admin/files/index.html.erb +61 -0
- data/app/views/panda/core/admin/import_sessions/column_map.html.erb +43 -0
- data/app/views/panda/core/admin/import_sessions/index.html.erb +48 -0
- data/app/views/panda/core/admin/import_sessions/new.html.erb +37 -0
- data/app/views/panda/core/admin/import_sessions/preview.html.erb +51 -0
- data/app/views/panda/core/admin/import_sessions/show.html.erb +87 -0
- data/app/views/panda/core/admin/my_profile/edit.html.erb +51 -39
- data/app/views/panda/core/admin/my_profile/logins/show.html.erb +72 -0
- data/app/views/panda/core/admin/sessions/new.html.erb +1 -37
- data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +1 -18
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +1 -99
- data/app/views/panda/core/admin/shared/_slideover.html.erb +3 -3
- data/app/views/panda/core/admin/tags/_form.html.erb +21 -0
- data/app/views/panda/core/admin/tags/edit.html.erb +4 -0
- data/app/views/panda/core/admin/tags/index.html.erb +41 -0
- data/app/views/panda/core/admin/tags/new.html.erb +4 -0
- data/app/views/panda/core/admin/users/activity.html.erb +90 -0
- data/app/views/panda/core/admin/users/edit.html.erb +48 -0
- data/app/views/panda/core/admin/users/index.html.erb +161 -0
- data/app/views/panda/core/admin/users/sessions.html.erb +103 -0
- data/app/views/panda/core/admin/users/show.html.erb +224 -0
- data/app/views/panda/core/shared/_footer.html.erb +1 -2
- data/app/views/panda/core/shared/_header.html.erb +4 -22
- data/config/brakeman.ignore +68 -31
- data/config/importmap.rb +13 -11
- data/config/routes.rb +49 -2
- data/db/migrate/20260202171614_create_panda_core_file_categories.rb +30 -0
- data/db/migrate/20260203000001_add_user_management_fields_to_panda_core_users.rb +21 -0
- data/db/migrate/20260203000002_create_panda_core_user_activities.rb +24 -0
- data/db/migrate/20260203000003_create_panda_core_user_sessions.rb +25 -0
- data/db/migrate/20260207100000_create_panda_core_presences.rb +21 -0
- data/db/migrate/20260213000001_fix_active_storage_attachments_record_id_type.rb +44 -0
- data/db/migrate/20260217000001_create_panda_core_feature_flags.rb +14 -0
- data/db/migrate/20260303000001_add_metadata_to_panda_core_users.rb +11 -0
- data/db/migrate/20260310000001_create_panda_core_tags.rb +27 -0
- data/db/migrate/20260310000002_create_panda_core_import_sessions.rb +29 -0
- data/lib/generators/panda/core/install_generator.rb +32 -0
- data/lib/generators/panda/core/templates/panda.rb +63 -0
- data/lib/panda/core/asset_loader.rb +5 -2
- data/lib/panda/core/authentication.rb +3 -3
- data/lib/panda/core/configuration.rb +195 -1
- data/lib/panda/core/engine/admin_controller_config.rb +3 -6
- data/lib/panda/core/engine/autoload_config.rb +21 -9
- data/lib/panda/core/engine/omniauth_config.rb +108 -38
- data/lib/panda/core/engine/pending_migrations_check.rb +46 -0
- data/lib/panda/core/engine/sqlite_schema_compatibility.rb +36 -0
- data/lib/panda/core/engine/{phlex_config.rb → view_component_config.rb} +5 -7
- data/lib/panda/core/engine.rb +90 -25
- data/lib/panda/core/middleware.rb +35 -0
- data/lib/panda/core/navigation_registry.rb +455 -0
- data/lib/panda/core/oauth_providers.rb +27 -0
- data/lib/panda/core/permission_registry.rb +89 -0
- data/lib/panda/core/search_registry.rb +69 -0
- data/lib/panda/core/seeds/file_categories.rb +30 -0
- data/lib/panda/core/shared/inflections_config.rb +1 -5
- data/lib/panda/core/testing/rails_helper.rb +87 -0
- data/lib/panda/core/testing/support/authentication_test_helpers.rb +22 -18
- data/lib/panda/core/testing/support/system/cuprite_helpers.rb +4 -4
- data/lib/panda/core/testing/view_component_test_controller.rb +19 -0
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core/widget_registry.rb +50 -0
- data/lib/panda/core.rb +17 -8
- data/lib/tasks/panda/core/file_categories.rake +41 -0
- data/public/panda-core-assets/panda-core-0.12.5.css +2 -0
- data/public/panda-core-assets/panda-core-0.13.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.4.css +2 -0
- data/public/panda-core-assets/panda-core.css +2 -2
- data/public/panda-core-assets/vanilla-calendar.css +47 -0
- metadata +203 -26
- data/lib/panda/core/engine/inflections_config.rb +0 -23
|
@@ -2,24 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
module Panda
|
|
4
4
|
module Core
|
|
5
|
-
# Base class for all
|
|
5
|
+
# Base class for all ViewComponent components in the Panda ecosystem.
|
|
6
6
|
#
|
|
7
7
|
# This base component provides:
|
|
8
|
-
# -
|
|
9
|
-
# - Tailwind CSS class merging
|
|
8
|
+
# - Tailwind CSS class merging via TailwindMerge
|
|
10
9
|
# - Attribute merging with sensible defaults
|
|
11
|
-
# - Rails helper integration
|
|
10
|
+
# - Rails helper integration (inherited from ViewComponent::Base)
|
|
12
11
|
# - Development-mode debugging comments
|
|
13
12
|
#
|
|
14
13
|
# @example Basic usage
|
|
15
14
|
# class MyComponent < Panda::Core::Base
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# div(**@attrs) { title }
|
|
15
|
+
# def initialize(title:, variant: :primary, **attrs)
|
|
16
|
+
# @title = title
|
|
17
|
+
# @variant = variant
|
|
18
|
+
# super(**attrs)
|
|
21
19
|
# end
|
|
22
20
|
#
|
|
21
|
+
# attr_reader :title, :variant
|
|
22
|
+
#
|
|
23
|
+
# private
|
|
24
|
+
#
|
|
23
25
|
# def default_attrs
|
|
24
26
|
# { class: "my-component my-component--#{variant}" }
|
|
25
27
|
# end
|
|
@@ -27,41 +29,42 @@ module Panda
|
|
|
27
29
|
#
|
|
28
30
|
# @example With attribute merging
|
|
29
31
|
# # Component definition
|
|
30
|
-
# class
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# button(**@attrs) { text }
|
|
32
|
+
# class ButtonComponent < Panda::Core::Base
|
|
33
|
+
# def initialize(text:, **attrs)
|
|
34
|
+
# @text = text
|
|
35
|
+
# super(**attrs)
|
|
35
36
|
# end
|
|
36
37
|
#
|
|
38
|
+
# attr_reader :text
|
|
39
|
+
#
|
|
40
|
+
# private
|
|
41
|
+
#
|
|
37
42
|
# def default_attrs
|
|
38
43
|
# { class: "btn btn-primary", type: "button" }
|
|
39
44
|
# end
|
|
40
45
|
# end
|
|
41
46
|
#
|
|
42
47
|
# # Usage - user attrs merge with defaults
|
|
43
|
-
# render
|
|
48
|
+
# render ButtonComponent.new(text: "Click me", class: "mt-4", type: "submit")
|
|
44
49
|
# # => <button type="submit" class="btn btn-primary mt-4">Click me</button>
|
|
45
50
|
#
|
|
46
|
-
class Base <
|
|
51
|
+
class Base < ViewComponent::Base
|
|
47
52
|
# Frozen instance of TailwindMerge for efficient class merging
|
|
48
53
|
TAILWIND_MERGER = ::TailwindMerge::Merger.new.freeze unless defined?(TAILWIND_MERGER)
|
|
49
54
|
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# Include Rails helpers for routes, etc.
|
|
54
|
-
include Phlex::Rails::Helpers::Routes
|
|
55
|
-
|
|
56
|
-
# Special handling for the attrs property - merges user attributes with defaults
|
|
57
|
-
# and intelligently handles Tailwind class merging
|
|
55
|
+
# Initialize the component with user-provided attributes.
|
|
56
|
+
# Subclasses should call super(**attrs) after setting their own instance variables.
|
|
58
57
|
#
|
|
59
|
-
# @param
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
merge_attrs(
|
|
58
|
+
# @param attrs [Hash] User-provided HTML attributes
|
|
59
|
+
def initialize(**user_attrs)
|
|
60
|
+
super()
|
|
61
|
+
@attrs = merge_attrs(user_attrs, default_attrs)
|
|
63
62
|
end
|
|
64
63
|
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
attr_reader :attrs
|
|
67
|
+
|
|
65
68
|
# Merges user-provided attributes with default attributes.
|
|
66
69
|
# Special handling for :class to merge Tailwind classes intelligently.
|
|
67
70
|
#
|
|
@@ -102,20 +105,10 @@ module Panda
|
|
|
102
105
|
{}
|
|
103
106
|
end
|
|
104
107
|
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
class_name = self.class.name
|
|
110
|
-
comment { "Begin #{class_name}" }
|
|
111
|
-
super
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def after_template
|
|
115
|
-
class_name = self.class.name
|
|
116
|
-
super
|
|
117
|
-
comment { "End #{class_name}" }
|
|
118
|
-
end
|
|
108
|
+
# ViewComponent automatically renders templates in the same directory
|
|
109
|
+
# We override call to make it public for testing
|
|
110
|
+
def call
|
|
111
|
+
super
|
|
119
112
|
end
|
|
120
113
|
end
|
|
121
114
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html data-theme="<%= Panda::Core::Current&.user&.current_theme || Panda::Core.config.default_theme %>" class="<%= @html_class %>">
|
|
3
|
+
<head>
|
|
4
|
+
<title><%= helpers.content_for?(:title) ? helpers.content_for(:title) : (Panda::Core.config.admin_title || "Panda Admin") %></title>
|
|
5
|
+
<%= helpers.csrf_meta_tags %>
|
|
6
|
+
<%= helpers.csp_meta_tag %>
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.1.0/css/all.min.css">
|
|
8
|
+
<%= helpers.panda_core_stylesheet %>
|
|
9
|
+
<link rel="stylesheet" href="/panda-core-assets/vanilla-calendar.css">
|
|
10
|
+
|
|
11
|
+
<% if Gem.loaded_specs["chartkick"] %>
|
|
12
|
+
<script src="/panda-core-assets/chartkick/Chart.bundle.js"></script>
|
|
13
|
+
<script src="/panda-core-assets/chartkick/chartkick.js"></script>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<!-- ES Module Shims polyfill - must load BEFORE importmap -->
|
|
17
|
+
<script async src="https://ga.jspm.io/npm:es-module-shims@2.6.2/dist/es-module-shims.js"></script>
|
|
18
|
+
|
|
19
|
+
<!-- Panda JavaScript (Core + all registered modules via ModuleRegistry) -->
|
|
20
|
+
<%= helpers.panda_core_javascript %>
|
|
21
|
+
|
|
22
|
+
<% if defined?(Panda::CMS) && helpers.controller.class.name.start_with?("Panda::CMS") %>
|
|
23
|
+
<%= render "panda/cms/shared/favicons" %>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<% if Panda::Core.config.additional_head_content.respond_to?(:call) %>
|
|
27
|
+
<%= Panda::Core.config.additional_head_content.call.html_safe %>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<%= helpers.content_for :head %>
|
|
31
|
+
</head>
|
|
32
|
+
<body class="overflow-hidden h-full <%= @body_class %>" data-environment="<%= Rails.env %>">
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Shared
|
|
6
|
+
# Header component for HTML document head
|
|
7
|
+
# Handles title, meta tags, stylesheets, and JavaScript
|
|
8
|
+
class HeaderComponent < ViewComponent::Base
|
|
9
|
+
def initialize(html_class: "", body_class: "", **attrs)
|
|
10
|
+
super()
|
|
11
|
+
@html_class = html_class
|
|
12
|
+
@body_class = body_class
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :html_class, :body_class
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -4,9 +4,9 @@ module Panda
|
|
|
4
4
|
module Core
|
|
5
5
|
class AdminConstraint
|
|
6
6
|
def matches?(request)
|
|
7
|
-
return false unless request.session[
|
|
7
|
+
return false unless request.session[Panda::Core::ADMIN_SESSION_KEY].present?
|
|
8
8
|
|
|
9
|
-
user = User.find_by(id: request.session[
|
|
9
|
+
user = User.find_by(id: request.session[Panda::Core::ADMIN_SESSION_KEY])
|
|
10
10
|
user&.admin?
|
|
11
11
|
end
|
|
12
12
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
# Automatic permission enforcement for admin controllers.
|
|
6
|
+
#
|
|
7
|
+
# Reads from {PermissionRegistry} to determine which permission is required
|
|
8
|
+
# for the current controller action, then delegates to +authorize!+ (from
|
|
9
|
+
# {Authorizable}) to check the configured +authorization_policy+.
|
|
10
|
+
#
|
|
11
|
+
# This concern is included in {Admin::BaseController} so all admin
|
|
12
|
+
# controllers automatically get permission enforcement. No per-controller
|
|
13
|
+
# configuration is needed — just register permissions in the registry.
|
|
14
|
+
#
|
|
15
|
+
# Admin users (+user.admin?+) bypass all checks via {Authorizable#authorize!}.
|
|
16
|
+
#
|
|
17
|
+
# == Behavior for unmapped controllers/actions
|
|
18
|
+
#
|
|
19
|
+
# * Controller not in registry → access allowed (open by default)
|
|
20
|
+
# * Action not in registry → access allowed
|
|
21
|
+
# * Action mapped → +authorize!(permission_key)+ is called
|
|
22
|
+
#
|
|
23
|
+
# == Example
|
|
24
|
+
#
|
|
25
|
+
# # Register permissions (in an engine or initializer):
|
|
26
|
+
# Panda::Core::PermissionRegistry.register(
|
|
27
|
+
# "Panda::CMS::Admin::PagesController",
|
|
28
|
+
# index: :edit_content, destroy: :delete_content
|
|
29
|
+
# )
|
|
30
|
+
#
|
|
31
|
+
# # Enforcement happens automatically via before_action
|
|
32
|
+
module AdminAuthorization
|
|
33
|
+
extend ActiveSupport::Concern
|
|
34
|
+
|
|
35
|
+
included do
|
|
36
|
+
before_action :enforce_registry_permissions!
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def enforce_registry_permissions!
|
|
42
|
+
# Admin users bypass all checks (handled by authorize! but skip lookup too)
|
|
43
|
+
return if current_user&.admin?
|
|
44
|
+
|
|
45
|
+
required_permission = Panda::Core::PermissionRegistry.permission_for(
|
|
46
|
+
self.class.name,
|
|
47
|
+
action_name.to_sym
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# No registration = allowed
|
|
51
|
+
return unless required_permission
|
|
52
|
+
|
|
53
|
+
# Delegate to Authorizable#authorize! which checks authorization_policy
|
|
54
|
+
authorize!(required_permission)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
# Authorizable concern for admin controllers.
|
|
6
|
+
#
|
|
7
|
+
# Provides a generic authorization layer that delegates permission checks
|
|
8
|
+
# to the configurable `Panda::Core.config.authorization_policy` lambda.
|
|
9
|
+
# This allows downstream gems (e.g. panda-cms-pro) to inject RBAC checks
|
|
10
|
+
# without panda-core needing any knowledge of roles or permissions.
|
|
11
|
+
#
|
|
12
|
+
# Usage in controllers:
|
|
13
|
+
#
|
|
14
|
+
# class PagesController < Admin::BaseController
|
|
15
|
+
# require_permission :edit_content, only: [:edit, :update]
|
|
16
|
+
# require_permission :publish_content, only: [:publish]
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Or manually:
|
|
20
|
+
#
|
|
21
|
+
# def update
|
|
22
|
+
# authorize!(:edit_content, @page)
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
module Authorizable
|
|
26
|
+
extend ActiveSupport::Concern
|
|
27
|
+
|
|
28
|
+
included do
|
|
29
|
+
helper_method :can? if respond_to?(:helper_method)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class_methods do
|
|
33
|
+
# DSL for declarative permission checks.
|
|
34
|
+
# Registers a before_action that calls authorize! for the given permission.
|
|
35
|
+
#
|
|
36
|
+
# @param permission [Symbol] the permission key to check
|
|
37
|
+
# @param options [Hash] standard before_action options (only:, except:, etc.)
|
|
38
|
+
def require_permission(permission, **options)
|
|
39
|
+
before_action(**options) do
|
|
40
|
+
authorize!(permission)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check whether the current user is authorized for the given action.
|
|
46
|
+
#
|
|
47
|
+
# @param action [Symbol] the action/permission to check
|
|
48
|
+
# @param resource [Object, nil] optional resource context
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def authorized_for?(action, resource = nil)
|
|
51
|
+
return false unless current_user
|
|
52
|
+
|
|
53
|
+
# Admin users bypass all authorization checks
|
|
54
|
+
return true if current_user.admin?
|
|
55
|
+
|
|
56
|
+
# Delegate to the configured authorization policy
|
|
57
|
+
policy = Panda::Core.config.authorization_policy
|
|
58
|
+
policy.call(current_user, action, resource)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Authorize the current user or render a 403/redirect.
|
|
62
|
+
#
|
|
63
|
+
# @param action [Symbol] the action/permission to check
|
|
64
|
+
# @param resource [Object, nil] optional resource context
|
|
65
|
+
# @raise renders 403 or redirects if not authorized
|
|
66
|
+
def authorize!(action, resource = nil)
|
|
67
|
+
return if authorized_for?(action, resource)
|
|
68
|
+
|
|
69
|
+
respond_to do |format|
|
|
70
|
+
format.html do
|
|
71
|
+
flash[:error] = "You do not have permission to perform this action."
|
|
72
|
+
redirect_to(request.referer || panda_core.admin_root_path)
|
|
73
|
+
end
|
|
74
|
+
format.json do
|
|
75
|
+
render json: {error: "Forbidden", status: 403}, status: :forbidden
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# View-layer helper for checking permissions.
|
|
81
|
+
# Can be used in templates: `if can?(:edit_content)`
|
|
82
|
+
#
|
|
83
|
+
# @param action [Symbol] the action/permission to check
|
|
84
|
+
# @param resource [Object, nil] optional resource context
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def can?(action, resource = nil)
|
|
87
|
+
authorized_for?(action, resource)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check whether the current user is authorized to access the admin panel.
|
|
91
|
+
# This is called during authentication to allow non-admin users with roles
|
|
92
|
+
# to access the admin area.
|
|
93
|
+
#
|
|
94
|
+
# @return [Boolean]
|
|
95
|
+
def authorized_for_admin_access?
|
|
96
|
+
return false unless current_user
|
|
97
|
+
|
|
98
|
+
# Admin users always have access
|
|
99
|
+
return true if current_user.admin?
|
|
100
|
+
|
|
101
|
+
# Check the authorization policy for :access_admin
|
|
102
|
+
policy = Panda::Core.config.authorization_policy
|
|
103
|
+
policy.call(current_user, :access_admin, nil)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -6,20 +6,31 @@ module Panda
|
|
|
6
6
|
# Base controller for all admin interfaces across Panda gems
|
|
7
7
|
# Provides authentication, helpers, and hooks for extending functionality
|
|
8
8
|
class BaseController < ::ActionController::Base
|
|
9
|
+
include Panda::Core::Authorizable
|
|
10
|
+
|
|
9
11
|
layout "panda/core/admin"
|
|
10
12
|
|
|
11
13
|
protect_from_forgery with: :exception
|
|
12
14
|
|
|
15
|
+
default_form_builder Panda::Core::FormBuilder
|
|
16
|
+
|
|
13
17
|
# Add flash types for improved alert support with Tailwind
|
|
14
18
|
add_flash_types :success, :warning, :error, :info
|
|
15
19
|
|
|
16
20
|
# Include helper modules
|
|
17
21
|
helper Panda::Core::SessionsHelper
|
|
22
|
+
helper Panda::Core::FormHelper
|
|
23
|
+
helper Panda::Core::FeatureFlagHelper
|
|
18
24
|
helper Panda::Core::AssetHelper if defined?(Panda::Core::AssetHelper)
|
|
25
|
+
helper Panda::Core::ActiveStorageHelper
|
|
19
26
|
|
|
20
27
|
before_action :set_current_request_details
|
|
21
28
|
before_action :authenticate_admin_user!
|
|
22
29
|
|
|
30
|
+
# Must be included AFTER the authentication before_actions above,
|
|
31
|
+
# because enforce_registry_permissions! needs current_user to be set.
|
|
32
|
+
include Panda::Core::AdminAuthorization
|
|
33
|
+
|
|
23
34
|
helper_method :breadcrumbs
|
|
24
35
|
helper_method :current_user
|
|
25
36
|
helper_method :user_signed_in?
|
|
@@ -40,7 +51,9 @@ module Panda
|
|
|
40
51
|
Panda::Core::Current.user_agent = request.user_agent
|
|
41
52
|
Panda::Core::Current.ip_address = request.ip
|
|
42
53
|
Panda::Core::Current.root = request.base_url
|
|
43
|
-
Panda::Core::Current.user ||= Panda::Core::User.find_by(id: session[
|
|
54
|
+
Panda::Core::Current.user ||= Panda::Core::User.find_by(id: session[Panda::Core::ADMIN_SESSION_KEY]) if session[Panda::Core::ADMIN_SESSION_KEY]
|
|
55
|
+
|
|
56
|
+
track_session_activity if Panda::Core::Current.user
|
|
44
57
|
end
|
|
45
58
|
|
|
46
59
|
def authenticate_user!
|
|
@@ -48,10 +61,25 @@ module Panda
|
|
|
48
61
|
end
|
|
49
62
|
|
|
50
63
|
def authenticate_admin_user!
|
|
51
|
-
|
|
64
|
+
unless user_signed_in?
|
|
65
|
+
# Preserve the requested path so it can be restored after authentication.
|
|
66
|
+
# Skip login/auth paths to avoid redirect loops.
|
|
67
|
+
admin_prefix = Core.config.admin_path
|
|
68
|
+
if request.path.start_with?(admin_prefix) &&
|
|
69
|
+
!request.path.start_with?("#{admin_prefix}/login") &&
|
|
70
|
+
!request.path.start_with?("#{admin_prefix}/auth/")
|
|
71
|
+
session[:post_auth_redirect_path] = request.path
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
redirect_to panda_core.admin_login_path,
|
|
75
|
+
flash: {error: "Please login to view this!"}
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
return if authorized_for_admin_access?
|
|
52
80
|
|
|
53
81
|
redirect_to panda_core.admin_login_path,
|
|
54
|
-
flash: {error: "
|
|
82
|
+
flash: {error: "You are not authorized to access the admin area."}
|
|
55
83
|
end
|
|
56
84
|
|
|
57
85
|
# Required for paper_trail and seems as good as convention these days
|
|
@@ -62,6 +90,37 @@ module Panda
|
|
|
62
90
|
def user_signed_in?
|
|
63
91
|
!!Panda::Core::Current.user
|
|
64
92
|
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def track_session_activity
|
|
97
|
+
return unless session.id.present?
|
|
98
|
+
|
|
99
|
+
last_tracked = session[:_session_tracked_at]
|
|
100
|
+
return if last_tracked && Time.current.to_i - last_tracked < 60
|
|
101
|
+
|
|
102
|
+
begin
|
|
103
|
+
Panda::Core::UserSession.find_or_create_by(
|
|
104
|
+
session_id: session.id.to_s,
|
|
105
|
+
user: Panda::Core::Current.user
|
|
106
|
+
) do |us|
|
|
107
|
+
us.ip_address = request.remote_ip
|
|
108
|
+
us.user_agent = request.user_agent
|
|
109
|
+
end
|
|
110
|
+
Panda::Core::UserSession.where(
|
|
111
|
+
session_id: session.id.to_s,
|
|
112
|
+
user: Panda::Core::Current.user
|
|
113
|
+
).update_all(
|
|
114
|
+
last_active_at: Time.current,
|
|
115
|
+
active: true
|
|
116
|
+
)
|
|
117
|
+
session[:_session_tracked_at] = Time.current.to_i
|
|
118
|
+
rescue ActiveRecord::RecordNotUnique => e
|
|
119
|
+
Rails.logger.debug("Session already exists: #{e.message}")
|
|
120
|
+
rescue => e
|
|
121
|
+
Rails.logger.debug("Session tracking skipped: #{e.message}")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
65
124
|
end
|
|
66
125
|
end
|
|
67
126
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
module Editor
|
|
7
|
+
class SearchController < ::Panda::Core::Admin::BaseController
|
|
8
|
+
def index
|
|
9
|
+
query = params[:search].to_s.strip
|
|
10
|
+
items = Panda::Core::SearchRegistry.search(query, limit: 10)
|
|
11
|
+
render json: {success: true, items: items}
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class FeatureFlagsController < BaseController
|
|
7
|
+
before_action :set_initial_breadcrumb
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@feature_flags = FeatureFlag.order(:key)
|
|
11
|
+
@grouped_flags = @feature_flags.group_by { |flag| flag.key.split(".").first }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def update
|
|
15
|
+
@feature_flag = FeatureFlag.find(params[:id])
|
|
16
|
+
FeatureFlag.toggle!(@feature_flag.key)
|
|
17
|
+
|
|
18
|
+
new_state = @feature_flag.reload.enabled? ? "enabled" : "disabled"
|
|
19
|
+
flash[:success] = "Feature flag \"#{@feature_flag.key}\" has been #{new_state}."
|
|
20
|
+
redirect_to admin_feature_flags_path
|
|
21
|
+
rescue ActiveRecord::RecordNotFound
|
|
22
|
+
flash[:alert] = "Feature flag not found."
|
|
23
|
+
redirect_to admin_feature_flags_path
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def set_initial_breadcrumb
|
|
29
|
+
add_breadcrumb "Settings", admin_settings_path
|
|
30
|
+
add_breadcrumb "Feature Flags", admin_feature_flags_path
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
class FileCategoriesController < BaseController
|
|
7
|
+
before_action :set_initial_breadcrumb
|
|
8
|
+
before_action :set_file_category, only: %i[edit update destroy]
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
@file_categories = FileCategory.roots.ordered.includes(:children)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def new
|
|
15
|
+
@file_category = FileCategory.new
|
|
16
|
+
add_breadcrumb "New Category"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create
|
|
20
|
+
@file_category = FileCategory.new(file_category_params)
|
|
21
|
+
|
|
22
|
+
if @file_category.save
|
|
23
|
+
flash[:success] = "File category has been created successfully."
|
|
24
|
+
redirect_to admin_file_categories_path
|
|
25
|
+
else
|
|
26
|
+
add_breadcrumb "New Category"
|
|
27
|
+
render :new, status: :unprocessable_content
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def edit
|
|
32
|
+
if @file_category.system?
|
|
33
|
+
flash[:warning] = "System categories cannot be edited."
|
|
34
|
+
redirect_to admin_file_categories_path
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
add_breadcrumb @file_category.name
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def update
|
|
41
|
+
if @file_category.system?
|
|
42
|
+
flash[:warning] = "System categories cannot be edited."
|
|
43
|
+
redirect_to admin_file_categories_path
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if @file_category.update(file_category_params)
|
|
48
|
+
flash[:success] = "File category has been updated successfully."
|
|
49
|
+
redirect_to admin_file_categories_path
|
|
50
|
+
else
|
|
51
|
+
add_breadcrumb @file_category.name
|
|
52
|
+
render :edit, status: :unprocessable_content
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def destroy
|
|
57
|
+
if @file_category.system?
|
|
58
|
+
flash[:warning] = "System categories cannot be deleted."
|
|
59
|
+
elsif @file_category.destroy
|
|
60
|
+
flash[:success] = "File category has been deleted successfully."
|
|
61
|
+
else
|
|
62
|
+
flash[:error] = @file_category.errors.full_messages.to_sentence
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
redirect_to admin_file_categories_path, status: :see_other
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def set_initial_breadcrumb
|
|
71
|
+
add_breadcrumb "File Categories", admin_file_categories_path
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def set_file_category
|
|
75
|
+
@file_category = FileCategory.find(params[:id])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def file_category_params
|
|
79
|
+
params.require(:file_category).permit(:name, :icon, :position, :parent_id)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|