panda-core 0.2.4 → 0.6.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/tailwind/application.css +199 -7
  3. data/app/assets/tailwind/tailwind.config.js +8 -0
  4. data/app/components/panda/core/UI/badge.rb +107 -0
  5. data/app/components/panda/core/UI/button.rb +110 -0
  6. data/app/components/panda/core/UI/card.rb +88 -0
  7. data/app/components/panda/core/admin/breadcrumb_component.rb +133 -0
  8. data/app/components/panda/core/admin/button_component.rb +46 -28
  9. data/app/components/panda/core/admin/container_component.rb +75 -4
  10. data/app/components/panda/core/admin/file_gallery_component.rb +157 -0
  11. data/app/components/panda/core/admin/flash_message_component.rb +98 -15
  12. data/app/components/panda/core/admin/form_error_component.rb +48 -0
  13. data/app/components/panda/core/admin/form_input_component.rb +50 -0
  14. data/app/components/panda/core/admin/form_select_component.rb +68 -0
  15. data/app/components/panda/core/admin/heading_component.rb +53 -24
  16. data/app/components/panda/core/admin/page_header_component.rb +107 -0
  17. data/app/components/panda/core/admin/panel_component.rb +33 -4
  18. data/app/components/panda/core/admin/slideover_component.rb +66 -4
  19. data/app/components/panda/core/admin/statistics_component.rb +19 -0
  20. data/app/components/panda/core/admin/tab_bar_component.rb +101 -0
  21. data/app/components/panda/core/admin/table_component.rb +92 -11
  22. data/app/components/panda/core/admin/tag_component.rb +58 -16
  23. data/app/components/panda/core/admin/user_activity_component.rb +43 -0
  24. data/app/components/panda/core/admin/user_display_component.rb +77 -0
  25. data/app/components/panda/core/base.rb +122 -0
  26. data/app/controllers/panda/core/admin/base_controller.rb +68 -0
  27. data/app/controllers/panda/core/admin/dashboard_controller.rb +5 -3
  28. data/app/controllers/panda/core/admin/my_profile_controller.rb +4 -4
  29. data/app/controllers/panda/core/admin/sessions_controller.rb +15 -8
  30. data/app/controllers/panda/core/admin/test_sessions_controller.rb +60 -0
  31. data/app/helpers/panda/core/asset_helper.rb +31 -5
  32. data/app/helpers/panda/core/sessions_helper.rb +27 -2
  33. data/app/javascript/panda/core/application.js +8 -1
  34. data/app/javascript/panda/core/controllers/alert_controller.js +38 -0
  35. data/app/javascript/panda/core/controllers/index.js +3 -3
  36. data/app/javascript/panda/core/controllers/toggle_controller.js +41 -0
  37. data/app/javascript/panda/core/tailwindplus-elements.js +31 -0
  38. data/app/javascript/panda/core/vendor/@hotwired--stimulus.js +4 -0
  39. data/app/javascript/panda/core/vendor/@hotwired--turbo.js +160 -0
  40. data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +4 -0
  41. data/app/models/panda/core/user.rb +61 -14
  42. data/app/services/panda/core/attach_avatar_service.rb +67 -0
  43. data/app/views/layouts/panda/core/admin.html.erb +40 -3
  44. data/app/views/layouts/panda/core/admin_simple.html.erb +6 -0
  45. data/app/views/panda/core/admin/dashboard/_default_content.html.erb +4 -4
  46. data/app/views/panda/core/admin/dashboard/show.html.erb +2 -2
  47. data/app/views/panda/core/admin/my_profile/edit.html.erb +36 -25
  48. data/app/views/panda/core/admin/sessions/new.html.erb +9 -10
  49. data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +27 -34
  50. data/app/views/panda/core/admin/shared/_flash.html.erb +4 -30
  51. data/app/views/panda/core/admin/shared/_sidebar.html.erb +41 -20
  52. data/app/views/panda/core/shared/_header.html.erb +13 -5
  53. data/config/importmap.rb +19 -6
  54. data/config/routes.rb +10 -3
  55. data/db/migrate/20250810120000_add_current_theme_to_panda_core_users.rb +7 -0
  56. data/db/migrate/20250811120000_add_oauth_avatar_url_to_panda_core_users.rb +7 -0
  57. data/lib/panda/core/asset_loader.rb +23 -8
  58. data/lib/panda/core/configuration.rb +12 -9
  59. data/lib/panda/core/debug.rb +47 -0
  60. data/lib/panda/core/engine.rb +55 -9
  61. data/lib/panda/core/services/base_service.rb +19 -4
  62. data/lib/panda/core/version.rb +1 -1
  63. data/lib/panda/core.rb +2 -0
  64. data/lib/tasks/panda_core_users.rake +158 -0
  65. metadata +103 -14
  66. data/app/components/panda/core/admin/container_component.html.erb +0 -12
  67. data/app/components/panda/core/admin/flash_message_component.html.erb +0 -31
  68. data/app/components/panda/core/admin/panel_component.html.erb +0 -7
  69. data/app/components/panda/core/admin/slideover_component.html.erb +0 -9
  70. data/app/components/panda/core/admin/table_component.html.erb +0 -29
  71. data/app/controllers/panda/core/admin_controller.rb +0 -30
@@ -3,32 +3,113 @@
3
3
  module Panda
4
4
  module Core
5
5
  module Admin
6
- class TableComponent < ViewComponent::Base
6
+ class TableComponent < Panda::Core::Base
7
+ prop :term, String
8
+ prop :rows, _Nilable(Object), default: -> { [] }
9
+ prop :icon, String, default: ""
10
+
7
11
  attr_reader :columns
8
12
 
9
- def initialize(term:, rows:)
10
- @term = term
11
- @rows = rows
13
+ def initialize(**props)
14
+ super
12
15
  @columns = []
13
16
  end
14
17
 
15
- def column(label, &)
16
- @columns << Column.new(label, &)
18
+ def view_template(&block)
19
+ # Capture the block to populate columns
20
+ instance_eval(&block) if block_given?
21
+
22
+ if @rows.any?
23
+ render_table_with_rows
24
+ else
25
+ render_empty_table
26
+ end
27
+ end
28
+
29
+ def column(label, width: nil, &cell_block)
30
+ @columns << Column.new(label, width, &cell_block)
17
31
  end
18
32
 
19
33
  private
20
34
 
21
- # Ensures @columns gets populated [https://dev.to/rolandstuder/supercharged-table-component-built-with-viewcomponent-3j6i]
22
- def before_render
23
- content
35
+ def render_table_with_rows
36
+ div(class: "table overflow-x-auto mb-12 w-full rounded-lg border border-dark", style: "table-layout: fixed;") do
37
+ render_header
38
+ render_rows
39
+ end
40
+ end
41
+
42
+ def render_empty_table
43
+ div(class: "text-center block border border-dashed py-12 rounded-lg") do
44
+ i(class: "#{@icon} text-4xl text-gray-400 mb-3") if @icon.present?
45
+ h3(class: "py-1 text-xl font-semibold text-gray-900") { "No #{@term.pluralize}" }
46
+ p(class: "py-1 text-base text-gray-500") { "Get started by creating a new #{@term}." }
47
+ end
48
+ end
49
+
50
+ def render_header
51
+ div(class: "table-header-group") do
52
+ div(class: "table-row text-base font-medium text-white bg-dark") do
53
+ @columns.each_with_index do |column, i|
54
+ header_classes = "table-cell sticky top-0 z-10 p-4"
55
+ header_classes += " rounded-tl-md" if i.zero?
56
+ header_classes += " rounded-tr-md" if i == @columns.size - 1
57
+
58
+ header_style = column.width ? "width: #{column.width};" : nil
59
+ div(class: header_classes, style: header_style) { column.label }
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def render_rows
66
+ div(class: "table-row-group") do
67
+ @rows.each do |row|
68
+ div(
69
+ class: "table-row relative bg-gray-500/5 hover:bg-gray-500/20",
70
+ data: {post_id: row.id}
71
+ ) do
72
+ @columns.each do |column|
73
+ div(class: "table-cell py-5 px-3 h-20 text-sm align-middle whitespace-nowrap border-b border-dark/20") do
74
+ # Capture the cell content by calling the block with the row
75
+ render_cell_content(row, column.cell)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ def render_cell_content(row, cell_block)
84
+ # When called from ERB, we need to capture the block's output buffer
85
+ # When called from Phlex, evaluate directly
86
+ if defined?(view_context) && view_context
87
+ # Use capture to get ERB output buffer content
88
+ captured_html = view_context.capture(row, &cell_block)
89
+ # Render the captured HTML (already html_safe from capture)
90
+ raw(captured_html)
91
+ else
92
+ # Pure Phlex context - execute block directly
93
+ result = cell_block.call(row)
94
+
95
+ # Handle different return types
96
+ if result.is_a?(String)
97
+ plain(result)
98
+ elsif result.respond_to?(:render_in)
99
+ render(result)
100
+ else
101
+ plain(result.to_s)
102
+ end
103
+ end
24
104
  end
25
105
  end
26
106
 
27
107
  class Column
28
- attr_reader :label, :cell
108
+ attr_reader :label, :cell, :width
29
109
 
30
- def initialize(label, &block)
110
+ def initialize(label, width = nil, &block)
31
111
  @label = label
112
+ @width = width
32
113
  @cell = block
33
114
  end
34
115
  end
@@ -3,30 +3,72 @@
3
3
  module Panda
4
4
  module Core
5
5
  module Admin
6
- class TagComponent < ViewComponent::Base
7
- attr_accessor :status, :text
6
+ class TagComponent < Panda::Core::Base
7
+ prop :status, Symbol, default: :active
8
+ prop :text, _Nilable(String), default: -> {}
9
+ prop :page_type, _Nilable(Symbol), default: -> {}
8
10
 
9
- def initialize(status: :active, text: nil)
10
- @status = status.to_sym
11
- @text = text || status.to_s.humanize
11
+ def view_template
12
+ span(class: tag_classes) { computed_text }
12
13
  end
13
14
 
14
- def call
15
- classes = "inline-flex items-center py-1 px-2 text-xs font-medium rounded-md ring-1 ring-inset "
15
+ private
16
16
 
17
- classes += case @status
17
+ def computed_text
18
+ if @page_type
19
+ @text || type_display_text
20
+ else
21
+ @text || @status.to_s.humanize
22
+ end
23
+ end
24
+
25
+ def type_display_text
26
+ case @page_type
27
+ when :standard
28
+ "Active"
29
+ when :hidden_type
30
+ "Hidden"
31
+ else
32
+ @page_type.to_s.humanize
33
+ end
34
+ end
35
+
36
+ def tag_classes
37
+ base = "inline-flex items-center py-1 px-2 text-xs font-medium rounded-md ring-1 ring-inset "
38
+ base + (@page_type ? type_classes : status_classes)
39
+ end
40
+
41
+ def type_classes
42
+ case @page_type
43
+ when :system
44
+ "text-red-700 bg-red-100 ring-red-600/20 dark:bg-red-400/10 dark:text-red-400"
45
+ when :posts
46
+ "text-purple-700 bg-purple-100 ring-purple-600/20 dark:bg-purple-400/10 dark:text-purple-400"
47
+ when :code
48
+ "text-blue-700 bg-blue-100 ring-blue-600/20 dark:bg-blue-400/10 dark:text-blue-400"
49
+ when :standard
50
+ "text-green-700 bg-green-100 ring-green-600/20 dark:bg-green-400/10 dark:text-green-400"
51
+ when :hidden_type
52
+ "text-gray-700 bg-gray-100 ring-gray-600/20 dark:bg-gray-400/10 dark:text-gray-400"
53
+ else
54
+ "text-gray-700 bg-gray-100 ring-gray-600/20 dark:bg-gray-400/10 dark:text-gray-400"
55
+ end
56
+ end
57
+
58
+ def status_classes
59
+ case @status
18
60
  when :active
19
- "text-white ring-black/30 bg-green-600 border-0 "
61
+ "text-white ring-black/30 bg-green-600 border-0"
20
62
  when :draft
21
- "text-black ring-black/30 bg-yellow-400 "
63
+ "text-black ring-black/30 bg-yellow-400"
22
64
  when :inactive, :hidden
23
- "text-black ring-black/30 bg-black/5 bg-white "
65
+ "text-black ring-black/30 bg-black/5 bg-white"
66
+ when :auto
67
+ "text-blue-700 bg-blue-100 ring-blue-600/20 dark:bg-blue-400/10 dark:text-blue-400"
68
+ when :static
69
+ "text-gray-700 bg-gray-100 ring-gray-600/20 dark:bg-gray-400/10 dark:text-gray-400"
24
70
  else
25
- "text-black bg-white "
26
- end
27
-
28
- content_tag :span, class: classes do
29
- @text
71
+ "text-black bg-white"
30
72
  end
31
73
  end
32
74
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class UserActivityComponent < Panda::Core::Base
7
+ include ActionView::Helpers::DateHelper
8
+
9
+ prop :model, _Nilable(Object), default: -> {}
10
+ prop :at, _Nilable(Object), default: -> {}
11
+ prop :user, _Nilable(Object), default: -> {}
12
+
13
+ def view_template
14
+ return unless should_render?
15
+
16
+ if @user.is_a?(Panda::Core::User) && time
17
+ render Panda::Core::Admin::UserDisplayComponent.new(
18
+ user: @user,
19
+ metadata: "#{time_ago_in_words(time)} ago"
20
+ )
21
+ elsif @user.is_a?(Panda::Core::User)
22
+ render Panda::Core::Admin::UserDisplayComponent.new(
23
+ user: @user,
24
+ metadata: "Not published"
25
+ )
26
+ elsif time
27
+ div(class: "text-black/60") { "#{time_ago_in_words(time)} ago" }
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def time
34
+ @at if @at.is_a?(ActiveSupport::TimeWithZone)
35
+ end
36
+
37
+ def should_render?
38
+ @user.is_a?(Panda::Core::User) || time
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ class UserDisplayComponent < Panda::Core::Base
7
+ prop :user_id, _Nilable(String), default: -> {}
8
+ prop :user, _Nilable(Object), default: -> {}
9
+ prop :metadata, String, default: ""
10
+
11
+ def view_template
12
+ return unless resolved_user
13
+
14
+ div(class: "block flex-shrink-0 group") do
15
+ div(class: "flex items-center") do
16
+ render_avatar
17
+ render_user_info
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def resolved_user
25
+ @resolved_user ||= if @user.nil? && @user_id.present?
26
+ Panda::Core::User.find_by(id: @user_id)
27
+ else
28
+ @user
29
+ end
30
+ end
31
+
32
+ def render_avatar
33
+ has_image = resolved_user.respond_to?(:avatar_url) &&
34
+ resolved_user.avatar_url.present?
35
+
36
+ if has_image
37
+ div do
38
+ img(
39
+ class: "inline-block w-10 h-10 rounded-full object-cover",
40
+ src: resolved_user.avatar_url,
41
+ alt: ""
42
+ )
43
+ end
44
+ else
45
+ div(class: "inline-block w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center") do
46
+ span(class: "text-sm font-medium text-gray-600") { user_initials }
47
+ end
48
+ end
49
+ end
50
+
51
+ def user_initials
52
+ return "" unless resolved_user.respond_to?(:name)
53
+
54
+ name_parts = resolved_user.name.to_s.split
55
+ if name_parts.length >= 2
56
+ "#{name_parts.first[0]}#{name_parts.last[0]}".upcase
57
+ elsif name_parts.length == 1
58
+ name_parts.first[0..1].upcase
59
+ else
60
+ ""
61
+ end
62
+ end
63
+
64
+ def render_user_info
65
+ div(class: "ml-3") do
66
+ p(class: "text-sm text-black") { resolved_user.name }
67
+ if @metadata.present?
68
+ p(class: "text-sm text-black/60") { @metadata }
69
+ elsif resolved_user.respond_to?(:email) && resolved_user.email.present?
70
+ p(class: "text-sm text-gray-500") { resolved_user.email }
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ # Base class for all Phlex components in the Panda ecosystem.
6
+ #
7
+ # This base component provides:
8
+ # - Type-safe properties via Literal
9
+ # - Tailwind CSS class merging
10
+ # - Attribute merging with sensible defaults
11
+ # - Rails helper integration
12
+ # - Development-mode debugging comments
13
+ #
14
+ # @example Basic usage
15
+ # class MyComponent < Panda::Core::Base
16
+ # prop :title, String
17
+ # prop :variant, Symbol, default: :primary
18
+ #
19
+ # def view_template
20
+ # div(**@attrs) { title }
21
+ # end
22
+ #
23
+ # def default_attrs
24
+ # { class: "my-component my-component--#{variant}" }
25
+ # end
26
+ # end
27
+ #
28
+ # @example With attribute merging
29
+ # # Component definition
30
+ # class Button < Panda::Core::Base
31
+ # prop :text, String
32
+ #
33
+ # def view_template
34
+ # button(**@attrs) { text }
35
+ # end
36
+ #
37
+ # def default_attrs
38
+ # { class: "btn btn-primary", type: "button" }
39
+ # end
40
+ # end
41
+ #
42
+ # # Usage - user attrs merge with defaults
43
+ # render Button.new(text: "Click me", class: "mt-4", type: "submit")
44
+ # # => <button type="submit" class="btn btn-primary mt-4">Click me</button>
45
+ #
46
+ class Base < Phlex::HTML
47
+ # Frozen instance of TailwindMerge for efficient class merging
48
+ TAILWIND_MERGER = ::TailwindMerge::Merger.new.freeze unless defined?(TAILWIND_MERGER)
49
+
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
58
+ #
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)
63
+ end
64
+
65
+ # Merges user-provided attributes with default attributes.
66
+ # Special handling for :class to merge Tailwind classes intelligently.
67
+ #
68
+ # @param user_attrs [Hash] Attributes provided by the user
69
+ # @param default_attrs [Hash] Default attributes from the component
70
+ # @return [Hash] Merged attributes
71
+ def merge_attrs(user_attrs, default_attrs)
72
+ attrs = default_attrs.merge(user_attrs)
73
+ if attrs[:class].is_a?(String)
74
+ attrs[:class] = TAILWIND_MERGER.merge(attrs[:class])
75
+ end
76
+ attrs
77
+ end
78
+
79
+ # Helper alias for merge_attrs with clearer intent
80
+ #
81
+ # @param user_attrs [Hash] Attributes provided by the user
82
+ # @param default_attrs [Hash] Default attributes from the component
83
+ # @return [Hash] Merged attributes with Tailwind classes combined
84
+ def tailwind_merge_attrs(user_attrs, default_attrs)
85
+ merge_attrs(user_attrs, default_attrs)
86
+ end
87
+
88
+ # Override this method in subclasses to provide default attributes
89
+ # for your component.
90
+ #
91
+ # @return [Hash] Default HTML attributes for the component
92
+ #
93
+ # @example
94
+ # def default_attrs
95
+ # {
96
+ # class: "btn btn-#{variant}",
97
+ # type: "button",
98
+ # data: { controller: "button" }
99
+ # }
100
+ # end
101
+ def default_attrs
102
+ {}
103
+ end
104
+
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
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ module Admin
6
+ # Base controller for all admin interfaces across Panda gems
7
+ # Provides authentication, helpers, and hooks for extending functionality
8
+ class BaseController < ::ActionController::Base
9
+ layout "panda/core/admin"
10
+
11
+ protect_from_forgery with: :exception
12
+
13
+ # Add flash types for improved alert support with Tailwind
14
+ add_flash_types :success, :warning, :error, :info
15
+
16
+ # Include helper modules
17
+ helper Panda::Core::SessionsHelper
18
+ helper Panda::Core::AssetHelper if defined?(Panda::Core::AssetHelper)
19
+
20
+ before_action :set_current_request_details
21
+ before_action :authenticate_admin_user!
22
+
23
+ helper_method :breadcrumbs
24
+ helper_method :current_user
25
+ helper_method :user_signed_in?
26
+
27
+ def breadcrumbs
28
+ @breadcrumbs ||= []
29
+ end
30
+
31
+ def add_breadcrumb(name, path = nil)
32
+ breadcrumbs << Breadcrumb.new(name, path)
33
+ end
34
+
35
+ # Set the current request details
36
+ # @return [void]
37
+ def set_current_request_details
38
+ # Set Core current attributes
39
+ Panda::Core::Current.request_id = request.uuid
40
+ Panda::Core::Current.user_agent = request.user_agent
41
+ Panda::Core::Current.ip_address = request.ip
42
+ 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]
44
+ end
45
+
46
+ def authenticate_user!
47
+ redirect_to main_app.root_path, flash: {error: "Please login to view this!"} unless user_signed_in?
48
+ end
49
+
50
+ def authenticate_admin_user!
51
+ return if user_signed_in? && current_user.admin?
52
+
53
+ redirect_to panda_core.admin_login_path,
54
+ flash: {error: "Please login to view this!"}
55
+ end
56
+
57
+ # Required for paper_trail and seems as good as convention these days
58
+ def current_user
59
+ Panda::Core::Current.user
60
+ end
61
+
62
+ def user_signed_in?
63
+ !!Panda::Core::Current.user
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -3,13 +3,15 @@
3
3
  module Panda
4
4
  module Core
5
5
  module Admin
6
- class DashboardController < AdminController
6
+ class DashboardController < BaseController
7
7
  # Authentication is automatically enforced by AdminController
8
8
 
9
9
  def show
10
10
  # If a custom dashboard path is configured, redirect there
11
- if Panda::Core.configuration.dashboard_redirect_path
12
- redirect_to Panda::Core.configuration.dashboard_redirect_path
11
+ if Panda::Core.config.dashboard_redirect_path
12
+ redirect_path = Panda::Core.config.dashboard_redirect_path
13
+ redirect_path = redirect_path.call if redirect_path.respond_to?(:call)
14
+ redirect_to redirect_path
13
15
  else
14
16
  # Render the dashboard view
15
17
  render :show
@@ -3,7 +3,7 @@
3
3
  module Panda
4
4
  module Core
5
5
  module Admin
6
- class MyProfileController < ::Panda::Core::AdminController
6
+ class MyProfileController < BaseController
7
7
  before_action :set_initial_breadcrumb, only: %i[edit update]
8
8
 
9
9
  # Shows the edit form for the current user's profile
@@ -21,7 +21,7 @@ module Panda
21
21
  flash[:success] = "Your profile has been updated successfully."
22
22
  redirect_to edit_admin_my_profile_path
23
23
  else
24
- render :edit, locals: {user: current_user}, status: :unprocessable_entity
24
+ render :edit, locals: {user: current_user}, status: :unprocessable_content
25
25
  end
26
26
  end
27
27
 
@@ -36,10 +36,10 @@ module Panda
36
36
  # @return ActionController::StrongParameters
37
37
  def user_params
38
38
  # Base parameters that Core always allows
39
- base_params = [:firstname, :lastname, :email, :current_theme]
39
+ base_params = [:name, :email, :current_theme, :avatar]
40
40
 
41
41
  # Allow additional params from configuration
42
- additional_params = Core.configuration.additional_user_params || []
42
+ additional_params = Core.config.additional_user_params || []
43
43
 
44
44
  params.require(:user).permit(*(base_params + additional_params))
45
45
  end
@@ -3,12 +3,14 @@
3
3
  module Panda
4
4
  module Core
5
5
  module Admin
6
- class SessionsController < AdminController
6
+ class SessionsController < BaseController
7
+ layout "panda/core/admin_simple"
8
+
7
9
  # Skip authentication for login/logout actions
8
10
  skip_before_action :authenticate_admin_user!, only: [:new, :create, :destroy, :failure]
9
11
 
10
12
  def new
11
- @providers = Core.configuration.authentication_providers.keys
13
+ @providers = Core.config.authentication_providers.keys
12
14
  end
13
15
 
14
16
  def create
@@ -18,7 +20,7 @@ module Panda
18
20
  # Find the actual provider key (might be using path_name override)
19
21
  provider = find_provider_by_path(provider_path)
20
22
 
21
- unless provider && Core.configuration.authentication_providers.key?(provider)
23
+ unless provider && Core.config.authentication_providers.key?(provider)
22
24
  redirect_to admin_login_path, flash: {error: "Authentication provider not enabled"}
23
25
  return
24
26
  end
@@ -28,7 +30,9 @@ module Panda
28
30
  if user.persisted?
29
31
  # Check if user is admin before allowing access
30
32
  unless user.admin?
31
- redirect_to admin_login_path, flash: {error: "You do not have permission to access the admin area"}
33
+ flash[:error] = "You do not have permission to access the admin area"
34
+ flash.keep(:error) if Rails.env.test?
35
+ redirect_to admin_login_path
32
36
  return
33
37
  end
34
38
 
@@ -40,7 +44,8 @@ module Panda
40
44
  provider: provider)
41
45
 
42
46
  # Use configured dashboard path or default to admin_root_path
43
- redirect_path = Panda::Core.configuration.dashboard_redirect_path || admin_root_path
47
+ redirect_path = Panda::Core.config.dashboard_redirect_path || admin_root_path
48
+ redirect_path = redirect_path.call if redirect_path.respond_to?(:call)
44
49
  redirect_to redirect_path, flash: {success: "Successfully logged in as #{user.name}"}
45
50
  else
46
51
  redirect_to admin_login_path, flash: {error: "Unable to create account: #{user.errors.full_messages.join(", ")}"}
@@ -55,7 +60,9 @@ module Panda
55
60
  strategy = params[:strategy] || "unknown"
56
61
 
57
62
  Rails.logger.error "OmniAuth failure: strategy=#{strategy}, message=#{message}"
58
- redirect_to admin_login_path, flash: {error: "Authentication failed: #{message}"}
63
+ flash[:error] = "Authentication failed: #{message}"
64
+ flash.keep(:error) if Rails.env.test?
65
+ redirect_to admin_login_path
59
66
  end
60
67
 
61
68
  def destroy
@@ -72,10 +79,10 @@ module Panda
72
79
  # Find the provider key by path name (handles path_name override)
73
80
  def find_provider_by_path(provider_path)
74
81
  # First check if it's a direct match
75
- return provider_path if Core.configuration.authentication_providers.key?(provider_path)
82
+ return provider_path if Core.config.authentication_providers.key?(provider_path)
76
83
 
77
84
  # Then check if any provider has a matching path_name
78
- Core.configuration.authentication_providers.each do |key, config|
85
+ Core.config.authentication_providers.each do |key, config|
79
86
  return key if config[:path_name]&.to_sym == provider_path
80
87
  end
81
88