ruby_ui_admin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +165 -0
- data/app/assets/stylesheets/ruby_ui_admin/application.tailwind.css +111 -0
- data/app/components/ruby_ui_admin/ui/base.rb +26 -0
- data/app/components/ruby_ui_admin/ui/icon.rb +79 -0
- data/app/components/ruby_ui_admin/ui/native_select.rb +37 -0
- data/app/components/ruby_ui_admin/ui/native_select_group.rb +18 -0
- data/app/components/ruby_ui_admin/ui/native_select_icon.rb +42 -0
- data/app/components/ruby_ui_admin/ui/native_select_option.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination.rb +62 -0
- data/app/components/ruby_ui_admin/ui/pagination_content.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination_ellipsis.rb +45 -0
- data/app/components/ruby_ui_admin/ui/pagination_item.rb +32 -0
- data/app/components/ruby_ui_admin/ui/select.rb +29 -0
- data/app/components/ruby_ui_admin/ui/toast_action.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_cancel.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_close.rb +43 -0
- data/app/components/ruby_ui_admin/ui/toast_description.rb +21 -0
- data/app/components/ruby_ui_admin/ui/toast_icon.rb +69 -0
- data/app/components/ruby_ui_admin/ui/toast_item.rb +75 -0
- data/app/components/ruby_ui_admin/ui/toast_region.rb +147 -0
- data/app/components/ruby_ui_admin/ui/toast_title.rb +21 -0
- data/app/components/ruby_ui_admin/ui.rb +12 -0
- data/app/components/ruby_ui_admin/views/action.rb +37 -0
- data/app/components/ruby_ui_admin/views/action_form.rb +104 -0
- data/app/components/ruby_ui_admin/views/action_trigger.rb +148 -0
- data/app/components/ruby_ui_admin/views/actions_menu.rb +34 -0
- data/app/components/ruby_ui_admin/views/base.rb +301 -0
- data/app/components/ruby_ui_admin/views/card_component.rb +69 -0
- data/app/components/ruby_ui_admin/views/dashboard.rb +40 -0
- data/app/components/ruby_ui_admin/views/docs.rb +91 -0
- data/app/components/ruby_ui_admin/views/field_input.rb +156 -0
- data/app/components/ruby_ui_admin/views/field_value.rb +301 -0
- data/app/components/ruby_ui_admin/views/filter_bar.rb +111 -0
- data/app/components/ruby_ui_admin/views/form.rb +151 -0
- data/app/components/ruby_ui_admin/views/home.rb +35 -0
- data/app/components/ruby_ui_admin/views/index.rb +324 -0
- data/app/components/ruby_ui_admin/views/path_helpers.rb +36 -0
- data/app/components/ruby_ui_admin/views/rails_helpers.rb +15 -0
- data/app/components/ruby_ui_admin/views/show.rb +77 -0
- data/app/components/ruby_ui_admin/views/show_fields.rb +37 -0
- data/app/components/ruby_ui_admin/views/show_tab.rb +58 -0
- data/app/components/ruby_ui_admin/views/structure_renderer.rb +148 -0
- data/app/components/ruby_ui_admin/views/translation.rb +12 -0
- data/app/controllers/ruby_ui_admin/actions_controller.rb +162 -0
- data/app/controllers/ruby_ui_admin/application_controller.rb +81 -0
- data/app/controllers/ruby_ui_admin/dashboards_controller.rb +12 -0
- data/app/controllers/ruby_ui_admin/docs_controller.rb +166 -0
- data/app/controllers/ruby_ui_admin/home_controller.rb +18 -0
- data/app/controllers/ruby_ui_admin/resources_controller.rb +259 -0
- data/config/locales/ruby_ui_admin.en.yml +59 -0
- data/config/locales/ruby_ui_admin.pt-BR.yml +59 -0
- data/config/routes/dynamic_routes.rb +11 -0
- data/config/routes.rb +23 -0
- data/docs/README.md +53 -0
- data/docs/actions/overview.md +112 -0
- data/docs/authorization/action-policy.md +72 -0
- data/docs/authorization/adapters.md +101 -0
- data/docs/authorization/explicit-mode.md +45 -0
- data/docs/authorization/field-authorization.md +53 -0
- data/docs/authorization/policies.md +62 -0
- data/docs/authorization/scopes.md +48 -0
- data/docs/customization/controllers.md +62 -0
- data/docs/customization/ejecting.md +55 -0
- data/docs/customization/javascript.md +161 -0
- data/docs/customization/theming-rubyui.md +80 -0
- data/docs/dashboards/overview.md +65 -0
- data/docs/fields/overview.md +94 -0
- data/docs/filters/overview.md +73 -0
- data/docs/generators-and-tasks/overview.md +42 -0
- data/docs/getting-started/authentication.md +98 -0
- data/docs/getting-started/configuration.md +174 -0
- data/docs/getting-started/installation.md +201 -0
- data/docs/getting-started/internationalization.md +50 -0
- data/docs/getting-started/practical-guide.md +310 -0
- data/docs/resources/associations.md +66 -0
- data/docs/resources/index-customization.md +68 -0
- data/docs/resources/overview.md +50 -0
- data/docs/resources/scopes.md +54 -0
- data/docs/resources/tabs-panels.md +65 -0
- data/lib/generators/ruby_ui_admin/action/action_generator.rb +21 -0
- data/lib/generators/ruby_ui_admin/action/templates/action.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/assets/assets_generator.rb +71 -0
- data/lib/generators/ruby_ui_admin/card/card_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/card/templates/chart_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/metric_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/partial_card.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/components/components_generator.rb +43 -0
- data/lib/generators/ruby_ui_admin/controller/controller_generator.rb +30 -0
- data/lib/generators/ruby_ui_admin/controller/templates/controller.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/dashboard/dashboard_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/dashboard/templates/dashboard.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/eject/eject_generator.rb +55 -0
- data/lib/generators/ruby_ui_admin/filter/filter_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/filter/templates/boolean_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/filter/templates/select_filter.rb.tt +19 -0
- data/lib/generators/ruby_ui_admin/filter/templates/text_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/install/install_generator.rb +62 -0
- data/lib/generators/ruby_ui_admin/install/templates/initializer.rb.tt +41 -0
- data/lib/generators/ruby_ui_admin/locales/locales_generator.rb +22 -0
- data/lib/generators/ruby_ui_admin/policy/policy_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/policy/templates/policy.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/resource/resource_generator.rb +84 -0
- data/lib/generators/ruby_ui_admin/resource/templates/resource.rb.tt +15 -0
- data/lib/generators/ruby_ui_admin/scope/scope_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/scope/templates/scope.rb.tt +11 -0
- data/lib/ruby_ui_admin/authorization/action_policy_adapter.rb +69 -0
- data/lib/ruby_ui_admin/authorization/adapter.rb +80 -0
- data/lib/ruby_ui_admin/authorization/can_can_can_adapter.rb +70 -0
- data/lib/ruby_ui_admin/authorization/pundit_adapter.rb +86 -0
- data/lib/ruby_ui_admin/authorization.rb +40 -0
- data/lib/ruby_ui_admin/base_action.rb +211 -0
- data/lib/ruby_ui_admin/base_dashboard.rb +70 -0
- data/lib/ruby_ui_admin/base_policy.rb +38 -0
- data/lib/ruby_ui_admin/base_resource.rb +503 -0
- data/lib/ruby_ui_admin/cards/base_card.rb +59 -0
- data/lib/ruby_ui_admin/cards/chart_card.rb +16 -0
- data/lib/ruby_ui_admin/cards/metric_card.rb +22 -0
- data/lib/ruby_ui_admin/cards/partial_card.rb +14 -0
- data/lib/ruby_ui_admin/configuration.rb +177 -0
- data/lib/ruby_ui_admin/current.rb +20 -0
- data/lib/ruby_ui_admin/dashboard_manager.rb +52 -0
- data/lib/ruby_ui_admin/engine.rb +80 -0
- data/lib/ruby_ui_admin/execution_context.rb +36 -0
- data/lib/ruby_ui_admin/fields/association_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/badge_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/base_field.rb +208 -0
- data/lib/ruby_ui_admin/fields/belongs_to_field.rb +49 -0
- data/lib/ruby_ui_admin/fields/boolean_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/boolean_group_field.rb +54 -0
- data/lib/ruby_ui_admin/fields/code_field.rb +18 -0
- data/lib/ruby_ui_admin/fields/date_field.rb +34 -0
- data/lib/ruby_ui_admin/fields/date_time_field.rb +37 -0
- data/lib/ruby_ui_admin/fields/field_manager.rb +27 -0
- data/lib/ruby_ui_admin/fields/file_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/files_field.rb +62 -0
- data/lib/ruby_ui_admin/fields/has_and_belongs_to_many_field.rb +9 -0
- data/lib/ruby_ui_admin/fields/has_many_field.rb +24 -0
- data/lib/ruby_ui_admin/fields/has_one_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/hidden_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/id_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/key_value_field.rb +41 -0
- data/lib/ruby_ui_admin/fields/number_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/password_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/record_link_field.rb +45 -0
- data/lib/ruby_ui_admin/fields/select_field.rb +71 -0
- data/lib/ruby_ui_admin/fields/status_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/text_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/textarea_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/url_field.rb +21 -0
- data/lib/ruby_ui_admin/filters/base_filter.rb +81 -0
- data/lib/ruby_ui_admin/filters/boolean_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/multiple_select_filter.rb +11 -0
- data/lib/ruby_ui_admin/filters/select_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/text_filter.rb +9 -0
- data/lib/ruby_ui_admin/menu/builder.rb +94 -0
- data/lib/ruby_ui_admin/resource_manager.rb +82 -0
- data/lib/ruby_ui_admin/scopes/base_scope.rb +107 -0
- data/lib/ruby_ui_admin/services/authorization_service.rb +38 -0
- data/lib/ruby_ui_admin/structure.rb +40 -0
- data/lib/ruby_ui_admin/tailwind_source.rb +65 -0
- data/lib/ruby_ui_admin/version.rb +5 -0
- data/lib/ruby_ui_admin/view.rb +34 -0
- data/lib/ruby_ui_admin.rb +110 -0
- data/lib/tasks/ruby_ui_admin.rake +71 -0
- data/public/ruby-ui-admin-assets/application.css +2 -0
- data/public/ruby-ui-admin-assets/controllers/index.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/rua--bulk-select_controller.js +19 -0
- data/public/ruby-ui-admin-assets/controllers/rua--confirm_controller.js +51 -0
- data/public/ruby-ui-admin-assets/controllers/rua--dialog_controller.js +55 -0
- data/public/ruby-ui-admin-assets/controllers/rua--row-link_controller.js +16 -0
- data/public/ruby-ui-admin-assets/controllers/rua--tabs_controller.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--combobox_controller.js +197 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_content_controller.js +7 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_controller.js +15 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sidebar_controller.js +67 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toast_controller.js +151 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toaster_controller.js +314 -0
- data/public/ruby-ui-admin-assets/vendor/stimulus.js +2567 -0
- data/public/ruby-ui-admin-assets/vendor/turbo.js +7178 -0
- metadata +379 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Views
|
|
5
|
+
# Renders a form input for a field, dispatching on the field type.
|
|
6
|
+
class FieldInput < Phlex::HTML
|
|
7
|
+
include RubyUIAdmin::UI
|
|
8
|
+
include Translation
|
|
9
|
+
|
|
10
|
+
def initialize(field:, record:, name_prefix: "record")
|
|
11
|
+
@field = field
|
|
12
|
+
@record = record
|
|
13
|
+
@name_prefix = name_prefix
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def view_template
|
|
17
|
+
case @field.type
|
|
18
|
+
when :boolean then render_boolean
|
|
19
|
+
when :number then render_number
|
|
20
|
+
when :date then render_date
|
|
21
|
+
when :date_time then render_date_time
|
|
22
|
+
when :textarea then render_textarea
|
|
23
|
+
when :code then render_code
|
|
24
|
+
when :key_value then render_key_value
|
|
25
|
+
when :select then render_select
|
|
26
|
+
when :url then render_url
|
|
27
|
+
when :hidden then render_hidden
|
|
28
|
+
when :password then render_password
|
|
29
|
+
when :boolean_group then render_boolean_group
|
|
30
|
+
when :belongs_to then render_belongs_to
|
|
31
|
+
when :file then render_file
|
|
32
|
+
when :files then render_files
|
|
33
|
+
else render_text
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def input_name
|
|
40
|
+
"#{@name_prefix}[#{@field.permitted_param}]"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def render_text
|
|
44
|
+
render RubyUI::Input.new(type: :text, name: input_name, value: @field.value(@record), placeholder: @field.placeholder)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def render_number
|
|
48
|
+
render RubyUI::Input.new(type: :number, name: input_name, value: @field.value(@record))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def render_url
|
|
52
|
+
render RubyUI::Input.new(type: :url, name: input_name, value: @field.value(@record), placeholder: @field.placeholder)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_password
|
|
56
|
+
render RubyUI::Input.new(type: :password, name: input_name, autocomplete: "new-password")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def render_hidden
|
|
60
|
+
input(type: :hidden, name: input_name, value: @field.value(@record))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render_date
|
|
64
|
+
value = @field.value(@record)
|
|
65
|
+
formatted = value.respond_to?(:strftime) ? value.strftime("%Y-%m-%d") : value
|
|
66
|
+
render RubyUI::Input.new(type: :date, name: input_name, value: formatted)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def render_date_time
|
|
70
|
+
value = @field.value(@record)
|
|
71
|
+
formatted = value.respond_to?(:strftime) ? value.strftime("%Y-%m-%dT%H:%M") : value
|
|
72
|
+
render RubyUI::Input.new(type: "datetime-local", name: input_name, value: formatted)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def render_textarea
|
|
76
|
+
render RubyUI::Textarea.new(name: input_name, rows: @field.rows) { @field.value(@record).to_s }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def render_code
|
|
80
|
+
render RubyUI::Textarea.new(name: input_name, rows: 8, class: "font-mono") { @field.value(@record).to_s }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def render_key_value
|
|
84
|
+
render RubyUI::Textarea.new(name: input_name, rows: 6, class: "font-mono") { @field.value_as_json(@record) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def render_boolean
|
|
88
|
+
input(type: :hidden, name: input_name, value: "0")
|
|
89
|
+
render RubyUI::Checkbox.new(name: input_name, value: "1", checked: !!@field.value(@record))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def render_boolean_group
|
|
93
|
+
div(class: "flex flex-col gap-2") do
|
|
94
|
+
@field.group_options.each do |key, label_text|
|
|
95
|
+
option_name = "#{input_name}[#{key}]"
|
|
96
|
+
label(class: "flex items-center gap-2 text-sm") do
|
|
97
|
+
# Paired hidden "0" so unchecked boxes are submitted (and persisted as false).
|
|
98
|
+
input(type: :hidden, name: option_name, value: "0")
|
|
99
|
+
render RubyUI::Checkbox.new(name: option_name, value: "1", checked: @field.checked?(@record, key))
|
|
100
|
+
plain label_text
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render_select
|
|
107
|
+
render RubyUIAdmin::UI::Select.new(
|
|
108
|
+
name: input_name,
|
|
109
|
+
options: @field.select_options(@record),
|
|
110
|
+
selected: @field.value(@record),
|
|
111
|
+
include_blank: @field.include_blank?
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_belongs_to
|
|
116
|
+
render RubyUIAdmin::UI::Select.new(
|
|
117
|
+
name: input_name,
|
|
118
|
+
options: @field.options_for_select,
|
|
119
|
+
selected: @field.foreign_key_value(@record),
|
|
120
|
+
include_blank: true
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def render_file
|
|
125
|
+
div(class: "space-y-2") do
|
|
126
|
+
if @field.respond_to?(:attached?) && @field.attached?(@record)
|
|
127
|
+
blob = @field.attachment(@record).blob
|
|
128
|
+
div(class: "flex items-center gap-2 text-sm text-muted-foreground") do
|
|
129
|
+
render RubyUIAdmin::UI::Icon.new(:file, class: "size-4 shrink-0")
|
|
130
|
+
span { blob.filename.to_s }
|
|
131
|
+
end
|
|
132
|
+
label(class: "flex items-center gap-2 text-sm text-muted-foreground") do
|
|
133
|
+
render RubyUI::Checkbox.new(name: "#{@name_prefix}[#{@field.remove_param}]", value: "1")
|
|
134
|
+
plain rua_t("fields.file.remove")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
render RubyUI::Input.new(type: :file, name: input_name, accept: @field.accept)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def render_files
|
|
142
|
+
div(class: "space-y-2") do
|
|
143
|
+
@field.attachments(@record).each do |att|
|
|
144
|
+
label(class: "flex items-center gap-2 text-sm text-muted-foreground") do
|
|
145
|
+
render RubyUI::Checkbox.new(name: "#{@name_prefix}[#{@field.remove_ids_param}][]", value: att.id.to_s)
|
|
146
|
+
render RubyUIAdmin::UI::Icon.new(:file, class: "size-4 shrink-0")
|
|
147
|
+
span { att.blob.filename.to_s }
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
# New uploads are appended (not replacing the existing ones); check a box above to remove.
|
|
151
|
+
render RubyUI::Input.new(type: :file, name: "#{input_name}[]", accept: @field.accept, multiple: true)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Views
|
|
5
|
+
# Renders a field's value for index/show views, dispatching on the field type.
|
|
6
|
+
class FieldValue < Phlex::HTML
|
|
7
|
+
include RubyUIAdmin::UI
|
|
8
|
+
include RailsHelpers
|
|
9
|
+
include PathHelpers
|
|
10
|
+
include Translation
|
|
11
|
+
|
|
12
|
+
def initialize(field:, record:, link: nil)
|
|
13
|
+
@field = field
|
|
14
|
+
@record = record
|
|
15
|
+
@link = link
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def view_template
|
|
19
|
+
case @field.type
|
|
20
|
+
when :boolean then render_boolean
|
|
21
|
+
when :belongs_to then render_belongs_to
|
|
22
|
+
when :has_one then render_has_one
|
|
23
|
+
when :has_many, :has_and_belongs_to_many then render_has_many
|
|
24
|
+
when :record_link then render_record_link
|
|
25
|
+
when :url then render_url
|
|
26
|
+
when :badge then render_badge
|
|
27
|
+
when :status then render_status
|
|
28
|
+
when :code then render_code
|
|
29
|
+
when :key_value then render_key_value
|
|
30
|
+
when :boolean_group then render_boolean_group
|
|
31
|
+
when :file then render_file
|
|
32
|
+
when :files then render_files
|
|
33
|
+
else render_text
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def render_text
|
|
40
|
+
value = @field.formatted_value(@record, view_context: view_context)
|
|
41
|
+
return plain("—") if value.nil?
|
|
42
|
+
|
|
43
|
+
# Computed blocks may return HTML (e.g. `link_to` -> html_safe); `as_html: true` opts a
|
|
44
|
+
# plain string into raw rendering. Everything else is escaped.
|
|
45
|
+
if (value.respond_to?(:html_safe?) && value.html_safe?) || (@field.respond_to?(:as_html?) && @field.as_html?)
|
|
46
|
+
# User-rendered HTML (e.g. `link_to` in a field block) carries no admin styling.
|
|
47
|
+
# The `contents` wrapper is layout-neutral and scopes `.rua-rich a` so raw links
|
|
48
|
+
# underline on hover, matching the framework's own links.
|
|
49
|
+
span(class: "rua-rich contents") { raw(safe(value.to_s)) }
|
|
50
|
+
elsif @link && @field.link_to_record?
|
|
51
|
+
render RubyUI::InlineLink.new(href: @link) { value.to_s }
|
|
52
|
+
else
|
|
53
|
+
plain value.to_s
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render_boolean
|
|
58
|
+
if @field.value(@record)
|
|
59
|
+
render RubyUI::Badge.new(variant: :success) { rua_t("booleans.true") }
|
|
60
|
+
else
|
|
61
|
+
render RubyUI::Badge.new(variant: :gray) { rua_t("booleans.false") }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def render_belongs_to
|
|
66
|
+
associated = @field.value(@record)
|
|
67
|
+
return plain("—") if associated.nil?
|
|
68
|
+
|
|
69
|
+
render_record_link_to(associated, @field.display_label(associated))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def render_has_one
|
|
73
|
+
associated = @field.associated_record(@record)
|
|
74
|
+
return plain("—") if associated.nil?
|
|
75
|
+
|
|
76
|
+
render_record_link_to(associated, @field.display_label(associated))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def render_has_many
|
|
80
|
+
count = @field.count(@record)
|
|
81
|
+
return plain("—") if count.zero?
|
|
82
|
+
|
|
83
|
+
shown = @field.associated_records(@record, limit: 10).to_a
|
|
84
|
+
# Columns come from the record's own resource (the fields must match the record's model);
|
|
85
|
+
# the row link routes through `use_resource:` when given, else the record's own resource.
|
|
86
|
+
column_resource = record_resource_class(shown.first)
|
|
87
|
+
link_resource = (@field.respond_to?(:use_resource) && @field.use_resource) || column_resource
|
|
88
|
+
columns = association_columns(column_resource)
|
|
89
|
+
|
|
90
|
+
div(class: "space-y-2") do
|
|
91
|
+
span(class: "text-xs font-medium text-muted-foreground") { rua_t("associations.count", count: count) }
|
|
92
|
+
render_association_table(shown, link_resource, columns)
|
|
93
|
+
|
|
94
|
+
if count > shown.size
|
|
95
|
+
p(class: "text-xs text-muted-foreground") { rua_t("associations.more", count: count - shown.size) }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Renders associated records as a RubyUI Table. When the associated model has a registered
|
|
101
|
+
# resource, its index fields become the columns (Avo-style) and each row links to the
|
|
102
|
+
# record's show page. Otherwise it falls back to a single column of record links.
|
|
103
|
+
def render_association_table(records, resource_class, columns)
|
|
104
|
+
render RubyUI::Table.new do
|
|
105
|
+
if columns
|
|
106
|
+
render RubyUI::TableHeader.new do
|
|
107
|
+
render RubyUI::TableRow.new do
|
|
108
|
+
columns.each { |field| render RubyUI::TableHead.new { field.name } }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
render RubyUI::TableBody.new do
|
|
114
|
+
records.each do |rec|
|
|
115
|
+
path = association_show_path(resource_class, rec)
|
|
116
|
+
attrs = if path
|
|
117
|
+
{class: "cursor-pointer", data: {controller: "rua--row-link", action: "click->rua--row-link#navigate", rua__row_link_url_value: path}}
|
|
118
|
+
else
|
|
119
|
+
{}
|
|
120
|
+
end
|
|
121
|
+
render RubyUI::TableRow.new(**attrs) do
|
|
122
|
+
if columns
|
|
123
|
+
columns.each_with_index do |field, index|
|
|
124
|
+
render RubyUI::TableCell.new(class: ("font-medium" if index.zero?)) do
|
|
125
|
+
link = field.link_to_record? ? path : nil
|
|
126
|
+
render RubyUIAdmin::Views::FieldValue.new(field: field, record: rec, link: link)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
else
|
|
130
|
+
render RubyUI::TableCell.new { render_record_link_to(rec, @field.display_label(rec)) }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The resource registered for a record's own model (used to build the table columns), or nil.
|
|
139
|
+
def record_resource_class(record)
|
|
140
|
+
return nil if record.nil?
|
|
141
|
+
|
|
142
|
+
RubyUIAdmin.resource_manager.find_for_model(record.class)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The columns for the association table. By default the associated resource's index fields;
|
|
146
|
+
# an explicit `fields:` option on the association field picks a subset (in that order) by
|
|
147
|
+
# field id — e.g. `field :suppliers, as: :has_many, fields: %i[name identifier state]`.
|
|
148
|
+
# Returns nil (single-column fallback) when there's no resource or the fields can't be built.
|
|
149
|
+
def association_columns(resource_class)
|
|
150
|
+
return nil unless resource_class
|
|
151
|
+
|
|
152
|
+
requested = Array(@field.options[:fields]).map(&:to_sym)
|
|
153
|
+
columns =
|
|
154
|
+
if requested.any?
|
|
155
|
+
by_id = resource_class.new.get_fields(view: nil).index_by(&:id)
|
|
156
|
+
requested.filter_map { |fid| by_id[fid] }
|
|
157
|
+
else
|
|
158
|
+
resource_class.new.get_fields(view: :index)
|
|
159
|
+
end
|
|
160
|
+
columns.presence
|
|
161
|
+
rescue StandardError
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def association_show_path(resource_class, record)
|
|
166
|
+
return nil unless resource_class
|
|
167
|
+
|
|
168
|
+
ruby_ui_admin.public_send("resources_#{resource_class.singular_route_key}_path", record)
|
|
169
|
+
rescue StandardError
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def render_record_link
|
|
174
|
+
linked = @field.linked_record(@record)
|
|
175
|
+
return plain("—") if linked.nil?
|
|
176
|
+
|
|
177
|
+
target = @field.target_resource(linked)
|
|
178
|
+
label = @field.display_label(linked)
|
|
179
|
+
path = target && ruby_ui_admin.public_send("resources_#{target.singular_route_key}_path", linked)
|
|
180
|
+
|
|
181
|
+
if path
|
|
182
|
+
render RubyUI::InlineLink.new(href: path) { label.to_s }
|
|
183
|
+
else
|
|
184
|
+
plain label.to_s
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def render_url
|
|
189
|
+
url = @field.value(@record)
|
|
190
|
+
return plain("—") if url.blank?
|
|
191
|
+
|
|
192
|
+
render RubyUI::InlineLink.new(href: url, target: @field.target) { @field.link_text(@record).to_s }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def render_badge
|
|
196
|
+
value = @field.value(@record)
|
|
197
|
+
return plain("—") if value.blank?
|
|
198
|
+
|
|
199
|
+
render RubyUI::Badge.new(variant: @field.variant_for(@record)) { value.to_s }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def render_status
|
|
203
|
+
value = @field.value(@record)
|
|
204
|
+
return plain("—") if value.blank?
|
|
205
|
+
|
|
206
|
+
render RubyUI::Badge.new(variant: @field.variant_for(@record)) { value.to_s }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def render_code
|
|
210
|
+
value = @field.value(@record)
|
|
211
|
+
lang = @field.respond_to?(:language) ? @field.language : nil
|
|
212
|
+
pre(class: "rounded-md bg-muted p-3 overflow-x-auto text-xs") do
|
|
213
|
+
# `language-<lang>` + data-language are the conventions syntax highlighters hook into.
|
|
214
|
+
code(class: ("language-#{lang}" if lang), data: {language: lang}) { value.to_s }
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def render_key_value
|
|
219
|
+
value = @field.value(@record)
|
|
220
|
+
return plain("—") if value.blank?
|
|
221
|
+
|
|
222
|
+
dl(class: "text-sm") do
|
|
223
|
+
value.each do |key, val|
|
|
224
|
+
div(class: "flex gap-2") do
|
|
225
|
+
dt(class: "font-medium text-muted-foreground") { "#{key}:" }
|
|
226
|
+
dd { val.to_s }
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def render_boolean_group
|
|
233
|
+
div(class: "flex flex-wrap gap-1") do
|
|
234
|
+
@field.group_options.each do |key, label_text|
|
|
235
|
+
on = @field.checked?(@record, key)
|
|
236
|
+
render RubyUI::Badge.new(variant: on ? :success : :gray) { label_text.to_s }
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def render_file
|
|
242
|
+
return plain("—") unless @field.attached?(@record)
|
|
243
|
+
|
|
244
|
+
render_attachment(@field.attachment(@record))
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def render_files
|
|
248
|
+
attachments = @field.attachments(@record)
|
|
249
|
+
return plain("—") if attachments.blank?
|
|
250
|
+
|
|
251
|
+
div(class: "flex flex-col gap-2") do
|
|
252
|
+
attachments.each { |attachment| render_attachment(attachment) }
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# An attached file: images render as a thumbnail linking to the file; everything else is a
|
|
257
|
+
# download link with the filename. Both resolve the URL through the host's ActiveStorage routes.
|
|
258
|
+
def render_attachment(attachment)
|
|
259
|
+
blob = attachment.respond_to?(:blob) ? attachment.blob : attachment
|
|
260
|
+
return plain("—") unless blob
|
|
261
|
+
|
|
262
|
+
if blob.content_type.to_s.start_with?("image/")
|
|
263
|
+
a(href: attachment_url(blob), target: "_blank", rel: "noopener", class: "inline-block") do
|
|
264
|
+
img(src: attachment_url(blob), alt: blob.filename.to_s, **image_preview_attrs)
|
|
265
|
+
end
|
|
266
|
+
else
|
|
267
|
+
a(href: attachment_url(blob, disposition: "attachment"), class: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline") do
|
|
268
|
+
render RubyUIAdmin::UI::Icon.new(:download, class: "size-4 shrink-0")
|
|
269
|
+
plain blob.filename.to_s
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Thumbnail sizing. With `preview_size:` on the field, the image is capped at that W×H (px)
|
|
275
|
+
# via an inline max-width/max-height (so large images scale down, preserving aspect). Inline
|
|
276
|
+
# style — not a Tailwind class — because the size is dynamic and wouldn't be in the CSS build.
|
|
277
|
+
def image_preview_attrs
|
|
278
|
+
size = @field.respond_to?(:preview_size) ? @field.preview_size : nil
|
|
279
|
+
return {class: "h-16 w-16 rounded-md border border-border object-cover"} unless size
|
|
280
|
+
|
|
281
|
+
w, h = Array(size).first(2)
|
|
282
|
+
h ||= w
|
|
283
|
+
{class: "rounded-md border border-border object-contain", style: "max-width: #{w.to_i}px; max-height: #{h.to_i}px"}
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Links to a record's show page through its registered resource, if any.
|
|
287
|
+
# A field's `use_resource:` overrides the default model→resource lookup.
|
|
288
|
+
def render_record_link_to(record, label)
|
|
289
|
+
rc = (@field.respond_to?(:use_resource) && @field.use_resource) ||
|
|
290
|
+
RubyUIAdmin.resource_manager.find_for_model(record.class)
|
|
291
|
+
|
|
292
|
+
if rc
|
|
293
|
+
path = ruby_ui_admin.public_send("resources_#{rc.singular_route_key}_path", record)
|
|
294
|
+
render RubyUI::InlineLink.new(href: path) { label.to_s }
|
|
295
|
+
else
|
|
296
|
+
plain label.to_s
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Views
|
|
5
|
+
# Renders a GET form with one control per filter, above the index table.
|
|
6
|
+
class FilterBar < Phlex::HTML
|
|
7
|
+
include RubyUIAdmin::UI
|
|
8
|
+
include Translation
|
|
9
|
+
|
|
10
|
+
def initialize(filters:, values:, action_path:, scope_param: nil)
|
|
11
|
+
@filters = filters
|
|
12
|
+
@values = values || {}
|
|
13
|
+
@action_path = action_path
|
|
14
|
+
@scope_param = scope_param
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def view_template
|
|
18
|
+
return if @filters.empty?
|
|
19
|
+
|
|
20
|
+
render RubyUI::Card.new(class: "mb-4") do
|
|
21
|
+
render RubyUI::CardContent.new(class: "pt-6") do
|
|
22
|
+
form(method: "get", action: @action_path, class: "flex flex-wrap items-end gap-4") do
|
|
23
|
+
# Preserve the active scope when applying filters.
|
|
24
|
+
input(type: "hidden", name: "scope", value: @scope_param) if @scope_param.present?
|
|
25
|
+
|
|
26
|
+
@filters.each { |filter| render_filter(filter) }
|
|
27
|
+
render_buttons
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def render_filter(filter)
|
|
36
|
+
key = filter.param_key
|
|
37
|
+
|
|
38
|
+
div(class: "space-y-1 min-w-48") do
|
|
39
|
+
label(class: "text-xs font-medium text-muted-foreground") { filter.name }
|
|
40
|
+
|
|
41
|
+
case filter.type
|
|
42
|
+
when :select
|
|
43
|
+
render RubyUIAdmin::UI::Select.new(name: "filters[#{key}]", options: option_pairs(filter), selected: @values[key], include_blank: true)
|
|
44
|
+
when :multiple_select
|
|
45
|
+
render_checkboxes(filter, key, multiple: true)
|
|
46
|
+
when :boolean
|
|
47
|
+
if filter.options.present?
|
|
48
|
+
render_checkboxes(filter, key, multiple: false)
|
|
49
|
+
else
|
|
50
|
+
render RubyUIAdmin::UI::Select.new(name: "filters[#{key}]", options: [["Yes", "true"], ["No", "false"]], selected: @values[key], include_blank: true)
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
render RubyUI::Input.new(type: :text, name: "filters[#{key}]", value: @values[key])
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Renders the option set with RubyUI's Combobox (searchable popover with checkboxes),
|
|
59
|
+
# so filters with many options stay compact and match the rest of the RubyUI UI.
|
|
60
|
+
#
|
|
61
|
+
# `multiple: true` -> array value, name `filters[key][]`.
|
|
62
|
+
# `multiple: false` -> hash value (boolean filter), name `filters[key][optkey]` with a
|
|
63
|
+
# paired hidden "false" so unchecked boxes are still submitted.
|
|
64
|
+
def render_checkboxes(filter, key, multiple:)
|
|
65
|
+
selected = @values[key]
|
|
66
|
+
|
|
67
|
+
render RubyUI::Combobox.new(term: rua_t("index.selected_suffix")) do
|
|
68
|
+
render RubyUI::ComboboxTrigger.new(placeholder: rua_t("index.all"))
|
|
69
|
+
render RubyUI::ComboboxPopover.new do
|
|
70
|
+
render RubyUI::ComboboxSearchInput.new(placeholder: rua_t("index.search"))
|
|
71
|
+
render RubyUI::ComboboxList.new do
|
|
72
|
+
render(RubyUI::ComboboxEmptyState.new) { rua_t("index.no_results") }
|
|
73
|
+
option_pairs(filter).each do |(label_text, value)|
|
|
74
|
+
input_name = multiple ? "filters[#{key}][]" : "filters[#{key}][#{value}]"
|
|
75
|
+
render RubyUI::ComboboxItem.new do
|
|
76
|
+
input(type: :hidden, name: input_name, value: "false") unless multiple
|
|
77
|
+
render RubyUI::ComboboxCheckbox.new(
|
|
78
|
+
name: input_name,
|
|
79
|
+
value: multiple ? value.to_s : "true",
|
|
80
|
+
checked: checkbox_checked?(selected, value, multiple)
|
|
81
|
+
)
|
|
82
|
+
span { label_text }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def checkbox_checked?(selected, value, multiple)
|
|
91
|
+
if multiple
|
|
92
|
+
Array(selected).map(&:to_s).include?(value.to_s)
|
|
93
|
+
else
|
|
94
|
+
selected.is_a?(Hash) && selected[value.to_s].to_s == "true"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def option_pairs(filter)
|
|
99
|
+
options = filter.options
|
|
100
|
+
options.is_a?(Hash) ? options.map { |value, label| [label, value] } : Array(options)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def render_buttons
|
|
104
|
+
div(class: "flex items-center gap-2") do
|
|
105
|
+
render(RubyUI::Button.new(type: :submit, variant: :primary)) { rua_t("index.filter") }
|
|
106
|
+
a(href: @action_path, class: "text-sm text-muted-foreground hover:underline") { rua_t("index.clear") }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|