plutonium 0.34.1 → 0.35.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/.claude/skills/plutonium/skill.md +53 -0
- data/.claude/skills/{assets → plutonium-assets}/SKILL.md +13 -8
- data/.claude/skills/{connect-resource → plutonium-connect-resource}/SKILL.md +1 -1
- data/.claude/skills/{controller → plutonium-controller}/SKILL.md +27 -13
- data/.claude/skills/{create-resource → plutonium-create-resource}/SKILL.md +1 -1
- data/.claude/skills/{definition → plutonium-definition}/SKILL.md +10 -10
- data/.claude/skills/{definition-actions → plutonium-definition-actions}/SKILL.md +34 -9
- data/.claude/skills/{definition-fields → plutonium-definition-fields}/SKILL.md +38 -10
- data/.claude/skills/plutonium-definition-query/SKILL.md +356 -0
- data/.claude/skills/{forms → plutonium-forms}/SKILL.md +6 -6
- data/.claude/skills/{installation → plutonium-installation}/SKILL.md +9 -9
- data/.claude/skills/{interaction → plutonium-interaction}/SKILL.md +20 -19
- data/.claude/skills/{model → plutonium-model}/SKILL.md +3 -3
- data/.claude/skills/{model-features → plutonium-model-features}/SKILL.md +3 -3
- data/.claude/skills/{nested-resources → plutonium-nested-resources}/SKILL.md +5 -5
- data/.claude/skills/{package → plutonium-package}/SKILL.md +7 -8
- data/.claude/skills/{policy → plutonium-policy}/SKILL.md +26 -4
- data/.claude/skills/{portal → plutonium-portal}/SKILL.md +33 -31
- data/.claude/skills/{resource → plutonium-resource}/SKILL.md +27 -27
- data/.claude/skills/{rodauth → plutonium-rodauth}/SKILL.md +5 -5
- data/.claude/skills/plutonium-theming/SKILL.md +424 -0
- data/.claude/skills/{views → plutonium-views}/SKILL.md +7 -7
- data/CHANGELOG.md +52 -0
- data/CLAUDE.md +215 -0
- data/CONTRIBUTING.md +72 -18
- data/README.md +100 -19
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1685 -1146
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +70 -70
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/resource/interactive_bulk_action.html.erb +1 -5
- data/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
- data/app/views/rodauth/_login_form.html.erb +15 -55
- data/app/views/rodauth/_login_form_footer.html.erb +2 -2
- data/app/views/rodauth/_password_visibility.html.erb +2 -8
- data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
- data/app/views/rodauth/change_login.html.erb +36 -19
- data/app/views/rodauth/change_password.html.erb +34 -10
- data/app/views/rodauth/close_account.html.erb +12 -4
- data/app/views/rodauth/confirm_password.html.erb +19 -17
- data/app/views/rodauth/create_account.html.erb +30 -109
- data/app/views/rodauth/email_auth.html.erb +1 -1
- data/app/views/rodauth/logout.html.erb +4 -4
- data/app/views/rodauth/otp_auth.html.erb +13 -4
- data/app/views/rodauth/otp_disable.html.erb +12 -4
- data/app/views/rodauth/otp_setup.html.erb +29 -12
- data/app/views/rodauth/otp_unlock.html.erb +19 -10
- data/app/views/rodauth/otp_unlock_not_available.html.erb +7 -7
- data/app/views/rodauth/recovery_auth.html.erb +12 -4
- data/app/views/rodauth/recovery_codes.html.erb +12 -4
- data/app/views/rodauth/remember.html.erb +7 -7
- data/app/views/rodauth/reset_password.html.erb +23 -7
- data/app/views/rodauth/reset_password_request.html.erb +14 -10
- data/app/views/rodauth/sms_auth.html.erb +13 -4
- data/app/views/rodauth/sms_confirm.html.erb +13 -4
- data/app/views/rodauth/sms_disable.html.erb +12 -4
- data/app/views/rodauth/sms_request.html.erb +1 -1
- data/app/views/rodauth/sms_setup.html.erb +23 -7
- data/app/views/rodauth/two_factor_auth.html.erb +2 -2
- data/app/views/rodauth/two_factor_disable.html.erb +12 -4
- data/app/views/rodauth/two_factor_manage.html.erb +7 -7
- data/app/views/rodauth/unlock_account.html.erb +13 -5
- data/app/views/rodauth/unlock_account_request.html.erb +2 -2
- data/app/views/rodauth/verify_account.html.erb +25 -7
- data/app/views/rodauth/verify_account_resend.html.erb +14 -10
- data/app/views/rodauth/verify_login_change.html.erb +1 -1
- data/app/views/rodauth/webauthn_auth.html.erb +1 -1
- data/app/views/rodauth/webauthn_remove.html.erb +18 -8
- data/app/views/rodauth/webauthn_setup.html.erb +12 -4
- data/docs/.vitepress/config.ts +15 -26
- data/docs/.vitepress/theme/custom.css +388 -29
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/tutorial/02-first-resource.md +9 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +2 -2
- data/docs/getting-started/tutorial/07-author-portal.md +191 -0
- data/docs/getting-started/tutorial/{07-customizing-ui.md → 08-customizing-ui.md} +7 -7
- data/docs/getting-started/tutorial/index.md +5 -2
- data/docs/guides/authorization.md +33 -0
- data/docs/guides/creating-packages.md +12 -16
- data/docs/guides/custom-actions.md +36 -0
- data/docs/guides/search-filtering.md +121 -42
- data/docs/guides/theming.md +232 -36
- data/docs/index.md +203 -57
- data/docs/public/og-image.png +0 -0
- data/docs/reference/controller/index.md +14 -16
- data/docs/reference/definition/actions.md +38 -3
- data/docs/reference/definition/fields.md +3 -3
- data/docs/reference/definition/index.md +2 -2
- data/docs/reference/generators/index.md +0 -1
- data/docs/reference/interaction/index.md +14 -10
- data/docs/reference/model/index.md +0 -1
- data/docs/reference/portal/index.md +13 -27
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/pkg/portal/portal_generator.rb +0 -2
- data/lib/generators/pu/pkg/portal/templates/app/views/package/dashboard/index.html.erb +28 -72
- data/lib/plutonium/action/interactive.rb +2 -2
- data/lib/plutonium/core/controller.rb +2 -1
- data/lib/plutonium/definition/actions.rb +2 -2
- data/lib/plutonium/lib/deep_freezer.rb +3 -7
- data/lib/plutonium/query/filter.rb +14 -0
- data/lib/plutonium/query/filters/association.rb +49 -0
- data/lib/plutonium/query/filters/boolean.rb +35 -0
- data/lib/plutonium/query/filters/date.rb +97 -0
- data/lib/plutonium/query/filters/date_range.rb +58 -0
- data/lib/plutonium/query/filters/select.rb +55 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +24 -6
- data/lib/plutonium/resource/controllers/interactive_actions.rb +76 -58
- data/lib/plutonium/resource/controllers/queryable.rb +4 -2
- data/lib/plutonium/resource/query_object.rb +1 -1
- data/lib/plutonium/ui/action_button.rb +23 -65
- data/lib/plutonium/ui/actions_dropdown.rb +103 -0
- data/lib/plutonium/ui/block.rb +1 -1
- data/lib/plutonium/ui/breadcrumbs.rb +12 -19
- data/lib/plutonium/ui/color_mode_selector.rb +1 -1
- data/lib/plutonium/ui/component/kit.rb +6 -0
- data/lib/plutonium/ui/component_classes.rb +102 -0
- data/lib/plutonium/ui/display/base.rb +15 -0
- data/lib/plutonium/ui/display/components/attachment.rb +6 -5
- data/lib/plutonium/ui/display/components/boolean.rb +23 -0
- data/lib/plutonium/ui/display/components/color.rb +23 -0
- data/lib/plutonium/ui/display/resource.rb +1 -1
- data/lib/plutonium/ui/display/theme.rb +29 -15
- data/lib/plutonium/ui/empty_card.rb +3 -3
- data/lib/plutonium/ui/form/base.rb +20 -0
- data/lib/plutonium/ui/form/components/key_value_store.rb +11 -11
- data/lib/plutonium/ui/form/components/resource_select.rb +31 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +1 -2
- data/lib/plutonium/ui/form/components/uppy.rb +5 -4
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +4 -4
- data/lib/plutonium/ui/form/interaction.rb +17 -1
- data/lib/plutonium/ui/form/query.rb +133 -80
- data/lib/plutonium/ui/form/theme.rb +50 -35
- data/lib/plutonium/ui/frame_navigator_panel.rb +2 -2
- data/lib/plutonium/ui/layout/base.rb +1 -1
- data/lib/plutonium/ui/layout/header.rb +4 -7
- data/lib/plutonium/ui/layout/rodauth_layout.rb +7 -7
- data/lib/plutonium/ui/layout/sidebar.rb +1 -1
- data/lib/plutonium/ui/nav_grid_menu.rb +7 -6
- data/lib/plutonium/ui/nav_user.rb +9 -8
- data/lib/plutonium/ui/page/interactive_action.rb +5 -5
- data/lib/plutonium/ui/page_header.rb +29 -10
- data/lib/plutonium/ui/panel.rb +4 -4
- data/lib/plutonium/ui/sidebar_menu.rb +8 -8
- data/lib/plutonium/ui/skeleton_table.rb +7 -8
- data/lib/plutonium/ui/tab_list.rb +5 -5
- data/lib/plutonium/ui/table/base.rb +3 -0
- data/lib/plutonium/ui/table/components/attachment.rb +4 -3
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +82 -0
- data/lib/plutonium/ui/table/components/pagy_info.rb +2 -2
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +13 -8
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +101 -0
- data/lib/plutonium/ui/table/components/scopes_bar.rb +2 -2
- data/lib/plutonium/ui/table/components/selection_column.rb +100 -0
- data/lib/plutonium/ui/table/display_theme.rb +6 -6
- data/lib/plutonium/ui/table/resource.rb +93 -52
- data/lib/plutonium/ui/table/theme.rb +28 -15
- data/lib/plutonium/version.rb +1 -1
- data/package.json +2 -2
- data/plutonium.gemspec +5 -4
- data/src/css/components.css +471 -0
- data/src/css/intl_tel_input.css +2 -2
- data/src/css/plutonium.css +2 -0
- data/src/css/tokens.css +149 -0
- data/src/js/controllers/bulk_actions_controller.js +109 -0
- data/src/js/controllers/filter_panel_controller.js +35 -0
- data/src/js/controllers/register_controllers.js +5 -1
- data/src/js/controllers/resource_drop_down_controller.js +25 -1
- data/src/js/controllers/slim_select_controller.js +6 -2
- data/src/js/turbo/turbo_actions.js +1 -1
- metadata +52 -39
- data/.claude/skills/definition-query/SKILL.md +0 -334
- data/docs/concepts/architecture.md +0 -226
- data/docs/concepts/auto-detection.md +0 -254
- data/docs/concepts/index.md +0 -61
- data/docs/concepts/packages-portals.md +0 -304
- data/docs/concepts/resources.md +0 -224
- data/docs/cookbook/blog.md +0 -411
- data/docs/cookbook/index.md +0 -289
- data/docs/cookbook/saas.md +0 -481
- data/docs/public/CLAUDE.md +0 -578
- data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +0 -5
|
@@ -47,12 +47,12 @@ module Plutonium
|
|
|
47
47
|
def render_header
|
|
48
48
|
div(class: "key-value-store-header") do
|
|
49
49
|
if attributes[:label]
|
|
50
|
-
h3(class: "text-lg font-semibold text-
|
|
50
|
+
h3(class: "text-lg font-semibold text-[var(--pu-text)]") do
|
|
51
51
|
plain attributes[:label]
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
if attributes[:description]
|
|
55
|
-
p(class: "text-sm text-
|
|
55
|
+
p(class: "text-sm text-[var(--pu-text-muted)]") do
|
|
56
56
|
plain attributes[:description]
|
|
57
57
|
end
|
|
58
58
|
end
|
|
@@ -69,7 +69,7 @@ module Plutonium
|
|
|
69
69
|
|
|
70
70
|
def render_key_value_pair(key, value, index)
|
|
71
71
|
div(
|
|
72
|
-
class: "key-value-pair flex items-center gap-2 p-2 border border-
|
|
72
|
+
class: "key-value-pair flex items-center gap-2 p-2 border border-[var(--pu-border)] rounded-[var(--pu-radius-sm)]",
|
|
73
73
|
data_key_value_store_target: "pair"
|
|
74
74
|
) do
|
|
75
75
|
# Key input
|
|
@@ -79,7 +79,7 @@ module Plutonium
|
|
|
79
79
|
value: key,
|
|
80
80
|
name: "#{field_name}[#{index}][key]",
|
|
81
81
|
id: "#{field.dom.id}_#{index}_key",
|
|
82
|
-
class: "flex-1 px-3 py-1 text-sm border border-
|
|
82
|
+
class: "flex-1 px-3 py-1 text-sm border border-[var(--pu-border)] rounded-[var(--pu-radius-sm)] focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-[var(--pu-surface)] text-[var(--pu-text)]",
|
|
83
83
|
data_key_value_store_target: "keyInput"
|
|
84
84
|
)
|
|
85
85
|
|
|
@@ -90,14 +90,14 @@ module Plutonium
|
|
|
90
90
|
value: value,
|
|
91
91
|
name: "#{field_name}[#{index}][value]",
|
|
92
92
|
id: "#{field.dom.id}_#{index}_value",
|
|
93
|
-
class: "flex-1 px-3 py-1 text-sm border border-
|
|
93
|
+
class: "flex-1 px-3 py-1 text-sm border border-[var(--pu-border)] rounded-[var(--pu-radius-sm)] focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-[var(--pu-surface)] text-[var(--pu-text)]",
|
|
94
94
|
data_key_value_store_target: "valueInput"
|
|
95
95
|
)
|
|
96
96
|
|
|
97
97
|
# Remove button
|
|
98
98
|
button(
|
|
99
99
|
type: :button,
|
|
100
|
-
class: "px-2 py-1 text-
|
|
100
|
+
class: "px-2 py-1 text-danger-600 hover:text-danger-800 focus:outline-none transition-colors",
|
|
101
101
|
data_action: "key-value-store#removePair"
|
|
102
102
|
) do
|
|
103
103
|
plain "×"
|
|
@@ -110,7 +110,7 @@ module Plutonium
|
|
|
110
110
|
button(
|
|
111
111
|
type: :button,
|
|
112
112
|
id: "#{field.dom.id}_add_button",
|
|
113
|
-
class: "
|
|
113
|
+
class: "pu-btn pu-btn-sm pu-btn-soft-primary",
|
|
114
114
|
data: {
|
|
115
115
|
action: "key-value-store#addPair",
|
|
116
116
|
key_value_store_target: "addButton"
|
|
@@ -124,7 +124,7 @@ module Plutonium
|
|
|
124
124
|
def render_template
|
|
125
125
|
template(data_key_value_store_target: "template") do
|
|
126
126
|
div(
|
|
127
|
-
class: "key-value-pair flex items-center gap-2 p-2 border border-
|
|
127
|
+
class: "key-value-pair flex items-center gap-2 p-2 border border-[var(--pu-border)] rounded-[var(--pu-radius-sm)]",
|
|
128
128
|
data_key_value_store_target: "pair"
|
|
129
129
|
) do
|
|
130
130
|
input(
|
|
@@ -132,7 +132,7 @@ module Plutonium
|
|
|
132
132
|
placeholder: "Key",
|
|
133
133
|
name: "#{field_name}[__INDEX__][key]",
|
|
134
134
|
id: "#{field.dom.id}___INDEX___key",
|
|
135
|
-
class: "flex-1 px-3 py-1 text-sm border border-
|
|
135
|
+
class: "flex-1 px-3 py-1 text-sm border border-[var(--pu-border)] rounded-[var(--pu-radius-sm)] focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-[var(--pu-surface)] text-[var(--pu-text)]",
|
|
136
136
|
data_key_value_store_target: "keyInput"
|
|
137
137
|
)
|
|
138
138
|
|
|
@@ -141,13 +141,13 @@ module Plutonium
|
|
|
141
141
|
placeholder: "Value",
|
|
142
142
|
name: "#{field_name}[__INDEX__][value]",
|
|
143
143
|
id: "#{field.dom.id}___INDEX___value",
|
|
144
|
-
class: "flex-1 px-3 py-1 text-sm border border-
|
|
144
|
+
class: "flex-1 px-3 py-1 text-sm border border-[var(--pu-border)] rounded-[var(--pu-radius-sm)] focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-[var(--pu-surface)] text-[var(--pu-text)]",
|
|
145
145
|
data_key_value_store_target: "valueInput"
|
|
146
146
|
)
|
|
147
147
|
|
|
148
148
|
button(
|
|
149
149
|
type: :button,
|
|
150
|
-
class: "px-2 py-1 text-
|
|
150
|
+
class: "px-2 py-1 text-danger-600 hover:text-danger-800 focus:outline-none transition-colors",
|
|
151
151
|
data_action: "key-value-store#removePair"
|
|
152
152
|
) do
|
|
153
153
|
plain "×"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Form
|
|
6
|
+
module Components
|
|
7
|
+
# Select for choosing a resource record
|
|
8
|
+
class ResourceSelect < Phlexi::Form::Components::Select
|
|
9
|
+
protected
|
|
10
|
+
|
|
11
|
+
def choices
|
|
12
|
+
@choices ||= begin
|
|
13
|
+
collection = attributes.delete(:choices) || @association_class&.all || []
|
|
14
|
+
build_choice_mapper(collection)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def build_attributes
|
|
19
|
+
super
|
|
20
|
+
@association_class = attributes.delete(:association_class)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Use include_blank string as blank option text (Phlexi default uses placeholder)
|
|
24
|
+
def blank_option_text
|
|
25
|
+
@include_blank.is_a?(String) ? @include_blank : super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -23,8 +23,7 @@ module Plutonium
|
|
|
23
23
|
|
|
24
24
|
a(
|
|
25
25
|
href: add_url,
|
|
26
|
-
class:
|
|
27
|
-
"bg-gray-100 dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-500 hover:bg-gray-200 border border-gray-300 rounded-lg p-3 focus:ring-gray-100 dark:focus:ring-gray-700 focus:ring-2 focus:outline-none dark:text-white"
|
|
26
|
+
class: "bg-[var(--pu-surface-alt)] hover:bg-[var(--pu-border)] border border-[var(--pu-border)] rounded-[var(--pu-radius-md)] p-3 focus:ring-2 focus:ring-[var(--pu-border)] focus:outline-none text-[var(--pu-text)] transition-colors"
|
|
28
27
|
) do
|
|
29
28
|
render Phlex::TablerIcons::Plus.new(class: "w-3 h-3")
|
|
30
29
|
end
|
|
@@ -67,7 +67,8 @@ module Plutonium
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
div(
|
|
70
|
-
class: "attachment-preview group relative bg-
|
|
70
|
+
class: "attachment-preview group relative bg-[var(--pu-surface)] border border-[var(--pu-border)] rounded-[var(--pu-radius-md)] hover:shadow-md transition-all duration-300",
|
|
71
|
+
style: "box-shadow: var(--pu-shadow-sm)",
|
|
71
72
|
data: {
|
|
72
73
|
controller: "attachment-preview",
|
|
73
74
|
attachment_preview_mime_type_value: attachment.content_type,
|
|
@@ -99,7 +100,7 @@ module Plutonium
|
|
|
99
100
|
)
|
|
100
101
|
else
|
|
101
102
|
div(
|
|
102
|
-
class: "w-full h-full flex items-center justify-center bg-
|
|
103
|
+
class: "w-full h-full flex items-center justify-center bg-[var(--pu-surface-alt)] text-[var(--pu-text-muted)] font-mono"
|
|
103
104
|
) do
|
|
104
105
|
".#{attachment_extension(attachment)}"
|
|
105
106
|
end
|
|
@@ -109,7 +110,7 @@ module Plutonium
|
|
|
109
110
|
|
|
110
111
|
def render_filename(attachment)
|
|
111
112
|
div(
|
|
112
|
-
class: "px-2 py-1.5 text-sm text-
|
|
113
|
+
class: "px-2 py-1.5 text-sm text-[var(--pu-text-muted)] border-t border-[var(--pu-border)] truncate text-center bg-[var(--pu-surface)]",
|
|
113
114
|
title: attachment.filename
|
|
114
115
|
) do
|
|
115
116
|
plain attachment.filename.to_s
|
|
@@ -119,7 +120,7 @@ module Plutonium
|
|
|
119
120
|
def render_delete_button
|
|
120
121
|
button(
|
|
121
122
|
type: "button",
|
|
122
|
-
class: "w-full py-2 px-4 text-sm text-
|
|
123
|
+
class: "w-full py-2 px-4 text-sm text-danger-600 dark:text-danger-400 bg-[var(--pu-surface)] hover:bg-danger-50 dark:hover:bg-danger-900/50 rounded-b-[var(--pu-radius-md)] transition-colors duration-200 flex items-center justify-center gap-2 border-t border-[var(--pu-border)]",
|
|
123
124
|
data: {action: "click->attachment-preview#remove"}
|
|
124
125
|
) do
|
|
125
126
|
span(class: "bi bi-trash")
|
|
@@ -143,13 +143,13 @@ module Plutonium
|
|
|
143
143
|
|
|
144
144
|
def render_nested_field_header(context)
|
|
145
145
|
div do
|
|
146
|
-
h2(class: "text-lg font-semibold text-
|
|
146
|
+
h2(class: "text-lg font-semibold text-[var(--pu-text)]") { context.name.to_s.humanize }
|
|
147
147
|
render_nested_fields_header_description(context.options[:description]) if context.options[:description]
|
|
148
148
|
end
|
|
149
149
|
end
|
|
150
150
|
|
|
151
151
|
def render_nested_fields_header_description(description)
|
|
152
|
-
p(class: "text-md font-normal text-
|
|
152
|
+
p(class: "text-md font-normal text-[var(--pu-text-muted)]") { description }
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
def render_nested_field_content(context)
|
|
@@ -193,7 +193,7 @@ module Plutonium
|
|
|
193
193
|
def render_nested_fields_fieldset(nested, context)
|
|
194
194
|
fieldset(
|
|
195
195
|
data_new_record: !nested.object&.persisted?,
|
|
196
|
-
class: "nested-resource-form-fields border border-
|
|
196
|
+
class: "nested-resource-form-fields border border-[var(--pu-border)] rounded-[var(--pu-radius-md)] p-4 space-y-4 relative"
|
|
197
197
|
) do
|
|
198
198
|
render_nested_fields_fieldset_content(nested, context)
|
|
199
199
|
render_nested_fields_delete_button(nested, context.options)
|
|
@@ -238,7 +238,7 @@ module Plutonium
|
|
|
238
238
|
def render_nested_fields_delete_checkbox
|
|
239
239
|
input(
|
|
240
240
|
type: :checkbox,
|
|
241
|
-
class: "w-4 h-4 ms-2 text-
|
|
241
|
+
class: "w-4 h-4 ms-2 text-danger-600 bg-danger-100 border-danger-300 rounded focus:ring-danger-500 dark:focus:ring-danger-600 focus:ring-2 dark:bg-[var(--pu-surface-alt)] dark:border-[var(--pu-border)] cursor-pointer",
|
|
242
242
|
data_action: "nested-resource-form-fields#remove"
|
|
243
243
|
)
|
|
244
244
|
end
|
|
@@ -38,7 +38,7 @@ module Plutonium
|
|
|
38
38
|
:commit_interactive_record_action
|
|
39
39
|
when :interactive_resource_action
|
|
40
40
|
:commit_interactive_resource_action
|
|
41
|
-
when :
|
|
41
|
+
when :interactive_bulk_action
|
|
42
42
|
:commit_interactive_bulk_action
|
|
43
43
|
else
|
|
44
44
|
action_name
|
|
@@ -47,9 +47,25 @@ module Plutonium
|
|
|
47
47
|
|
|
48
48
|
def initialize_attributes
|
|
49
49
|
super
|
|
50
|
+
attributes[:id] = :interaction_form
|
|
50
51
|
attributes.fetch(:data_turbo) { attributes[:data_turbo] = object.turbo.to_s }
|
|
51
52
|
end
|
|
52
53
|
|
|
54
|
+
def render_bulk_action_ids
|
|
55
|
+
action = helpers.current_interactive_action
|
|
56
|
+
return unless action&.bulk_action?
|
|
57
|
+
|
|
58
|
+
ids = Array(helpers.params[:ids])
|
|
59
|
+
ids.each do |id|
|
|
60
|
+
input(type: :hidden, name: "ids[]", value: id)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def form_template
|
|
65
|
+
render_bulk_action_ids
|
|
66
|
+
super
|
|
67
|
+
end
|
|
68
|
+
|
|
53
69
|
def submit_button(*, **)
|
|
54
70
|
super do
|
|
55
71
|
object.label
|
|
@@ -11,7 +11,6 @@ module Plutonium
|
|
|
11
11
|
options[:method] = :get
|
|
12
12
|
attributes = mix(attributes.deep_merge(
|
|
13
13
|
id: :search_form,
|
|
14
|
-
class!: "space-y-2 mb-4",
|
|
15
14
|
controller: "form",
|
|
16
15
|
data: {controller: "form", turbo_frame: nil}
|
|
17
16
|
))
|
|
@@ -21,6 +20,10 @@ module Plutonium
|
|
|
21
20
|
@page_size = page_size
|
|
22
21
|
end
|
|
23
22
|
|
|
23
|
+
def form_class
|
|
24
|
+
"mb-4"
|
|
25
|
+
end
|
|
26
|
+
|
|
24
27
|
def form_template
|
|
25
28
|
render_fields
|
|
26
29
|
end
|
|
@@ -28,9 +31,23 @@ module Plutonium
|
|
|
28
31
|
private
|
|
29
32
|
|
|
30
33
|
def render_fields
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
has_search = query_object.search_filter.present?
|
|
35
|
+
has_filters = query_object.filter_definitions.present?
|
|
36
|
+
|
|
37
|
+
if has_search || has_filters
|
|
38
|
+
div(class: "flex items-center gap-3") do
|
|
39
|
+
# Search takes remaining space
|
|
40
|
+
if has_search
|
|
41
|
+
render_search_field
|
|
42
|
+
else
|
|
43
|
+
div(class: "flex-1") # Spacer when no search
|
|
44
|
+
end
|
|
45
|
+
render_filter_button if has_filters
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Hidden fields for sorting, scope, etc.
|
|
50
|
+
div(hidden: true) do
|
|
34
51
|
input(name: "limit", value: @page_size, type: :hidden, hidden: true) if @page_size
|
|
35
52
|
render_sort_fields
|
|
36
53
|
render_scope_fields
|
|
@@ -38,13 +55,11 @@ module Plutonium
|
|
|
38
55
|
end
|
|
39
56
|
|
|
40
57
|
def render_sort_fields
|
|
41
|
-
# q[sort_fields][]=name&q[sort_fields][]=created_at
|
|
42
58
|
field :sort_fields do |name|
|
|
43
59
|
render name.input_array_tag do |array|
|
|
44
60
|
render array.input_tag(type: :hidden, hidden: true)
|
|
45
61
|
end
|
|
46
62
|
end
|
|
47
|
-
# q[sort_directions][created_at]=ASC&q[sort_directions][name]=ASC&
|
|
48
63
|
nest_one :sort_directions do |nested|
|
|
49
64
|
query_object.sort_definitions.each do |filter_name, definition|
|
|
50
65
|
nested.field(filter_name) do |f|
|
|
@@ -55,118 +70,156 @@ module Plutonium
|
|
|
55
70
|
end
|
|
56
71
|
|
|
57
72
|
def render_scope_fields
|
|
58
|
-
# q[scope]=&
|
|
59
73
|
return if query_object.scope_definitions.blank?
|
|
60
74
|
|
|
61
75
|
render field(:scope).input_tag(type: :hidden, hidden: true)
|
|
62
76
|
end
|
|
63
77
|
|
|
64
|
-
def
|
|
65
|
-
# q[search]=&
|
|
66
|
-
return unless query_object.search_filter
|
|
67
|
-
|
|
78
|
+
def render_search_field
|
|
68
79
|
search_query = query_object.search_query
|
|
69
|
-
div(class: "relative") do
|
|
80
|
+
div(class: "relative flex-1 min-w-0") do
|
|
70
81
|
div(class: "absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none") do
|
|
71
|
-
|
|
72
|
-
class: "w-5 h-5 text-gray-500 dark:text-gray-400",
|
|
73
|
-
aria_hidden: "true",
|
|
74
|
-
fill: "currentColor",
|
|
75
|
-
viewbox: "0 0 20 20",
|
|
76
|
-
xmlns: "http://www.w3.org/2000/svg"
|
|
77
|
-
) do |s|
|
|
78
|
-
s.path(
|
|
79
|
-
fill_rule: "evenodd",
|
|
80
|
-
d:
|
|
81
|
-
"M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z",
|
|
82
|
-
clip_rule: "evenodd"
|
|
83
|
-
)
|
|
84
|
-
end
|
|
82
|
+
render Phlex::TablerIcons::Search.new(class: "w-5 h-5 text-[var(--pu-text-muted)]")
|
|
85
83
|
end
|
|
86
84
|
render field(:search, value: search_query)
|
|
87
85
|
.placeholder("Search...")
|
|
88
86
|
.input_tag(
|
|
89
87
|
value: search_query,
|
|
90
|
-
class: "
|
|
91
|
-
data: {
|
|
92
|
-
action: "form#submit",
|
|
93
|
-
turbo_permanent: true
|
|
94
|
-
}
|
|
88
|
+
class: "pu-input pu-input-icon-left w-full",
|
|
89
|
+
data: {action: "form#submit", turbo_permanent: true}
|
|
95
90
|
)
|
|
96
91
|
end
|
|
97
92
|
end
|
|
98
93
|
|
|
99
|
-
def
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
div(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
94
|
+
def render_filter_button
|
|
95
|
+
active_count = count_active_filters
|
|
96
|
+
|
|
97
|
+
div(
|
|
98
|
+
class: "relative",
|
|
99
|
+
data: {controller: "resource-drop-down", resource_drop_down_placement_value: "left-start"}
|
|
100
|
+
) do
|
|
101
|
+
# Filter button (trigger)
|
|
102
|
+
button(
|
|
103
|
+
type: "button",
|
|
104
|
+
class: "pu-btn pu-btn-secondary px-4 py-3 text-base",
|
|
105
|
+
data: {resource_drop_down_target: "trigger"}
|
|
106
|
+
) do
|
|
107
|
+
render Phlex::TablerIcons::Filter.new(class: "w-4 h-4 inline-block align-text-bottom")
|
|
108
|
+
plain " Filters"
|
|
109
|
+
if active_count > 0
|
|
110
|
+
plain " "
|
|
111
|
+
span(class: "inline-flex items-center justify-center w-5 h-5 text-xs font-semibold rounded-full text-gray-800 bg-white") do
|
|
112
|
+
plain active_count.to_s
|
|
112
113
|
end
|
|
113
114
|
end
|
|
114
115
|
end
|
|
115
|
-
div(class: "flex flex-wrap items-center gap-2") do
|
|
116
|
-
actions_wrapper do
|
|
117
|
-
render field(:submit).submit_button_tag(
|
|
118
|
-
name: nil,
|
|
119
|
-
class!: "inline-flex items-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
|
120
|
-
) do
|
|
121
|
-
render Phlex::TablerIcons::Filter.new(class: "w-4 h-4 mr-2")
|
|
122
|
-
plain "Apply Filters"
|
|
123
|
-
end
|
|
124
116
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
117
|
+
# Filter panel (dropdown menu)
|
|
118
|
+
# Mobile: fullscreen (override Popper with !important)
|
|
119
|
+
# Desktop: Popper positions the dropdown
|
|
120
|
+
div(
|
|
121
|
+
class: "hidden z-[100] bg-[var(--pu-surface)] shadow-lg flex flex-col " \
|
|
122
|
+
"max-md:!fixed max-md:!inset-0 max-md:!transform-none " \
|
|
123
|
+
"md:w-80 md:max-h-[70vh] md:border md:border-[var(--pu-border)] md:rounded-[var(--pu-radius-lg)]",
|
|
124
|
+
data: {resource_drop_down_target: "menu", controller: "filter-panel"},
|
|
125
|
+
aria_hidden: "true"
|
|
126
|
+
) do
|
|
127
|
+
render_filter_panel
|
|
134
128
|
end
|
|
135
129
|
end
|
|
136
130
|
end
|
|
137
131
|
|
|
138
|
-
def
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
132
|
+
def render_filter_panel
|
|
133
|
+
# Sticky header
|
|
134
|
+
div(class: "sticky top-0 z-10 flex items-center justify-between p-4 bg-[var(--pu-surface)] border-b border-[var(--pu-border)]") do
|
|
135
|
+
div(class: "flex items-center gap-3") do
|
|
136
|
+
# Close button (mobile only)
|
|
137
|
+
button(
|
|
138
|
+
type: "button",
|
|
139
|
+
class: "md:hidden p-1 text-[var(--pu-text-muted)] hover:text-[var(--pu-text)]",
|
|
140
|
+
data: {action: "resource-drop-down#hide"}
|
|
141
|
+
) do
|
|
142
|
+
render Phlex::TablerIcons::X.new(class: "w-5 h-5")
|
|
143
|
+
end
|
|
144
|
+
span(class: "text-sm font-semibold text-[var(--pu-text)]") { "Filters" }
|
|
145
|
+
end
|
|
146
|
+
button(
|
|
147
|
+
type: "button",
|
|
148
|
+
class: "text-sm text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] transition-colors",
|
|
149
|
+
data: {action: "filter-panel#clear"}
|
|
150
|
+
) { "Clear all" }
|
|
151
|
+
end
|
|
142
152
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
# Scrollable filter fields
|
|
154
|
+
div(class: "p-4 overflow-y-auto flex-1 space-y-4") do
|
|
155
|
+
query_object.filter_definitions.each do |filter_name, definition|
|
|
156
|
+
nest_one filter_name do |nested|
|
|
157
|
+
inputs = definition.defined_inputs
|
|
158
|
+
has_multiple_inputs = inputs.size > 1
|
|
159
|
+
inputs.each do |input_name, _|
|
|
160
|
+
# For multi-input filters (like date range), include the input name in the label
|
|
161
|
+
label = if has_multiple_inputs
|
|
162
|
+
"#{filter_name.to_s.humanize} (#{input_name.to_s.humanize.downcase})"
|
|
163
|
+
else
|
|
164
|
+
filter_name.to_s.humanize
|
|
165
|
+
end
|
|
166
|
+
render_filter_field nested, definition, input_name, filter_label: label
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
151
171
|
|
|
152
|
-
|
|
172
|
+
# Sticky footer
|
|
173
|
+
div(class: "sticky bottom-0 z-10 p-4 bg-[var(--pu-surface)] border-t border-[var(--pu-border)]") do
|
|
174
|
+
render field(:submit).submit_button_tag(
|
|
175
|
+
name: nil,
|
|
176
|
+
class!: "pu-btn pu-btn-md pu-btn-primary w-full"
|
|
177
|
+
) { "Apply Filters" }
|
|
178
|
+
end
|
|
179
|
+
end
|
|
153
180
|
|
|
154
|
-
|
|
181
|
+
def render_filter_field(nested, resource_definition, name, filter_label: nil)
|
|
182
|
+
input_definition = resource_definition.defined_inputs[name] || {}
|
|
155
183
|
input_options = input_definition[:options] || {}
|
|
184
|
+
field_options = resource_definition.defined_fields[name] ? resource_definition.defined_fields[name][:options].dup : {}
|
|
156
185
|
|
|
157
186
|
tag = input_options[:as] || field_options[:as]
|
|
158
187
|
tag_attributes = input_options.except(:wrapper, :as)
|
|
188
|
+
|
|
159
189
|
tag_block = input_definition[:block] || ->(f) {
|
|
160
190
|
tag ||= f.inferred_field_component
|
|
161
|
-
f.send(:"#{tag}_tag", **tag_attributes, class:
|
|
191
|
+
f.send(:"#{tag}_tag", **tag_attributes, class: "w-full")
|
|
162
192
|
}
|
|
163
193
|
|
|
164
194
|
field_options = field_options.except(:as)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
195
|
+
|
|
196
|
+
# Render with label
|
|
197
|
+
div(class: "space-y-1.5") do
|
|
198
|
+
label(class: "text-sm font-medium text-[var(--pu-text)]") { filter_label }
|
|
199
|
+
nested.field(name, **field_options) do |f|
|
|
200
|
+
# Set placeholder for blank option text in selects
|
|
201
|
+
f.placeholder(input_options[:include_blank] || "All") if input_options[:include_blank]
|
|
202
|
+
render instance_exec(f, &tag_block)
|
|
203
|
+
end
|
|
168
204
|
end
|
|
169
205
|
end
|
|
206
|
+
|
|
207
|
+
def count_active_filters
|
|
208
|
+
count = 0
|
|
209
|
+
query_object.filter_definitions.each do |filter_name, _|
|
|
210
|
+
filter_params = helpers.params.dig(:q, filter_name)
|
|
211
|
+
next unless filter_params.is_a?(Hash) || filter_params.is_a?(ActionController::Parameters)
|
|
212
|
+
|
|
213
|
+
filter_params.each_value do |v|
|
|
214
|
+
count += 1 if v.present?
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
count
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def form_action
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
170
223
|
end
|
|
171
224
|
end
|
|
172
225
|
end
|