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,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Views
|
|
5
|
+
class Form < Base
|
|
6
|
+
include StructureRenderer
|
|
7
|
+
|
|
8
|
+
def initialize(resource:, record:, view:)
|
|
9
|
+
@resource = resource
|
|
10
|
+
@record = record
|
|
11
|
+
@view = view.to_sym
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def page_title
|
|
15
|
+
"#{title_prefix} · #{RubyUIAdmin.configuration.app_name}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def content
|
|
19
|
+
render_header
|
|
20
|
+
render_errors_summary
|
|
21
|
+
|
|
22
|
+
form(action: form_action, method: "post", enctype: form_enctype, class: "space-y-6") do
|
|
23
|
+
hidden_method_field
|
|
24
|
+
csrf_field
|
|
25
|
+
|
|
26
|
+
render_structure(@resource.field_structure(view: @view))
|
|
27
|
+
|
|
28
|
+
render_actions
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# File-upload fields require a multipart form.
|
|
35
|
+
def form_enctype
|
|
36
|
+
@resource.get_fields(view: @view).any? { |f| %i[file files].include?(f.type) } ? "multipart/form-data" : nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def new_record?
|
|
40
|
+
@view == :new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def title_prefix
|
|
44
|
+
new_record? ? "New #{@resource.model_class.model_name.human}" : "Edit #{@resource.record_title(@record)}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def form_action
|
|
48
|
+
if new_record?
|
|
49
|
+
resource_index_path(@resource.class)
|
|
50
|
+
else
|
|
51
|
+
resource_show_path(@resource.class, @record)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_header
|
|
56
|
+
div(class: "mb-6") do
|
|
57
|
+
a(href: resource_index_path(@resource.class), class: "text-sm text-muted-foreground hover:underline") { "← #{@resource.navigation_label}" }
|
|
58
|
+
h1(class: "text-2xl font-semibold tracking-tight mt-1") { title_prefix }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_errors_summary
|
|
63
|
+
return unless @record.errors.any?
|
|
64
|
+
|
|
65
|
+
div(class: "mb-4 rounded-md border border-destructive/30 bg-destructive/5 p-4") do
|
|
66
|
+
p(class: "text-sm font-medium text-destructive mb-1") { rua_t("form.errors") }
|
|
67
|
+
ul(class: "list-disc list-inside text-sm text-destructive") do
|
|
68
|
+
@record.errors.full_messages.each { |message| li { message } }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def render_field(field)
|
|
74
|
+
# Hidden fields carry a value with no visible label/row.
|
|
75
|
+
if field.type == :hidden
|
|
76
|
+
render RubyUIAdmin::Views::FieldInput.new(field: field, record: @record)
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A lone checkbox reads as "[ ] Label" — render it inline beside the label
|
|
81
|
+
# instead of stacking (which would leave the checkbox glued to the label text).
|
|
82
|
+
return render_boolean_field(field) if field.type == :boolean && !field.readonly?
|
|
83
|
+
|
|
84
|
+
div(class: "space-y-1.5 w-full md:w-8/12") do
|
|
85
|
+
label(class: "text-sm font-medium leading-none", title: field.description) do
|
|
86
|
+
plain field.name
|
|
87
|
+
span(class: "text-destructive ml-0.5") { "*" } if field.required?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if field.readonly?
|
|
91
|
+
div(class: "text-sm text-muted-foreground") do
|
|
92
|
+
render RubyUIAdmin::Views::FieldValue.new(field: field, record: @record)
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
render RubyUIAdmin::Views::FieldInput.new(field: field, record: @record)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if (help = field.help)
|
|
99
|
+
p(class: "text-xs text-muted-foreground") { help }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
field_errors(field)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render_boolean_field(field)
|
|
107
|
+
div(class: "space-y-1.5 w-full md:w-8/12") do
|
|
108
|
+
label(class: "flex items-center gap-2 text-sm font-medium leading-none", title: field.description) do
|
|
109
|
+
render RubyUIAdmin::Views::FieldInput.new(field: field, record: @record)
|
|
110
|
+
plain field.name
|
|
111
|
+
span(class: "text-destructive ml-0.5") { "*" } if field.required?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if (help = field.help)
|
|
115
|
+
p(class: "text-xs text-muted-foreground") { help }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
field_errors(field)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def field_errors(field)
|
|
123
|
+
messages = @record.errors[field.database_id]
|
|
124
|
+
return if messages.blank?
|
|
125
|
+
|
|
126
|
+
p(class: "text-xs text-destructive") { messages.join(", ") }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def render_actions
|
|
130
|
+
div(class: "flex items-center gap-3 pt-2") do
|
|
131
|
+
button(
|
|
132
|
+
type: "submit",
|
|
133
|
+
class: "inline-flex items-center h-9 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium shadow hover:bg-primary/90"
|
|
134
|
+
) { new_record? ? rua_t("actions.create") : rua_t("actions.update") }
|
|
135
|
+
|
|
136
|
+
a(href: resource_index_path(@resource.class), class: "text-sm text-muted-foreground hover:underline") { rua_t("actions.cancel") }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def hidden_method_field
|
|
141
|
+
return if new_record?
|
|
142
|
+
|
|
143
|
+
input(type: "hidden", name: "_method", value: "patch")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def csrf_field
|
|
147
|
+
input(type: "hidden", name: "authenticity_token", value: form_authenticity_token)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Views
|
|
5
|
+
class Home < Base
|
|
6
|
+
def initialize(resources:)
|
|
7
|
+
@resources = resources
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def content
|
|
11
|
+
h1(class: "text-2xl font-semibold tracking-tight mb-6") { RubyUIAdmin.configuration.app_name }
|
|
12
|
+
|
|
13
|
+
if @resources.empty?
|
|
14
|
+
p(class: "text-muted-foreground") { "No resources defined yet. Create one in app/ruby_ui_admin/resources." }
|
|
15
|
+
else
|
|
16
|
+
div(class: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3") do
|
|
17
|
+
@resources.each { |rc| render_card(rc) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def render_card(rc)
|
|
25
|
+
a(href: resource_index_path(rc), class: "block") do
|
|
26
|
+
render RubyUI::Card.new(class: "hover:shadow-md transition-shadow") do
|
|
27
|
+
render RubyUI::CardHeader.new do
|
|
28
|
+
render RubyUI::CardTitle.new { rc.navigation_label }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Views
|
|
5
|
+
class Index < Base
|
|
6
|
+
include ActionsMenu
|
|
7
|
+
|
|
8
|
+
def initialize(resource:, records:, pagy:, filters: [], filter_values: {}, scopes: [], current_scope_key: nil, scope_param: nil, remove_scope_all: false, query_params: {}, sort_by: nil, sort_direction: nil)
|
|
9
|
+
@resource = resource
|
|
10
|
+
@records = records
|
|
11
|
+
@pagy = pagy
|
|
12
|
+
@filters = filters
|
|
13
|
+
@filter_values = filter_values
|
|
14
|
+
@scopes = scopes
|
|
15
|
+
@current_scope_key = current_scope_key
|
|
16
|
+
@scope_param = scope_param
|
|
17
|
+
@remove_scope_all = remove_scope_all
|
|
18
|
+
@query_params = query_params
|
|
19
|
+
@sort_by = sort_by
|
|
20
|
+
@sort_direction = sort_direction
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def page_title
|
|
24
|
+
"#{@resource.navigation_label} · #{RubyUIAdmin.configuration.app_name}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
BULK_FORM_ID = "rua-bulk-form"
|
|
28
|
+
|
|
29
|
+
def content
|
|
30
|
+
render_header
|
|
31
|
+
render_scope_tabs
|
|
32
|
+
render_filter_bar
|
|
33
|
+
render_bulk_toolbar
|
|
34
|
+
# The `rua--bulk-select` controller (select-all + checked-ids) scopes to this table card.
|
|
35
|
+
card_attrs = {class: "p-4"}
|
|
36
|
+
card_attrs[:data] = {controller: "rua--bulk-select"} if bulk?
|
|
37
|
+
render RubyUI::Card.new(**card_attrs) do
|
|
38
|
+
render_table
|
|
39
|
+
end
|
|
40
|
+
render_pagination
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def policy_class
|
|
46
|
+
@resource.class.authorization_policy
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def fields
|
|
50
|
+
@fields ||= @resource.get_fields(view: :index)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Non-standalone actions become bulk actions (run against selected rows).
|
|
54
|
+
def bulk_actions
|
|
55
|
+
return [] unless authorized_to?(:act_on, @resource.model_class, policy_class: policy_class)
|
|
56
|
+
|
|
57
|
+
@bulk_actions ||= @resource.actions_for(view: :index).reject(&:standalone?)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def bulk?
|
|
61
|
+
@resource.class.record_selector && bulk_actions.any?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def render_header
|
|
65
|
+
div(class: "flex items-center justify-between mb-6") do
|
|
66
|
+
div do
|
|
67
|
+
h1(class: "text-2xl font-semibold tracking-tight") { @resource.navigation_label }
|
|
68
|
+
if (description = @resource.class.description)
|
|
69
|
+
p(class: "text-sm text-muted-foreground mt-1") { description }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
div(class: "flex items-center gap-2") do
|
|
74
|
+
render_index_controls
|
|
75
|
+
render_standalone_actions
|
|
76
|
+
|
|
77
|
+
if authorized_to?(:create, @resource.model_class, policy_class: policy_class)
|
|
78
|
+
render(RubyUI::Link.new(href: resource_new_path(@resource.class), variant: :primary)) do
|
|
79
|
+
rua_t("actions.new", model: @resource.model_class.model_name.human)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Renders the resource's `self.index_controls` block (custom header buttons/links).
|
|
87
|
+
# The block is evaluated in this component, so it can use Phlex (`a`, `button`, …),
|
|
88
|
+
# the `control_link` helper, route helpers (`ruby_ui_admin.*`) and `@resource`.
|
|
89
|
+
def render_index_controls
|
|
90
|
+
controls = @resource.class.index_controls
|
|
91
|
+
return if controls.nil?
|
|
92
|
+
|
|
93
|
+
instance_exec(&controls)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# `actions_list` is accepted inside `index_controls` for compatibility, but standalone
|
|
97
|
+
# actions are already rendered in the header (`render_standalone_actions`), so it is a
|
|
98
|
+
# no-op here.
|
|
99
|
+
def actions_list = nil
|
|
100
|
+
|
|
101
|
+
# Convenience helper for `index_controls`: a button-styled link (RubyUI Button as `<a>`).
|
|
102
|
+
def control_link(label, href, variant: :outline)
|
|
103
|
+
render(RubyUI::Link.new(href: href, variant: (variant == :primary) ? :primary : :outline)) { label }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Link helpers for use inside `index_controls`/`row_controls` blocks.
|
|
107
|
+
def show_button(record, label: nil)
|
|
108
|
+
a(href: resource_show_path(@resource.class, record), class: "text-sm text-primary hover:underline") { label || rua_t("actions.show") }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def edit_button(record, label: nil)
|
|
112
|
+
a(href: resource_edit_path(@resource.class, record), class: "text-sm text-primary hover:underline") { label || rua_t("actions.edit") }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def create_button(label: nil)
|
|
116
|
+
control_link(label || rua_t("actions.new", model: @resource.model_class.model_name.human), resource_new_path(@resource.class), variant: :primary)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Standalone actions (no record selection needed) shown on the index header.
|
|
120
|
+
def render_standalone_actions
|
|
121
|
+
return unless authorized_to?(:act_on, @resource.model_class, policy_class: policy_class)
|
|
122
|
+
|
|
123
|
+
actions = @resource.actions_for(view: :index).select(&:standalone?)
|
|
124
|
+
return if actions.empty?
|
|
125
|
+
|
|
126
|
+
render_actions_menu(actions, bulk: false)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Bulk actions: an empty form (referenced by row checkboxes via the HTML `form`
|
|
130
|
+
# attribute) plus an "Actions" dropdown of the bulk-action triggers.
|
|
131
|
+
def render_bulk_toolbar
|
|
132
|
+
return unless bulk?
|
|
133
|
+
|
|
134
|
+
form(id: BULK_FORM_ID, method: "get")
|
|
135
|
+
|
|
136
|
+
div(class: "flex items-center justify-end gap-2 mb-4") do
|
|
137
|
+
span(class: "text-sm text-muted-foreground") { rua_t("index.with_selected") }
|
|
138
|
+
render_actions_menu(bulk_actions, bulk: true, form_id: BULK_FORM_ID)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def render_table
|
|
143
|
+
render RubyUI::Table.new do
|
|
144
|
+
render RubyUI::TableHeader.new do
|
|
145
|
+
render RubyUI::TableRow.new do
|
|
146
|
+
render RubyUI::TableHead.new(class: "w-10") { select_all_checkbox } if bulk?
|
|
147
|
+
fields.each do |field|
|
|
148
|
+
render RubyUI::TableHead.new { field.name }
|
|
149
|
+
end
|
|
150
|
+
render RubyUI::TableHead.new(class: "text-right") { rua_t("index.actions") }
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
render RubyUI::TableBody.new do
|
|
155
|
+
if @records.empty?
|
|
156
|
+
render RubyUI::TableRow.new do
|
|
157
|
+
render RubyUI::TableCell.new(colspan: column_count, class: "text-center text-muted-foreground py-8") { rua_t("index.empty") }
|
|
158
|
+
end
|
|
159
|
+
else
|
|
160
|
+
@records.each { |record| render_row(record) }
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def column_count
|
|
167
|
+
fields.size + 1 + (bulk? ? 1 : 0)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Hidden until the JS reveals it (progressive enhancement).
|
|
171
|
+
def select_all_checkbox
|
|
172
|
+
render RubyUI::Checkbox.new(
|
|
173
|
+
hidden: true,
|
|
174
|
+
data: {rua__bulk_select_target: "selectAll", action: "change->rua--bulk-select#toggleAll"}
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def row_attrs(record)
|
|
179
|
+
return {} unless RubyUIAdmin.configuration.click_row_to_view_record
|
|
180
|
+
|
|
181
|
+
{
|
|
182
|
+
class: "cursor-pointer",
|
|
183
|
+
data: {
|
|
184
|
+
controller: "rua--row-link",
|
|
185
|
+
action: "click->rua--row-link#navigate",
|
|
186
|
+
rua__row_link_url_value: resource_show_path(@resource.class, record)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def render_row(record)
|
|
192
|
+
render RubyUI::TableRow.new(**row_attrs(record)) do
|
|
193
|
+
if bulk?
|
|
194
|
+
render RubyUI::TableCell.new do
|
|
195
|
+
render RubyUI::Checkbox.new(
|
|
196
|
+
name: "record_ids[]",
|
|
197
|
+
value: record.id,
|
|
198
|
+
form: BULK_FORM_ID,
|
|
199
|
+
data: {rua_row_select: true}
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
fields.each_with_index do |field, index|
|
|
205
|
+
render RubyUI::TableCell.new(class: ("font-medium" if index.zero?)) do
|
|
206
|
+
link = field.link_to_record? ? resource_show_path(@resource.class, record) : nil
|
|
207
|
+
render RubyUIAdmin::Views::FieldValue.new(field: field, record: record, link: link)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
render RubyUI::TableCell.new(class: "text-right whitespace-nowrap") do
|
|
212
|
+
render_row_actions(record)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def render_row_actions(record)
|
|
218
|
+
cfg = @resource.class.row_controls_config || {}
|
|
219
|
+
placement = (cfg[:placement] == :left) ? "justify-start" : "justify-end"
|
|
220
|
+
modes = [("float" if cfg[:float]), ("hover" if cfg[:show_on_hover])].compact.join(" ")
|
|
221
|
+
data = modes.empty? ? {} : {rua_row_controls: modes}
|
|
222
|
+
|
|
223
|
+
div(class: "inline-flex items-center gap-2 #{placement}", data: data) do
|
|
224
|
+
render_row_controls(record)
|
|
225
|
+
|
|
226
|
+
label = rua_t("actions.show")
|
|
227
|
+
a(href: resource_show_path(@resource.class, record), title: label, aria_label: label,
|
|
228
|
+
class: "inline-flex p-1 text-muted-foreground hover:text-foreground transition-colors") do
|
|
229
|
+
render RubyUIAdmin::UI::Icon.new(:eye)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if authorized_to?(:update, record, policy_class: policy_class)
|
|
233
|
+
label = rua_t("actions.edit")
|
|
234
|
+
a(href: resource_edit_path(@resource.class, record), title: label, aria_label: label,
|
|
235
|
+
class: "inline-flex p-1 text-muted-foreground hover:text-foreground transition-colors") do
|
|
236
|
+
render RubyUIAdmin::UI::Icon.new(:pencil)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
if authorized_to?(:destroy, record, policy_class: policy_class)
|
|
241
|
+
render_delete_button(record)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Resource `self.row_controls = ->(record) { ... }` — extra per-row buttons/links.
|
|
247
|
+
# Evaluated in this component (Phlex methods, `control_link`, route helpers available).
|
|
248
|
+
def render_row_controls(record)
|
|
249
|
+
controls = @resource.class.row_controls
|
|
250
|
+
return if controls.nil?
|
|
251
|
+
|
|
252
|
+
instance_exec(record, &controls)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def render_delete_button(record)
|
|
256
|
+
form(action: resource_show_path(@resource.class, record), method: "post", class: "inline") do
|
|
257
|
+
input(type: "hidden", name: "_method", value: "delete")
|
|
258
|
+
input(type: "hidden", name: "authenticity_token", value: form_authenticity_token)
|
|
259
|
+
# `rua--confirm#request` opens the shared AlertDialog before submitting (JS); without
|
|
260
|
+
# JS the form submits directly. The message names the record being deleted.
|
|
261
|
+
label = rua_t("actions.destroy")
|
|
262
|
+
button(
|
|
263
|
+
type: "submit",
|
|
264
|
+
title: label,
|
|
265
|
+
aria_label: label,
|
|
266
|
+
data: {
|
|
267
|
+
action: "click->rua--confirm#request",
|
|
268
|
+
rua__confirm_message_param: rua_t("confirm.destroy", model: @resource.record_title(record)),
|
|
269
|
+
rua__confirm_heading_param: rua_t("confirm.destroy_title")
|
|
270
|
+
},
|
|
271
|
+
class: "inline-flex p-1 text-destructive hover:text-destructive/80 transition-colors"
|
|
272
|
+
) { render RubyUIAdmin::UI::Icon.new(:trash) }
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def render_scope_tabs
|
|
277
|
+
scopes = @scopes.select { |scope| scope.visible?(user: RubyUIAdmin::Current.user) }
|
|
278
|
+
return if scopes.empty?
|
|
279
|
+
|
|
280
|
+
base = resource_index_path(@resource.class)
|
|
281
|
+
|
|
282
|
+
nav(class: "flex items-center gap-1 mb-4 border-b border-border") do
|
|
283
|
+
unless @remove_scope_all
|
|
284
|
+
scope_tab(rua_t("index.all"), "#{base}?scope=all", active: @current_scope_key.nil?)
|
|
285
|
+
end
|
|
286
|
+
scopes.each do |scope|
|
|
287
|
+
scope_tab(scope.name, "#{base}?scope=#{scope.key}", active: @current_scope_key == scope.key, title: scope.description)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def scope_tab(label, href, active:, title: nil)
|
|
293
|
+
state = active ? "border-primary text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"
|
|
294
|
+
a(href: href, title: title, class: "px-3 py-2 -mb-px border-b-2 text-sm font-medium #{state}") { label }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def render_filter_bar
|
|
298
|
+
render RubyUIAdmin::Views::FilterBar.new(
|
|
299
|
+
filters: @filters,
|
|
300
|
+
values: @filter_values,
|
|
301
|
+
action_path: resource_index_path(@resource.class),
|
|
302
|
+
scope_param: @scope_param
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def countless_pagy?
|
|
307
|
+
defined?(Pagy::Countless) && @pagy.is_a?(Pagy::Countless)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def render_pagination
|
|
311
|
+
return if @pagy.nil?
|
|
312
|
+
# Countless has no total `pages`; render prev/next only.
|
|
313
|
+
return if countless_pagy? ? !(@pagy.prev || @pagy.next) : @pagy.pages <= 1
|
|
314
|
+
|
|
315
|
+
base = resource_index_path(@resource.class)
|
|
316
|
+
render RubyUIAdmin::UI::Pagination.new(
|
|
317
|
+
pagy: @pagy,
|
|
318
|
+
countless: countless_pagy?,
|
|
319
|
+
url_for_page: ->(page) { "#{base}?#{(@query_params || {}).merge("page" => page).to_query}" }
|
|
320
|
+
)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Views
|
|
5
|
+
# Resolves the dynamic per-resource engine routes from inside Phlex views.
|
|
6
|
+
# `rc` is a resource class. Engine routes are reached through the mounted
|
|
7
|
+
# `ruby_ui_admin` proxy (provided by RailsHelpers / Routes).
|
|
8
|
+
module PathHelpers
|
|
9
|
+
def resource_index_path(rc)
|
|
10
|
+
ruby_ui_admin.public_send("resources_#{rc.route_key}_path")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resource_new_path(rc)
|
|
14
|
+
ruby_ui_admin.public_send("new_resources_#{rc.singular_route_key}_path")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def resource_show_path(rc, record)
|
|
18
|
+
ruby_ui_admin.public_send("resources_#{rc.singular_route_key}_path", record)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def resource_edit_path(rc, record)
|
|
22
|
+
ruby_ui_admin.public_send("edit_resources_#{rc.singular_route_key}_path", record)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Path to an ActiveStorage blob. Generated via the application's url helpers (NOT the
|
|
26
|
+
# request-bound `main_app` proxy): ActiveStorage's blob route is a `direct` route, and inside
|
|
27
|
+
# a mounted engine the proxy prepends the engine's SCRIPT_NAME (e.g. `/admin/rails/...`),
|
|
28
|
+
# which 404s — AS lives at the host root. `disposition: "attachment"` forces a download.
|
|
29
|
+
def attachment_url(blob, disposition: nil)
|
|
30
|
+
opts = {only_path: true}
|
|
31
|
+
opts[:disposition] = disposition if disposition
|
|
32
|
+
Rails.application.routes.url_helpers.rails_blob_path(blob, **opts)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Views
|
|
5
|
+
# Bundles the non-deprecated phlex-rails helper adapters used across views.
|
|
6
|
+
# `Routes` brings the host app url helpers plus mounted-engine proxies, so
|
|
7
|
+
# engine routes are reached via the `ruby_ui_admin` proxy.
|
|
8
|
+
module RailsHelpers
|
|
9
|
+
include Phlex::Rails::Helpers::Routes
|
|
10
|
+
include Phlex::Rails::Helpers::Flash
|
|
11
|
+
include Phlex::Rails::Helpers::FormAuthenticityToken
|
|
12
|
+
include Phlex::Rails::Helpers::Request
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Views
|
|
5
|
+
class Show < Base
|
|
6
|
+
include StructureRenderer
|
|
7
|
+
include ShowFields
|
|
8
|
+
include ActionsMenu
|
|
9
|
+
|
|
10
|
+
def initialize(resource:, record:)
|
|
11
|
+
@resource = resource
|
|
12
|
+
@record = record
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def page_title
|
|
16
|
+
"#{@resource.record_title(@record)} · #{RubyUIAdmin.configuration.app_name}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def content
|
|
20
|
+
render_header
|
|
21
|
+
# `space-y-6` separates the structure's top-level sections (the fields card, panels
|
|
22
|
+
# and tab groups) so they aren't glued together — mirrors the form's `space-y-6`.
|
|
23
|
+
div(class: "space-y-6") do
|
|
24
|
+
render_structure(@resource.field_structure(view: :show))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Lazy tabs (config): non-active show tabs fetch their content when first opened.
|
|
29
|
+
def lazy_tabs?
|
|
30
|
+
RubyUIAdmin.configuration.lazy_tabs
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# URL a lazy tab fetches — the show path with the tab's flat index and `fragment=1`,
|
|
34
|
+
# so only that tab's structure is rendered (and its queries run) on demand.
|
|
35
|
+
def tab_fragment_url(flat_index)
|
|
36
|
+
"#{resource_show_path(@resource.class, @record)}?tab=#{flat_index}&fragment=1"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def policy_class
|
|
42
|
+
@resource.class.authorization_policy
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render_header
|
|
46
|
+
div(class: "flex items-center justify-between mb-6") do
|
|
47
|
+
div do
|
|
48
|
+
a(href: resource_index_path(@resource.class), class: "text-sm text-muted-foreground hover:underline") { "← #{@resource.navigation_label}" }
|
|
49
|
+
h1(class: "text-2xl font-semibold tracking-tight mt-1") { @resource.record_title(@record).to_s }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
div(class: "flex items-center gap-2") do
|
|
53
|
+
render_record_actions
|
|
54
|
+
|
|
55
|
+
if authorized_to?(:update, @record, policy_class: policy_class)
|
|
56
|
+
a(
|
|
57
|
+
href: resource_edit_path(@resource.class, @record),
|
|
58
|
+
class: "inline-flex items-center h-9 px-4 rounded-md border border-input bg-background text-sm font-medium shadow-sm hover:bg-accent"
|
|
59
|
+
) { rua_t("actions.edit") }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Actions available for this single record, shown on the show view. Collapsed into the
|
|
66
|
+
# searchable Combobox (same as the index) so many actions don't sprawl across the header.
|
|
67
|
+
def render_record_actions
|
|
68
|
+
return unless authorized_to?(:act_on, @record, policy_class: policy_class)
|
|
69
|
+
|
|
70
|
+
actions = @resource.actions_for(view: :show, record: @record)
|
|
71
|
+
return if actions.empty?
|
|
72
|
+
|
|
73
|
+
render_actions_menu(actions, record_ids: [@record.id])
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Views
|
|
5
|
+
# Renders a show field as a label/value row. Shared by the full Show view and the lazy
|
|
6
|
+
# tab fragment (ShowTab) so both render fields identically. Expects `@record` to be set.
|
|
7
|
+
module ShowFields
|
|
8
|
+
# Called by StructureRenderer for each field.
|
|
9
|
+
def render_field(field)
|
|
10
|
+
render_field_row(field)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render_field_row(field)
|
|
14
|
+
# Collection associations render as a full-width table, so the label goes above it
|
|
15
|
+
# (a left label column would squeeze the table into a third of the width).
|
|
16
|
+
return render_association_row(field) if %i[has_many has_and_belongs_to_many].include?(field.type)
|
|
17
|
+
|
|
18
|
+
div(class: "grid grid-cols-3 gap-4 py-3 border-b border-border last:border-b-0") do
|
|
19
|
+
dt(class: "text-sm font-medium text-muted-foreground", title: field.description) { field.name }
|
|
20
|
+
dd(class: "text-sm col-span-2") do
|
|
21
|
+
render RubyUIAdmin::Views::FieldValue.new(field: field, record: @record)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render_association_row(field)
|
|
27
|
+
div(class: "py-3 space-y-2 border-b border-border last:border-b-0") do
|
|
28
|
+
div do
|
|
29
|
+
span(class: "text-sm font-medium text-muted-foreground") { field.name }
|
|
30
|
+
p(class: "text-xs text-muted-foreground") { field.description } if field.description
|
|
31
|
+
end
|
|
32
|
+
render RubyUIAdmin::Views::FieldValue.new(field: field, record: @record)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|