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.
Files changed (251) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +53 -0
  4. data/app/assets/builds/panda_core_admin.css +2 -0
  5. data/app/assets/tailwind/application.css +236 -36
  6. data/app/builders/panda/core/form_builder.rb +120 -18
  7. data/app/components/panda/core/UI/badge.html.erb +10 -0
  8. data/app/components/panda/core/UI/badge.rb +14 -30
  9. data/app/components/panda/core/UI/button.html.erb +5 -0
  10. data/app/components/panda/core/UI/button.rb +11 -15
  11. data/app/components/panda/core/UI/card.html.erb +3 -0
  12. data/app/components/panda/core/UI/card.rb +7 -6
  13. data/app/components/panda/core/admin/attribute_diff_component.html.erb +21 -0
  14. data/app/components/panda/core/admin/attribute_diff_component.rb +29 -0
  15. data/app/components/panda/core/admin/breadcrumb_component.html.erb +36 -0
  16. data/app/components/panda/core/admin/breadcrumb_component.rb +12 -64
  17. data/app/components/panda/core/admin/breadcrumbs_component.html.erb +16 -0
  18. data/app/components/panda/core/admin/breadcrumbs_component.rb +26 -0
  19. data/app/components/panda/core/admin/button_component.html.erb +9 -0
  20. data/app/components/panda/core/admin/button_component.rb +36 -31
  21. data/app/components/panda/core/admin/callout_component.html.erb +17 -0
  22. data/app/components/panda/core/admin/callout_component.rb +57 -0
  23. data/app/components/panda/core/admin/code_block_component.html.erb +9 -0
  24. data/app/components/panda/core/admin/code_block_component.rb +23 -0
  25. data/app/components/panda/core/admin/container_component.html.erb +27 -0
  26. data/app/components/panda/core/admin/container_component.rb +11 -77
  27. data/app/components/panda/core/admin/contributors_summary_component.html.erb +15 -0
  28. data/app/components/panda/core/admin/contributors_summary_component.rb +23 -0
  29. data/app/components/panda/core/admin/dashboard_default_content_component.html.erb +70 -0
  30. data/app/components/panda/core/admin/dashboard_default_content_component.rb +32 -0
  31. data/app/components/panda/core/admin/delete_button_component.html.erb +4 -0
  32. data/app/components/panda/core/admin/delete_button_component.rb +41 -0
  33. data/app/components/panda/core/admin/dropdown_button_component.html.erb +28 -0
  34. data/app/components/panda/core/admin/dropdown_button_component.rb +100 -0
  35. data/app/components/panda/core/admin/dropdown_component.html.erb +32 -0
  36. data/app/components/panda/core/admin/dropdown_component.rb +65 -0
  37. data/app/components/panda/core/admin/empty_state_component.html.erb +16 -0
  38. data/app/components/panda/core/admin/empty_state_component.rb +25 -0
  39. data/app/components/panda/core/admin/file_gallery_component.html.erb +58 -0
  40. data/app/components/panda/core/admin/file_gallery_component.rb +26 -98
  41. data/app/components/panda/core/admin/filter_pills_component.html.erb +6 -0
  42. data/app/components/panda/core/admin/filter_pills_component.rb +29 -0
  43. data/app/components/panda/core/admin/flash_message_component.html.erb +11 -0
  44. data/app/components/panda/core/admin/flash_message_component.rb +33 -60
  45. data/app/components/panda/core/admin/form_error_component.html.erb +7 -0
  46. data/app/components/panda/core/admin/form_error_component.rb +8 -16
  47. data/app/components/panda/core/admin/form_footer_component.html.erb +16 -0
  48. data/app/components/panda/core/admin/form_footer_component.rb +65 -0
  49. data/app/components/panda/core/admin/form_input_component.html.erb +1 -0
  50. data/app/components/panda/core/admin/form_input_component.rb +21 -13
  51. data/app/components/panda/core/admin/form_section_component.html.erb +16 -0
  52. data/app/components/panda/core/admin/form_section_component.rb +64 -0
  53. data/app/components/panda/core/admin/form_select_component.html.erb +5 -0
  54. data/app/components/panda/core/admin/form_select_component.rb +25 -26
  55. data/app/components/panda/core/admin/heading_component.html.erb +26 -0
  56. data/app/components/panda/core/admin/heading_component.rb +23 -60
  57. data/app/components/panda/core/admin/login_form_component.html.erb +38 -0
  58. data/app/components/panda/core/admin/login_form_component.rb +28 -0
  59. data/app/components/panda/core/admin/main_layout_component.html.erb +88 -0
  60. data/app/components/panda/core/admin/main_layout_component.rb +19 -0
  61. data/app/components/panda/core/admin/my_profile/connected_account_component.html.erb +41 -0
  62. data/app/components/panda/core/admin/my_profile/connected_account_component.rb +27 -0
  63. data/app/components/panda/core/admin/navigation/item_component.html.erb +34 -0
  64. data/app/components/panda/core/admin/navigation/item_component.rb +69 -0
  65. data/app/components/panda/core/admin/navigation/sub_item_component.html.erb +9 -0
  66. data/app/components/panda/core/admin/navigation/sub_item_component.rb +44 -0
  67. data/app/components/panda/core/admin/navigation/user_menu_component.html.erb +30 -0
  68. data/app/components/panda/core/admin/navigation/user_menu_component.rb +36 -0
  69. data/app/components/panda/core/admin/page_header_component.html.erb +28 -0
  70. data/app/components/panda/core/admin/page_header_component.rb +19 -35
  71. data/app/components/panda/core/admin/pagination_component.html.erb +33 -0
  72. data/app/components/panda/core/admin/pagination_component.rb +56 -0
  73. data/app/components/panda/core/admin/panel_component.html.erb +19 -0
  74. data/app/components/panda/core/admin/panel_component.rb +4 -29
  75. data/app/components/panda/core/admin/prose_panel_component.html.erb +6 -0
  76. data/app/components/panda/core/admin/prose_panel_component.rb +43 -0
  77. data/app/components/panda/core/admin/search_bar_component.html.erb +19 -0
  78. data/app/components/panda/core/admin/search_bar_component.rb +31 -0
  79. data/app/components/panda/core/admin/search_filter_bar_component.html.erb +22 -0
  80. data/app/components/panda/core/admin/search_filter_bar_component.rb +27 -0
  81. data/app/components/panda/core/admin/secret_field_component.html.erb +24 -0
  82. data/app/components/panda/core/admin/secret_field_component.rb +31 -0
  83. data/app/components/panda/core/admin/sidebar_component.html.erb +160 -0
  84. data/app/components/panda/core/admin/sidebar_component.rb +55 -0
  85. data/app/components/panda/core/admin/slideover_component.html.erb +67 -0
  86. data/app/components/panda/core/admin/slideover_component.rb +31 -70
  87. data/app/components/panda/core/admin/statistics_component.html.erb +4 -0
  88. data/app/components/panda/core/admin/statistics_component.rb +6 -8
  89. data/app/components/panda/core/admin/tab_bar_component.html.erb +48 -0
  90. data/app/components/panda/core/admin/tab_bar_component.rb +32 -67
  91. data/app/components/panda/core/admin/table_component.html.erb +82 -0
  92. data/app/components/panda/core/admin/table_component.rb +54 -87
  93. data/app/components/panda/core/admin/tag_badge_component.html.erb +7 -0
  94. data/app/components/panda/core/admin/tag_badge_component.rb +21 -0
  95. data/app/components/panda/core/admin/tag_component.html.erb +1 -0
  96. data/app/components/panda/core/admin/tag_component.rb +51 -20
  97. data/app/components/panda/core/admin/tag_input_component.html.erb +37 -0
  98. data/app/components/panda/core/admin/tag_input_component.rb +22 -0
  99. data/app/components/panda/core/admin/tags_panel_component.html.erb +8 -0
  100. data/app/components/panda/core/admin/tags_panel_component.rb +33 -0
  101. data/app/components/panda/core/admin/user_activity_component.html.erb +15 -0
  102. data/app/components/panda/core/admin/user_activity_component.rb +9 -22
  103. data/app/components/panda/core/admin/user_display_component.html.erb +8 -0
  104. data/app/components/panda/core/admin/user_display_component.rb +19 -22
  105. data/app/components/panda/core/base.rb +35 -42
  106. data/app/components/panda/core/shared/footer_component.html.erb +2 -0
  107. data/app/components/panda/core/shared/footer_component.rb +11 -0
  108. data/app/components/panda/core/shared/header_component.html.erb +32 -0
  109. data/app/components/panda/core/shared/header_component.rb +19 -0
  110. data/app/constraints/panda/core/admin_constraint.rb +2 -2
  111. data/app/controllers/concerns/panda/core/admin_authorization.rb +58 -0
  112. data/app/controllers/concerns/panda/core/authorizable.rb +107 -0
  113. data/app/controllers/panda/core/admin/base_controller.rb +62 -3
  114. data/app/controllers/panda/core/admin/editor/search_controller.rb +17 -0
  115. data/app/controllers/panda/core/admin/feature_flags_controller.rb +35 -0
  116. data/app/controllers/panda/core/admin/file_categories_controller.rb +84 -0
  117. data/app/controllers/panda/core/admin/files_controller.rb +230 -0
  118. data/app/controllers/panda/core/admin/import_sessions_controller.rb +120 -0
  119. data/app/controllers/panda/core/admin/my_profile/logins_controller.rb +24 -0
  120. data/app/controllers/panda/core/admin/my_profile_controller.rb +31 -0
  121. data/app/controllers/panda/core/admin/search_controller.rb +15 -0
  122. data/app/controllers/panda/core/admin/sessions_controller.rb +84 -10
  123. data/app/controllers/panda/core/admin/tags_controller.rb +77 -0
  124. data/app/controllers/panda/core/admin/test_sessions_controller.rb +5 -6
  125. data/app/controllers/panda/core/admin/users_controller.rb +184 -0
  126. data/app/controllers/panda/core/application_controller.rb +13 -1
  127. data/app/helpers/panda/core/active_storage_helper.rb +24 -0
  128. data/app/helpers/panda/core/feature_flag_helper.rb +11 -0
  129. data/app/helpers/panda/core/form_helper.rb +15 -0
  130. data/app/javascript/panda/core/controllers/avatar_upload_controller.js +73 -0
  131. data/app/javascript/panda/core/controllers/clipboard_controller.js +119 -0
  132. data/app/javascript/panda/core/controllers/collapsible_item_controller.js +45 -0
  133. data/app/javascript/panda/core/controllers/custom_select_controller.js +256 -0
  134. data/app/javascript/panda/core/controllers/datepicker_controller.js +121 -0
  135. data/app/javascript/panda/core/controllers/daterange_controller.js +193 -0
  136. data/app/javascript/panda/core/controllers/file_gallery_controller.js +134 -0
  137. data/app/javascript/panda/core/controllers/file_upload_controller.js +165 -0
  138. data/app/javascript/panda/core/controllers/global_search_controller.js +184 -0
  139. data/app/javascript/panda/core/controllers/index.js +36 -0
  140. data/app/javascript/panda/core/controllers/mobile_sidebar_controller.js +58 -0
  141. data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +49 -24
  142. data/app/javascript/panda/core/controllers/row_link_controller.js +27 -0
  143. data/app/javascript/panda/core/controllers/tag_input_controller.js +183 -0
  144. data/app/javascript/panda/core/tailwindcss-stimulus-components.js +2 -3
  145. data/app/javascript/panda/core/vendor/@fortawesome--fontawesome-free@7.2.0.js +8129 -0
  146. data/app/javascript/panda/core/vendor/@hotwired--turbo.js +7197 -100
  147. data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +511 -2
  148. data/app/javascript/panda/core/vendor/@tailwindplus--elements@1.0.22.js +65 -0
  149. data/app/javascript/panda/core/vendor/cropperjs@2.1.0.js +3 -0
  150. data/app/javascript/panda/core/vendor/vanilla-calendar-pro.js +2 -0
  151. data/app/models/concerns/panda/core/has_metadata.rb +164 -0
  152. data/app/models/concerns/panda/core/importable.rb +79 -0
  153. data/app/models/concerns/panda/core/searchable.rb +66 -0
  154. data/app/models/concerns/panda/core/taggable.rb +37 -0
  155. data/app/models/panda/core/feature_flag.rb +56 -0
  156. data/app/models/panda/core/file_categorization.rb +14 -0
  157. data/app/models/panda/core/file_category.rb +45 -0
  158. data/app/models/panda/core/import_session.rb +79 -0
  159. data/app/models/panda/core/presence.rb +24 -0
  160. data/app/models/panda/core/tag.rb +33 -0
  161. data/app/models/panda/core/tagging.rb +14 -0
  162. data/app/models/panda/core/user.rb +94 -16
  163. data/app/models/panda/core/user_activity.rb +42 -0
  164. data/app/models/panda/core/user_session.rb +35 -0
  165. data/app/services/panda/core/attach_avatar_service.rb +15 -3
  166. data/app/services/panda/core/csv_import_service.rb +65 -0
  167. data/app/services/panda/core/file_categorizer.rb +52 -0
  168. data/app/services/panda/core/file_parser.rb +76 -0
  169. data/app/services/panda/core/invite_user_service.rb +44 -0
  170. data/app/services/panda/core/presence_service.rb +49 -0
  171. data/app/views/layouts/panda/core/admin.html.erb +6 -76
  172. data/app/views/layouts/panda/core/admin_simple.html.erb +2 -2
  173. data/app/views/panda/core/admin/dashboard/_default_content.html.erb +1 -73
  174. data/app/views/panda/core/admin/dashboard/show.html.erb +12 -15
  175. data/app/views/panda/core/admin/feature_flags/index.html.erb +56 -0
  176. data/app/views/panda/core/admin/file_categories/_form.html.erb +20 -0
  177. data/app/views/panda/core/admin/file_categories/edit.html.erb +5 -0
  178. data/app/views/panda/core/admin/file_categories/index.html.erb +55 -0
  179. data/app/views/panda/core/admin/file_categories/new.html.erb +5 -0
  180. data/app/views/panda/core/admin/files/_file_details.html.erb +156 -0
  181. data/app/views/panda/core/admin/files/index.html.erb +61 -0
  182. data/app/views/panda/core/admin/import_sessions/column_map.html.erb +43 -0
  183. data/app/views/panda/core/admin/import_sessions/index.html.erb +48 -0
  184. data/app/views/panda/core/admin/import_sessions/new.html.erb +37 -0
  185. data/app/views/panda/core/admin/import_sessions/preview.html.erb +51 -0
  186. data/app/views/panda/core/admin/import_sessions/show.html.erb +87 -0
  187. data/app/views/panda/core/admin/my_profile/edit.html.erb +51 -39
  188. data/app/views/panda/core/admin/my_profile/logins/show.html.erb +72 -0
  189. data/app/views/panda/core/admin/sessions/new.html.erb +1 -37
  190. data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +1 -18
  191. data/app/views/panda/core/admin/shared/_sidebar.html.erb +1 -99
  192. data/app/views/panda/core/admin/shared/_slideover.html.erb +3 -3
  193. data/app/views/panda/core/admin/tags/_form.html.erb +21 -0
  194. data/app/views/panda/core/admin/tags/edit.html.erb +4 -0
  195. data/app/views/panda/core/admin/tags/index.html.erb +41 -0
  196. data/app/views/panda/core/admin/tags/new.html.erb +4 -0
  197. data/app/views/panda/core/admin/users/activity.html.erb +90 -0
  198. data/app/views/panda/core/admin/users/edit.html.erb +48 -0
  199. data/app/views/panda/core/admin/users/index.html.erb +161 -0
  200. data/app/views/panda/core/admin/users/sessions.html.erb +103 -0
  201. data/app/views/panda/core/admin/users/show.html.erb +224 -0
  202. data/app/views/panda/core/shared/_footer.html.erb +1 -2
  203. data/app/views/panda/core/shared/_header.html.erb +4 -22
  204. data/config/brakeman.ignore +68 -31
  205. data/config/importmap.rb +13 -11
  206. data/config/routes.rb +49 -2
  207. data/db/migrate/20260202171614_create_panda_core_file_categories.rb +30 -0
  208. data/db/migrate/20260203000001_add_user_management_fields_to_panda_core_users.rb +21 -0
  209. data/db/migrate/20260203000002_create_panda_core_user_activities.rb +24 -0
  210. data/db/migrate/20260203000003_create_panda_core_user_sessions.rb +25 -0
  211. data/db/migrate/20260207100000_create_panda_core_presences.rb +21 -0
  212. data/db/migrate/20260213000001_fix_active_storage_attachments_record_id_type.rb +44 -0
  213. data/db/migrate/20260217000001_create_panda_core_feature_flags.rb +14 -0
  214. data/db/migrate/20260303000001_add_metadata_to_panda_core_users.rb +11 -0
  215. data/db/migrate/20260310000001_create_panda_core_tags.rb +27 -0
  216. data/db/migrate/20260310000002_create_panda_core_import_sessions.rb +29 -0
  217. data/lib/generators/panda/core/install_generator.rb +32 -0
  218. data/lib/generators/panda/core/templates/panda.rb +63 -0
  219. data/lib/panda/core/asset_loader.rb +5 -2
  220. data/lib/panda/core/authentication.rb +3 -3
  221. data/lib/panda/core/configuration.rb +195 -1
  222. data/lib/panda/core/engine/admin_controller_config.rb +3 -6
  223. data/lib/panda/core/engine/autoload_config.rb +21 -9
  224. data/lib/panda/core/engine/omniauth_config.rb +108 -38
  225. data/lib/panda/core/engine/pending_migrations_check.rb +46 -0
  226. data/lib/panda/core/engine/sqlite_schema_compatibility.rb +36 -0
  227. data/lib/panda/core/engine/{phlex_config.rb → view_component_config.rb} +5 -7
  228. data/lib/panda/core/engine.rb +90 -25
  229. data/lib/panda/core/middleware.rb +35 -0
  230. data/lib/panda/core/navigation_registry.rb +455 -0
  231. data/lib/panda/core/oauth_providers.rb +27 -0
  232. data/lib/panda/core/permission_registry.rb +89 -0
  233. data/lib/panda/core/search_registry.rb +69 -0
  234. data/lib/panda/core/seeds/file_categories.rb +30 -0
  235. data/lib/panda/core/shared/inflections_config.rb +1 -5
  236. data/lib/panda/core/testing/rails_helper.rb +87 -0
  237. data/lib/panda/core/testing/support/authentication_test_helpers.rb +22 -18
  238. data/lib/panda/core/testing/support/system/cuprite_helpers.rb +4 -4
  239. data/lib/panda/core/testing/view_component_test_controller.rb +19 -0
  240. data/lib/panda/core/version.rb +1 -1
  241. data/lib/panda/core/widget_registry.rb +50 -0
  242. data/lib/panda/core.rb +17 -8
  243. data/lib/tasks/panda/core/file_categories.rake +41 -0
  244. data/public/panda-core-assets/panda-core-0.12.5.css +2 -0
  245. data/public/panda-core-assets/panda-core-0.13.0.css +2 -0
  246. data/public/panda-core-assets/panda-core-0.14.0.css +2 -0
  247. data/public/panda-core-assets/panda-core-0.14.4.css +2 -0
  248. data/public/panda-core-assets/panda-core.css +2 -2
  249. data/public/panda-core-assets/vanilla-calendar.css +47 -0
  250. metadata +203 -26
  251. 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 Phlex components in the Panda ecosystem.
5
+ # Base class for all ViewComponent components in the Panda ecosystem.
6
6
  #
7
7
  # This base component provides:
8
- # - Type-safe properties via Literal
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
- # prop :title, String
17
- # prop :variant, Symbol, default: :primary
18
- #
19
- # def view_template
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 Button < Panda::Core::Base
31
- # prop :text, String
32
- #
33
- # def view_template
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 Button.new(text: "Click me", class: "mt-4", type: "submit")
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 < Phlex::HTML
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
- # Enable type-safe properties via Literal
51
- extend Literal::Properties
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 value [Hash] User-provided attributes
60
- # @return [Hash] Merged attributes with Tailwind classes properly combined
61
- prop :attrs, Hash, :**, reader: :private do |value|
62
- merge_attrs(value, default_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
- # In development mode, wrap components with HTML comments
106
- # showing their class name for easier debugging
107
- if Rails.env.development?
108
- def before_template
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,2 @@
1
+ </body>
2
+ </html>
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Shared
6
+ # Footer component for closing HTML document
7
+ class FooterComponent < ViewComponent::Base
8
+ end
9
+ end
10
+ end
11
+ 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[:user_id].present?
7
+ return false unless request.session[Panda::Core::ADMIN_SESSION_KEY].present?
8
8
 
9
- user = User.find_by(id: request.session[:user_id])
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[:user_id]) if session[:user_id]
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
- return if user_signed_in? && current_user.admin?
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: "Please login to view this!"}
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