panda-core 0.4.1 → 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.
- checksums.yaml +4 -4
- data/app/assets/tailwind/application.css +95 -0
- data/app/assets/tailwind/tailwind.config.js +8 -0
- data/app/components/panda/core/UI/button.rb +45 -24
- data/app/components/panda/core/admin/breadcrumb_component.rb +133 -0
- data/app/components/panda/core/admin/button_component.rb +1 -1
- data/app/components/panda/core/admin/container_component.rb +27 -4
- data/app/components/panda/core/admin/file_gallery_component.rb +157 -0
- data/app/components/panda/core/admin/flash_message_component.rb +54 -36
- data/app/components/panda/core/admin/heading_component.rb +8 -7
- data/app/components/panda/core/admin/page_header_component.rb +107 -0
- data/app/components/panda/core/admin/panel_component.rb +1 -1
- data/app/components/panda/core/admin/slideover_component.rb +62 -4
- data/app/components/panda/core/admin/table_component.rb +11 -11
- data/app/components/panda/core/admin/tag_component.rb +39 -2
- data/app/components/panda/core/admin/user_display_component.rb +4 -5
- data/app/controllers/panda/core/admin/my_profile_controller.rb +2 -2
- data/app/controllers/panda/core/admin/sessions_controller.rb +6 -2
- data/app/controllers/panda/core/admin/test_sessions_controller.rb +60 -0
- data/app/helpers/panda/core/asset_helper.rb +31 -5
- data/app/helpers/panda/core/sessions_helper.rb +26 -1
- data/app/javascript/panda/core/application.js +8 -1
- data/app/javascript/panda/core/controllers/alert_controller.js +38 -0
- data/app/javascript/panda/core/controllers/index.js +3 -3
- data/app/javascript/panda/core/controllers/toggle_controller.js +41 -0
- data/app/javascript/panda/core/tailwindplus-elements.js +31 -0
- data/app/models/panda/core/user.rb +49 -6
- data/app/services/panda/core/attach_avatar_service.rb +67 -0
- data/app/views/layouts/panda/core/admin_simple.html.erb +1 -0
- data/app/views/panda/core/admin/my_profile/edit.html.erb +26 -1
- data/app/views/panda/core/admin/sessions/new.html.erb +2 -3
- data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +3 -3
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +17 -12
- data/config/importmap.rb +15 -7
- data/config/routes.rb +9 -0
- data/db/migrate/20250811120000_add_oauth_avatar_url_to_panda_core_users.rb +7 -0
- data/lib/panda/core/engine.rb +12 -3
- data/lib/panda/core/services/base_service.rb +19 -4
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core.rb +1 -0
- data/lib/tasks/panda_core_users.rake +158 -0
- metadata +11 -1
|
@@ -7,15 +7,23 @@ module Panda
|
|
|
7
7
|
prop :message, String
|
|
8
8
|
prop :kind, Symbol
|
|
9
9
|
prop :temporary, _Boolean, default: true
|
|
10
|
+
prop :subtitle, _Nilable(String), default: -> {}
|
|
10
11
|
|
|
11
12
|
def view_template
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
# Global notification container (fixed position)
|
|
14
|
+
div(
|
|
15
|
+
aria: {live: "assertive"},
|
|
16
|
+
class: "pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6 z-50"
|
|
17
|
+
) do
|
|
18
|
+
div(class: "flex w-full flex-col items-center space-y-4 sm:items-end") do
|
|
19
|
+
# Notification panel with Tailwind Plus styling
|
|
20
|
+
div(**notification_attrs) do
|
|
21
|
+
div(class: "p-4") do
|
|
22
|
+
div(class: "flex items-start") do
|
|
23
|
+
render_icon
|
|
24
|
+
render_content
|
|
25
|
+
render_close_button
|
|
26
|
+
end
|
|
19
27
|
end
|
|
20
28
|
end
|
|
21
29
|
end
|
|
@@ -24,71 +32,81 @@ module Panda
|
|
|
24
32
|
|
|
25
33
|
private
|
|
26
34
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
class: "
|
|
35
|
+
def notification_attrs
|
|
36
|
+
{
|
|
37
|
+
class: "pointer-events-auto w-full max-w-sm translate-y-0 transform rounded-lg bg-white opacity-100 shadow-lg transition duration-300 ease-out sm:translate-x-0 dark:bg-gray-800 starting:translate-y-2 starting:opacity-0 starting:sm:translate-x-2 starting:sm:translate-y-0 #{border_color_css}",
|
|
30
38
|
data: {
|
|
31
39
|
controller: "alert",
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
transition_enter_to: "translate-x-0 opacity-100",
|
|
35
|
-
transition_leave: "ease-in-out duration-500",
|
|
36
|
-
transition_leave_from: "translate-x-0 opacity-100",
|
|
37
|
-
transition_leave_to: "translate-x-full opacity-0"
|
|
38
|
-
}
|
|
40
|
+
alert_dismiss_after_value: (@temporary ? "5000" : nil)
|
|
41
|
+
}.compact
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
attrs[:data][:alert_dismiss_after_value] = "3000" if @temporary
|
|
42
|
-
attrs
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def render_icon
|
|
46
|
-
div(class: "
|
|
47
|
-
i(class: "fa-
|
|
46
|
+
div(class: "shrink-0") do
|
|
47
|
+
i(class: "fa-solid size-6 #{icon_css} #{icon_colour_css}")
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def render_content
|
|
52
|
-
div(class: "flex-1 pt-0.5
|
|
53
|
-
p(class: "
|
|
54
|
-
|
|
52
|
+
div(class: "ml-3 w-0 flex-1 pt-0.5") do
|
|
53
|
+
p(class: "text-sm font-medium text-gray-900 dark:text-white flash-message-title") { @message }
|
|
54
|
+
if @subtitle
|
|
55
|
+
p(class: "mt-1 text-sm text-gray-500 dark:text-gray-400 flash-message-subtitle") { @subtitle }
|
|
56
|
+
end
|
|
55
57
|
end
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
def render_close_button
|
|
59
|
-
div(class: "flex
|
|
61
|
+
div(class: "ml-4 flex shrink-0") do
|
|
60
62
|
button(
|
|
61
63
|
type: "button",
|
|
62
|
-
class: "inline-flex
|
|
64
|
+
class: "inline-flex rounded-md text-gray-400 hover:text-gray-500 focus:outline-2 focus:outline-offset-2 focus:outline-blue-600 dark:hover:text-white dark:focus:outline-blue-500",
|
|
63
65
|
data: {action: "alert#close"}
|
|
64
66
|
) do
|
|
65
67
|
span(class: "sr-only") { "Close" }
|
|
66
68
|
svg(
|
|
67
|
-
class: "w-5 h-5",
|
|
68
69
|
viewBox: "0 0 20 20",
|
|
69
70
|
fill: "currentColor",
|
|
70
|
-
|
|
71
|
+
data: {slot: "icon"},
|
|
72
|
+
aria: {hidden: "true"},
|
|
73
|
+
class: "size-5"
|
|
71
74
|
) do |s|
|
|
72
75
|
s.path(
|
|
73
|
-
d: "M6.28 5.22a.75.75 0
|
|
76
|
+
d: "M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
|
|
74
77
|
)
|
|
75
78
|
end
|
|
76
79
|
end
|
|
77
80
|
end
|
|
78
81
|
end
|
|
79
82
|
|
|
80
|
-
def
|
|
83
|
+
def icon_colour_css
|
|
84
|
+
case @kind
|
|
85
|
+
when :success
|
|
86
|
+
"text-green-400 dark:text-green-500"
|
|
87
|
+
when :alert, :error
|
|
88
|
+
"text-red-400 dark:text-red-500"
|
|
89
|
+
when :warning
|
|
90
|
+
"text-yellow-400 dark:text-yellow-500"
|
|
91
|
+
when :info, :notice
|
|
92
|
+
"text-blue-400 dark:text-blue-500"
|
|
93
|
+
else
|
|
94
|
+
"text-gray-400 dark:text-gray-500"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def border_color_css
|
|
81
99
|
case @kind
|
|
82
100
|
when :success
|
|
83
|
-
"
|
|
101
|
+
"ring-2 ring-green-400/20 dark:ring-green-500/30"
|
|
84
102
|
when :alert, :error
|
|
85
|
-
"
|
|
103
|
+
"ring-2 ring-red-400/20 dark:ring-red-500/30"
|
|
86
104
|
when :warning
|
|
87
|
-
"
|
|
105
|
+
"ring-2 ring-yellow-400/20 dark:ring-yellow-500/30"
|
|
88
106
|
when :info, :notice
|
|
89
|
-
"
|
|
107
|
+
"ring-2 ring-blue-400/20 dark:ring-blue-500/30"
|
|
90
108
|
else
|
|
91
|
-
"
|
|
109
|
+
"ring-1 ring-gray-400/10 dark:ring-gray-500/20"
|
|
92
110
|
end
|
|
93
111
|
end
|
|
94
112
|
|
|
@@ -21,7 +21,7 @@ module Panda
|
|
|
21
21
|
when 3
|
|
22
22
|
h3(class: heading_classes) { render_content }
|
|
23
23
|
when :panel
|
|
24
|
-
h3(class: panel_heading_classes) {
|
|
24
|
+
h3(class: panel_heading_classes) { @text }
|
|
25
25
|
else
|
|
26
26
|
h2(class: heading_classes) { render_content }
|
|
27
27
|
end
|
|
@@ -35,12 +35,13 @@ module Panda
|
|
|
35
35
|
private
|
|
36
36
|
|
|
37
37
|
def render_content
|
|
38
|
-
div(class: "grow")
|
|
38
|
+
div(class: "grow flex items-center gap-x-2") do
|
|
39
|
+
i(class: @icon) if @icon.present?
|
|
40
|
+
span { @text }
|
|
41
|
+
end
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@buttons.each { |btn| render(btn) }
|
|
43
|
-
end
|
|
43
|
+
span(class: "actions flex gap-x-2 -mt-1 min-h-[2.5rem]") do
|
|
44
|
+
@buttons&.each { |btn| render(btn) }
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|
|
@@ -61,7 +62,7 @@ module Panda
|
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
def panel_heading_classes
|
|
64
|
-
"text-base font-medium
|
|
65
|
+
"text-base font-medium px-4 py-3 text-white"
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
def additional_styles_array
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Page header component with title, optional breadcrumbs, and action buttons.
|
|
7
|
+
#
|
|
8
|
+
# Follows Tailwind UI Plus pattern for page headers with responsive layout
|
|
9
|
+
# and support for multiple action buttons.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic header with title only
|
|
12
|
+
# render Panda::Core::Admin::PageHeaderComponent.new(
|
|
13
|
+
# title: "Back End Developer"
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# @example Header with breadcrumbs
|
|
17
|
+
# render Panda::Core::Admin::PageHeaderComponent.new(
|
|
18
|
+
# title: "Back End Developer",
|
|
19
|
+
# breadcrumbs: [
|
|
20
|
+
# { text: "Jobs", href: "/admin/jobs" },
|
|
21
|
+
# { text: "Engineering", href: "/admin/jobs/engineering" },
|
|
22
|
+
# { text: "Back End Developer", href: "/admin/jobs/engineering/1" }
|
|
23
|
+
# ]
|
|
24
|
+
# )
|
|
25
|
+
#
|
|
26
|
+
# @example Header with action buttons using block
|
|
27
|
+
# render Panda::Core::Admin::PageHeaderComponent.new(
|
|
28
|
+
# title: "Back End Developer",
|
|
29
|
+
# breadcrumbs: breadcrumb_items
|
|
30
|
+
# ) do |header|
|
|
31
|
+
# header.button(text: "Edit", variant: :secondary, href: edit_path)
|
|
32
|
+
# header.button(text: "Publish", variant: :primary, href: publish_path)
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
class PageHeaderComponent < Panda::Core::Base
|
|
36
|
+
prop :title, String
|
|
37
|
+
prop :breadcrumbs, _Nilable(Array), default: -> {}
|
|
38
|
+
prop :show_back, _Boolean, default: true
|
|
39
|
+
|
|
40
|
+
def initialize(**props)
|
|
41
|
+
super
|
|
42
|
+
@buttons = []
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def view_template(&block)
|
|
46
|
+
# Allow buttons to be defined via block
|
|
47
|
+
instance_eval(&block) if block_given?
|
|
48
|
+
|
|
49
|
+
div do
|
|
50
|
+
# Breadcrumbs section
|
|
51
|
+
if @breadcrumbs
|
|
52
|
+
render Panda::Core::Admin::BreadcrumbComponent.new(
|
|
53
|
+
items: @breadcrumbs,
|
|
54
|
+
show_back: @show_back
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Title and actions section
|
|
59
|
+
div(class: "mt-2 md:flex md:items-center md:justify-between") do
|
|
60
|
+
# Title
|
|
61
|
+
div(class: "min-w-0 flex-1") do
|
|
62
|
+
h2(class: "text-2xl/7 font-bold text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight dark:text-white") do
|
|
63
|
+
@title
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Action buttons
|
|
68
|
+
if @buttons.any?
|
|
69
|
+
div(class: "mt-4 flex shrink-0 md:mt-0 md:ml-4") do
|
|
70
|
+
@buttons.each_with_index do |button_data, index|
|
|
71
|
+
render create_button(button_data, index)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Define a button to be rendered in the header actions area
|
|
80
|
+
#
|
|
81
|
+
# @param text [String] Button text
|
|
82
|
+
# @param variant [Symbol] Button variant (:primary or :secondary)
|
|
83
|
+
# @param href [String] Link href
|
|
84
|
+
# @param props [Hash] Additional button properties
|
|
85
|
+
def button(text:, variant: :secondary, href: "#", **props)
|
|
86
|
+
@buttons << {text: text, variant: variant, href: href, **props}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def create_button(button_data, index)
|
|
92
|
+
Panda::Core::UI::Button.new(
|
|
93
|
+
text: button_data[:text],
|
|
94
|
+
variant: button_data[:variant],
|
|
95
|
+
href: button_data[:href],
|
|
96
|
+
class: button_margin_class(index),
|
|
97
|
+
**button_data.except(:text, :variant, :href)
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def button_margin_class(index)
|
|
102
|
+
index.zero? ? "" : "ml-3"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -16,7 +16,7 @@ module Panda
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
div(class: "col-span-3 mt-5 rounded-lg shadow-md bg-
|
|
19
|
+
div(class: "col-span-3 mt-5 rounded-lg shadow-md bg-dark shadow-inherit/20") do
|
|
20
20
|
@heading_content&.call
|
|
21
21
|
|
|
22
22
|
div(class: "p-4 text-black bg-white rounded-b-lg") do
|
|
@@ -5,13 +5,71 @@ module Panda
|
|
|
5
5
|
module Admin
|
|
6
6
|
class SlideoverComponent < Panda::Core::Base
|
|
7
7
|
prop :title, String, default: "Settings"
|
|
8
|
+
prop :open, _Nilable(_Boolean), default: -> { false }
|
|
8
9
|
|
|
9
10
|
def view_template(&block)
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
# Capture block content
|
|
12
|
+
if block_given?
|
|
13
|
+
if defined?(view_context) && view_context
|
|
14
|
+
@content_html = view_context.capture(&block)
|
|
15
|
+
else
|
|
16
|
+
@content_block = block
|
|
17
|
+
end
|
|
14
18
|
end
|
|
19
|
+
|
|
20
|
+
div(
|
|
21
|
+
**default_attrs,
|
|
22
|
+
data: {
|
|
23
|
+
toggle_target: "toggleable",
|
|
24
|
+
transition_enter: "transform transition ease-in-out duration-500",
|
|
25
|
+
transition_enter_from: "translate-x-full",
|
|
26
|
+
transition_enter_to: "translate-x-0",
|
|
27
|
+
transition_leave: "transform transition ease-in-out duration-500",
|
|
28
|
+
transition_leave_from: "translate-x-0",
|
|
29
|
+
transition_leave_to: "translate-x-full"
|
|
30
|
+
}
|
|
31
|
+
) do
|
|
32
|
+
# Header with title and close button
|
|
33
|
+
div(class: "py-3 px-4 mb-4 bg-black") do
|
|
34
|
+
div(class: "flex justify-between items-center") do
|
|
35
|
+
h2(class: "text-base font-semibold leading-6 text-white", id: "slideover-title") do
|
|
36
|
+
i(class: "mr-2 fa-light fa-gear")
|
|
37
|
+
plain " #{@title}"
|
|
38
|
+
end
|
|
39
|
+
button(
|
|
40
|
+
type: "button",
|
|
41
|
+
data: {action: "click->toggle#toggle touch->toggle#toggle"},
|
|
42
|
+
class: "text-white hover:text-gray-300 transition"
|
|
43
|
+
) do
|
|
44
|
+
i(class: "font-bold fa-regular fa-xmark right")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Content area
|
|
50
|
+
div(class: "overflow-y-auto px-4 pb-6 space-y-6") do
|
|
51
|
+
if @content_html
|
|
52
|
+
raw(@content_html)
|
|
53
|
+
elsif @content_block
|
|
54
|
+
instance_eval(&@content_block)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def default_attrs
|
|
63
|
+
{
|
|
64
|
+
id: "slideover",
|
|
65
|
+
class: slideover_classes
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def slideover_classes
|
|
70
|
+
base = "flex absolute right-0 flex-col h-full bg-white divide-y divide-gray-200 shadow-xl basis-3/12 z-50"
|
|
71
|
+
visibility = @open ? "" : "hidden"
|
|
72
|
+
[base, visibility].compact.join(" ")
|
|
15
73
|
end
|
|
16
74
|
end
|
|
17
75
|
end
|
|
@@ -6,6 +6,7 @@ module Panda
|
|
|
6
6
|
class TableComponent < Panda::Core::Base
|
|
7
7
|
prop :term, String
|
|
8
8
|
prop :rows, _Nilable(Object), default: -> { [] }
|
|
9
|
+
prop :icon, String, default: ""
|
|
9
10
|
|
|
10
11
|
attr_reader :columns
|
|
11
12
|
|
|
@@ -25,25 +26,22 @@ module Panda
|
|
|
25
26
|
end
|
|
26
27
|
end
|
|
27
28
|
|
|
28
|
-
def column(label, &cell_block)
|
|
29
|
-
@columns << Column.new(label, &cell_block)
|
|
29
|
+
def column(label, width: nil, &cell_block)
|
|
30
|
+
@columns << Column.new(label, width, &cell_block)
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
private
|
|
33
34
|
|
|
34
35
|
def render_table_with_rows
|
|
35
|
-
div(class: "table overflow-x-auto mb-12 w-full rounded-lg border border-dark") do
|
|
36
|
+
div(class: "table overflow-x-auto mb-12 w-full rounded-lg border border-dark", style: "table-layout: fixed;") do
|
|
36
37
|
render_header
|
|
37
38
|
render_rows
|
|
38
39
|
end
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
def render_empty_table
|
|
42
|
-
div(class: "
|
|
43
|
-
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
div(class: "text-center mx-12 block border border-dashed py-12 rounded-lg") do
|
|
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?
|
|
47
45
|
h3(class: "py-1 text-xl font-semibold text-gray-900") { "No #{@term.pluralize}" }
|
|
48
46
|
p(class: "py-1 text-base text-gray-500") { "Get started by creating a new #{@term}." }
|
|
49
47
|
end
|
|
@@ -57,7 +55,8 @@ module Panda
|
|
|
57
55
|
header_classes += " rounded-tl-md" if i.zero?
|
|
58
56
|
header_classes += " rounded-tr-md" if i == @columns.size - 1
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
header_style = column.width ? "width: #{column.width};" : nil
|
|
59
|
+
div(class: header_classes, style: header_style) { column.label }
|
|
61
60
|
end
|
|
62
61
|
end
|
|
63
62
|
end
|
|
@@ -106,10 +105,11 @@ module Panda
|
|
|
106
105
|
end
|
|
107
106
|
|
|
108
107
|
class Column
|
|
109
|
-
attr_reader :label, :cell
|
|
108
|
+
attr_reader :label, :cell, :width
|
|
110
109
|
|
|
111
|
-
def initialize(label, &block)
|
|
110
|
+
def initialize(label, width = nil, &block)
|
|
112
111
|
@label = label
|
|
112
|
+
@width = width
|
|
113
113
|
@cell = block
|
|
114
114
|
end
|
|
115
115
|
end
|
|
@@ -6,6 +6,7 @@ module Panda
|
|
|
6
6
|
class TagComponent < Panda::Core::Base
|
|
7
7
|
prop :status, Symbol, default: :active
|
|
8
8
|
prop :text, _Nilable(String), default: -> {}
|
|
9
|
+
prop :page_type, _Nilable(Symbol), default: -> {}
|
|
9
10
|
|
|
10
11
|
def view_template
|
|
11
12
|
span(class: tag_classes) { computed_text }
|
|
@@ -14,12 +15,44 @@ module Panda
|
|
|
14
15
|
private
|
|
15
16
|
|
|
16
17
|
def computed_text
|
|
17
|
-
|
|
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
|
|
18
34
|
end
|
|
19
35
|
|
|
20
36
|
def tag_classes
|
|
21
37
|
base = "inline-flex items-center py-1 px-2 text-xs font-medium rounded-md ring-1 ring-inset "
|
|
22
|
-
base + status_classes
|
|
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
|
|
23
56
|
end
|
|
24
57
|
|
|
25
58
|
def status_classes
|
|
@@ -30,6 +63,10 @@ module Panda
|
|
|
30
63
|
"text-black ring-black/30 bg-yellow-400"
|
|
31
64
|
when :inactive, :hidden
|
|
32
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"
|
|
33
70
|
else
|
|
34
71
|
"text-black bg-white"
|
|
35
72
|
end
|
|
@@ -30,15 +30,14 @@ module Panda
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def render_avatar
|
|
33
|
-
has_image = resolved_user.respond_to?(:
|
|
34
|
-
resolved_user.
|
|
35
|
-
!resolved_user.image_url.empty?
|
|
33
|
+
has_image = resolved_user.respond_to?(:avatar_url) &&
|
|
34
|
+
resolved_user.avatar_url.present?
|
|
36
35
|
|
|
37
36
|
if has_image
|
|
38
37
|
div do
|
|
39
38
|
img(
|
|
40
|
-
class: "inline-block w-10 h-10 rounded-full",
|
|
41
|
-
src: resolved_user.
|
|
39
|
+
class: "inline-block w-10 h-10 rounded-full object-cover",
|
|
40
|
+
src: resolved_user.avatar_url,
|
|
42
41
|
alt: ""
|
|
43
42
|
)
|
|
44
43
|
end
|
|
@@ -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: :
|
|
24
|
+
render :edit, locals: {user: current_user}, status: :unprocessable_content
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -36,7 +36,7 @@ module Panda
|
|
|
36
36
|
# @return ActionController::StrongParameters
|
|
37
37
|
def user_params
|
|
38
38
|
# Base parameters that Core always allows
|
|
39
|
-
base_params = [:name, :email, :current_theme]
|
|
39
|
+
base_params = [:name, :email, :current_theme, :avatar]
|
|
40
40
|
|
|
41
41
|
# Allow additional params from configuration
|
|
42
42
|
additional_params = Core.config.additional_user_params || []
|
|
@@ -30,7 +30,9 @@ module Panda
|
|
|
30
30
|
if user.persisted?
|
|
31
31
|
# Check if user is admin before allowing access
|
|
32
32
|
unless user.admin?
|
|
33
|
-
|
|
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
|
|
34
36
|
return
|
|
35
37
|
end
|
|
36
38
|
|
|
@@ -58,7 +60,9 @@ module Panda
|
|
|
58
60
|
strategy = params[:strategy] || "unknown"
|
|
59
61
|
|
|
60
62
|
Rails.logger.error "OmniAuth failure: strategy=#{strategy}, message=#{message}"
|
|
61
|
-
|
|
63
|
+
flash[:error] = "Authentication failed: #{message}"
|
|
64
|
+
flash.keep(:error) if Rails.env.test?
|
|
65
|
+
redirect_to admin_login_path
|
|
62
66
|
end
|
|
63
67
|
|
|
64
68
|
def destroy
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Admin
|
|
6
|
+
# Test-only controller for setting up authentication in system tests
|
|
7
|
+
# This bypasses OAuth to avoid cross-process issues with Capybara
|
|
8
|
+
# Security: This route is only defined in test environments, never in production
|
|
9
|
+
#
|
|
10
|
+
# Usage in tests:
|
|
11
|
+
# post "/admin/test_sessions", params: { user_id: user.id }
|
|
12
|
+
# post "/admin/test_sessions", params: { user_id: user.id, return_to: "/some/path" }
|
|
13
|
+
class TestSessionsController < ActionController::Base
|
|
14
|
+
# Enable CSRF protection for consistency, then skip for test-only endpoint
|
|
15
|
+
protect_from_forgery with: :exception
|
|
16
|
+
skip_before_action :verify_authenticity_token, raise: false
|
|
17
|
+
|
|
18
|
+
def create
|
|
19
|
+
user = Panda::Core::User.find(params[:user_id])
|
|
20
|
+
|
|
21
|
+
# Check if user is admin (mimics real OAuth behavior)
|
|
22
|
+
unless user.admin?
|
|
23
|
+
# Non-admin users are redirected to login with error (mimics real OAuth flow)
|
|
24
|
+
flash[:alert] = "You do not have permission to access the admin area."
|
|
25
|
+
# Keep flash for one more request to survive redirect in tests
|
|
26
|
+
flash.keep(:alert) if Rails.env.test?
|
|
27
|
+
# Use string path since route helpers aren't available in ActionController::Base
|
|
28
|
+
redirect_to "#{Panda::Core.config.admin_path || "/admin"}/login", allow_other_host: false, status: :found
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Set session (mimics real OAuth callback)
|
|
33
|
+
session[:user_id] = user.id
|
|
34
|
+
Panda::Core::Current.user = user
|
|
35
|
+
|
|
36
|
+
# Support custom redirect path for test flexibility
|
|
37
|
+
redirect_path = params[:return_to] || determine_default_redirect_path
|
|
38
|
+
redirect_to redirect_path, allow_other_host: false, status: :found
|
|
39
|
+
rescue ActiveRecord::RecordNotFound
|
|
40
|
+
render html: "User not found: #{params[:user_id]}", status: :not_found
|
|
41
|
+
rescue => e
|
|
42
|
+
render html: "Error: #{e.class} - #{e.message}<br>#{e.backtrace.first(5).join("<br>")}", status: :internal_server_error
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def determine_default_redirect_path
|
|
48
|
+
# Use configured dashboard path if available, otherwise default to admin root
|
|
49
|
+
if Panda::Core.config.dashboard_redirect_path
|
|
50
|
+
path = Panda::Core.config.dashboard_redirect_path
|
|
51
|
+
path.respond_to?(:call) ? path.call : path
|
|
52
|
+
else
|
|
53
|
+
# Use string path since route helpers aren't available in ActionController::Base
|
|
54
|
+
Panda::Core.config.admin_path || "/admin"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -10,13 +10,39 @@ module Panda
|
|
|
10
10
|
|
|
11
11
|
# Include only Core JavaScript
|
|
12
12
|
def panda_core_javascript
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# Use asset_tags for development mode (importmap) compatibility
|
|
14
|
+
# In development, this will use importmap; in production/test, compiled bundles
|
|
15
|
+
if Panda::Core::AssetLoader.use_github_assets?
|
|
16
|
+
js_url = Panda::Core::AssetLoader.javascript_url
|
|
17
|
+
return "" unless js_url
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
if js_url.start_with?("/panda-core-assets/")
|
|
20
|
+
javascript_include_tag(js_url)
|
|
21
|
+
else
|
|
22
|
+
javascript_include_tag(js_url, type: "module")
|
|
23
|
+
end
|
|
18
24
|
else
|
|
19
|
-
|
|
25
|
+
# Development mode - Load JavaScript with import map
|
|
26
|
+
# Files are served by Rack::Static middleware from engine's app/javascript
|
|
27
|
+
importmap_html = <<~HTML
|
|
28
|
+
<script type="importmap">
|
|
29
|
+
{
|
|
30
|
+
"imports": {
|
|
31
|
+
"@hotwired/stimulus": "/panda/core/vendor/@hotwired--stimulus.js",
|
|
32
|
+
"@hotwired/turbo": "/panda/core/vendor/@hotwired--turbo.js",
|
|
33
|
+
"@rails/actioncable/src": "/panda/core/vendor/@rails--actioncable--src.js",
|
|
34
|
+
"tailwindcss-stimulus-components": "/panda/core/tailwindcss-stimulus-components.js",
|
|
35
|
+
"@fortawesome/fontawesome-free": "https://ga.jspm.io/npm:@fortawesome/fontawesome-free@7.1.0/js/all.js",
|
|
36
|
+
"panda/core/application": "/panda/core/application.js",
|
|
37
|
+
"panda/core/controllers/toggle_controller": "/panda/core/controllers/toggle_controller.js",
|
|
38
|
+
"panda/core/controllers/theme_form_controller": "/panda/core/controllers/theme_form_controller.js"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
<script type="module" src="/panda/core/application.js"></script>
|
|
43
|
+
<script type="module" src="/panda/core/controllers/index.js"></script>
|
|
44
|
+
HTML
|
|
45
|
+
importmap_html.html_safe
|
|
20
46
|
end
|
|
21
47
|
end
|
|
22
48
|
|