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,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Views
|
|
5
|
+
# Renders a single show tab's content as a standalone fragment (no page chrome), fetched
|
|
6
|
+
# lazily by the JS when the tab is first opened. `tab_index` is the tab's flat position
|
|
7
|
+
# across all tab groups on the show view — the same index the full page assigned.
|
|
8
|
+
class ShowTab < Phlex::HTML
|
|
9
|
+
include RubyUIAdmin::UI
|
|
10
|
+
include RailsHelpers
|
|
11
|
+
include PathHelpers
|
|
12
|
+
include Translation
|
|
13
|
+
include StructureRenderer
|
|
14
|
+
include ShowFields
|
|
15
|
+
|
|
16
|
+
def initialize(resource:, record:, tab_index:)
|
|
17
|
+
@resource = resource
|
|
18
|
+
@record = record
|
|
19
|
+
@tab_index = tab_index
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def view_template
|
|
23
|
+
tab = flat_tabs[@tab_index]
|
|
24
|
+
return if tab.nil?
|
|
25
|
+
|
|
26
|
+
# Wrapped in the matching `<turbo-frame>` so the lazy frame on the show page swaps this in.
|
|
27
|
+
# `target="_top"` so links/forms inside the tab break out of the frame (else Turbo renders
|
|
28
|
+
# "Content missing" navigating within the frame to a page that has no matching frame).
|
|
29
|
+
tag(:turbo_frame, id: "rua-tab-frame-#{@tab_index}", target: "_top") do
|
|
30
|
+
render_tab_content(tab)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Walks the show structure in the same depth-first order the full page renders, collecting
|
|
37
|
+
# every tab so `@tab_index` maps to the same tab the page's flat counter assigned.
|
|
38
|
+
def flat_tabs
|
|
39
|
+
tabs = []
|
|
40
|
+
collect_tabs(@resource.field_structure(view: :show), tabs)
|
|
41
|
+
tabs
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def collect_tabs(items, acc)
|
|
45
|
+
items.each do |item|
|
|
46
|
+
if item.is_a?(RubyUIAdmin::Structure::Panel)
|
|
47
|
+
collect_tabs(item.items, acc)
|
|
48
|
+
elsif item.is_a?(RubyUIAdmin::Structure::TabGroup)
|
|
49
|
+
item.tabs.each do |tab|
|
|
50
|
+
acc << tab
|
|
51
|
+
collect_tabs(tab.items, acc)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Views
|
|
5
|
+
# Walks a resource's field structure (fields + panels + tabs) and renders it.
|
|
6
|
+
# The includer defines `render_field(field)` to render a single field row/input.
|
|
7
|
+
# Bare top-level fields are grouped into a default card.
|
|
8
|
+
module StructureRenderer
|
|
9
|
+
# `wrap_bare:` controls whether loose top-level fields get their own card. It's true
|
|
10
|
+
# at the top level (so naked fields aren't unframed) and false inside a panel/tab —
|
|
11
|
+
# which already provide a card — to avoid a redundant nested card layer.
|
|
12
|
+
def render_structure(items, wrap_bare: true)
|
|
13
|
+
buffer = []
|
|
14
|
+
|
|
15
|
+
flush = lambda do
|
|
16
|
+
next if buffer.empty?
|
|
17
|
+
|
|
18
|
+
group = buffer.dup
|
|
19
|
+
buffer.clear
|
|
20
|
+
if wrap_bare
|
|
21
|
+
render RubyUI::Card.new do
|
|
22
|
+
render RubyUI::CardContent.new(class: "pt-6 space-y-4") do
|
|
23
|
+
group.each { |field| render_field(field) }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
else
|
|
27
|
+
group.each { |field| render_field(field) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
items.each do |item|
|
|
32
|
+
if item.is_a?(RubyUIAdmin::Fields::BaseField)
|
|
33
|
+
buffer << item
|
|
34
|
+
elsif item.is_a?(RubyUIAdmin::Structure::Panel)
|
|
35
|
+
flush.call
|
|
36
|
+
render_panel(item)
|
|
37
|
+
elsif item.is_a?(RubyUIAdmin::Structure::TabGroup)
|
|
38
|
+
flush.call
|
|
39
|
+
render_tab_group(item)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
flush.call
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def render_panel(panel)
|
|
49
|
+
render RubyUI::Card.new(class: "mb-6") do
|
|
50
|
+
# Show the panel title, except when it merely repeats the name of the tab that
|
|
51
|
+
# contains it — that's pure duplication (the tab bar already labels the section),
|
|
52
|
+
# and the empty header would just push the content down. Distinct names still render.
|
|
53
|
+
if panel.name && panel.name.to_s.strip != @enclosing_tab_name.to_s.strip
|
|
54
|
+
render RubyUI::CardHeader.new do
|
|
55
|
+
render RubyUI::CardTitle.new { panel.name }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
render RubyUI::CardContent.new(class: "pt-6 space-y-4") do
|
|
59
|
+
render_structure(panel.items, wrap_bare: false)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Tabs render as a progressive enhancement, driven by the `rua--tabs` Stimulus controller:
|
|
65
|
+
# * Without JS: the tab bar is hidden and the panels stack (each as a titled card).
|
|
66
|
+
# * With JS: the controller reveals the tab bar, hides the per-panel headings, and shows
|
|
67
|
+
# only the active panel. Keys are matched via `data-rua-tab` / `data-rua-tab-panel`.
|
|
68
|
+
def render_tab_group(group)
|
|
69
|
+
@tab_group_seq = (@tab_group_seq || 0) + 1
|
|
70
|
+
base = "rua-tab-#{@tab_group_seq}"
|
|
71
|
+
|
|
72
|
+
div(data: {controller: "rua--tabs"}, class: "mb-6") do
|
|
73
|
+
nav(data: {rua__tabs_target: "nav"}, hidden: true, class: "flex items-center gap-1 border-b border-border mb-4") do
|
|
74
|
+
group.tabs.each_with_index do |tab, index|
|
|
75
|
+
button(
|
|
76
|
+
type: "button",
|
|
77
|
+
data: {rua__tabs_target: "tab", rua_tab: "#{base}-#{index}", action: "click->rua--tabs#show"},
|
|
78
|
+
class: "px-3 py-2 -mb-px border-b-2 border-transparent text-sm font-medium text-muted-foreground hover:text-foreground"
|
|
79
|
+
) { tab.name }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
group.tabs.each_with_index do |tab, index|
|
|
84
|
+
# Flat index across all tab groups on the page — the lazy fragment endpoint uses it
|
|
85
|
+
# to locate this exact tab. Incremented for every tab (eager and lazy) so it stays
|
|
86
|
+
# aligned with the controller's identical walk of the structure.
|
|
87
|
+
flat_index = (@tab_flat_seq ||= -1) + 1
|
|
88
|
+
@tab_flat_seq = flat_index
|
|
89
|
+
# Only non-active tabs (index > 0 within their group) load lazily; the first tab is
|
|
90
|
+
# the active one and always renders eagerly so there's no flash/spinner up front.
|
|
91
|
+
lazy_url = (lazy_tabs? && index.positive?) ? tab_fragment_url(flat_index) : nil
|
|
92
|
+
|
|
93
|
+
panel_data = {rua__tabs_target: "panel", rua_tab_panel: "#{base}-#{index}"}
|
|
94
|
+
|
|
95
|
+
# The tab is just the switchable container — its children (panels) provide the
|
|
96
|
+
# cards, so we don't wrap the tab body in its own Card (which caused a redundant
|
|
97
|
+
# nested-panel layer). `wrap_bare: true` still cards loose fields inside a tab.
|
|
98
|
+
div(data: panel_data, class: "space-y-4") do
|
|
99
|
+
# Heading shown when stacked (no JS); the controller hides it since the tab bar
|
|
100
|
+
# already labels the section.
|
|
101
|
+
h3(data: {rua__tabs_target: "heading"}, class: "text-sm font-semibold uppercase tracking-wide text-muted-foreground") { tab.name }
|
|
102
|
+
# Skip a description that merely repeats the tab name — it's pure duplication
|
|
103
|
+
# (the tab bar already shows the name). Genuine, distinct descriptions still render.
|
|
104
|
+
if tab.description.present? && tab.description.to_s.strip != tab.name.to_s.strip
|
|
105
|
+
p(class: "text-sm text-muted-foreground") { tab.description }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if lazy_url
|
|
109
|
+
render_lazy_tab_frame("rua-tab-frame-#{flat_index}", lazy_url)
|
|
110
|
+
else
|
|
111
|
+
render_tab_content(tab)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Renders a tab's inner structure, tracking the enclosing tab name so a child panel named
|
|
119
|
+
# identically to the tab can drop its redundant title (see render_panel).
|
|
120
|
+
def render_tab_content(tab)
|
|
121
|
+
prev_tab_name = @enclosing_tab_name
|
|
122
|
+
@enclosing_tab_name = tab.name
|
|
123
|
+
render_structure(tab.items, wrap_bare: true)
|
|
124
|
+
@enclosing_tab_name = prev_tab_name
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# A lazy `<turbo-frame loading="lazy">`: Turbo fetches `src` when the frame becomes visible
|
|
128
|
+
# (i.e. when its tab is shown) and swaps in the matching frame from the response (ShowTab).
|
|
129
|
+
# The spinner shows until then; without JS the <noscript> notice replaces it.
|
|
130
|
+
# `target="_top"` so links/forms inside the tab content navigate the whole page (otherwise
|
|
131
|
+
# they'd try to navigate within this frame and Turbo would render "Content missing" — the
|
|
132
|
+
# target page has no matching frame id). It doesn't affect the frame's own `src` lazy load.
|
|
133
|
+
def render_lazy_tab_frame(frame_id, src)
|
|
134
|
+
tag(:turbo_frame, id: frame_id, loading: "lazy", src: src, target: "_top") do
|
|
135
|
+
div(data: {rua_tab_spinner: true}, class: "flex items-center justify-center py-12") do
|
|
136
|
+
div(class: "size-6 rounded-full border-2 border-muted border-t-foreground animate-spin", role: "status", aria_label: rua_t("tabs.loading"))
|
|
137
|
+
end
|
|
138
|
+
noscript { p(class: "text-sm text-muted-foreground py-4") { rua_t("tabs.requires_js") } }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Overridden by the Show view to enable lazy tab loading; eager everywhere else.
|
|
143
|
+
def lazy_tabs? = false
|
|
144
|
+
|
|
145
|
+
def tab_fragment_url(_flat_index) = nil
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Views
|
|
5
|
+
# Translates framework UI strings under the `ruby_ui_admin.*` i18n namespace.
|
|
6
|
+
module Translation
|
|
7
|
+
def rua_t(key, **options)
|
|
8
|
+
I18n.t("ruby_ui_admin.#{key}", **options)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
# Renders an action's form (GET #show) and executes it (POST #run).
|
|
5
|
+
class ActionsController < ApplicationController
|
|
6
|
+
before_action :set_resource
|
|
7
|
+
before_action :set_action_entry
|
|
8
|
+
before_action :set_records
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
action = build_action
|
|
12
|
+
authorize_action!(:act_on, on: authorization_target, policy_class: policy_class)
|
|
13
|
+
|
|
14
|
+
# `?fragment=1` returns just the form (no page chrome), wrapped in the modal's matching
|
|
15
|
+
# `<turbo-frame id=frame_id>` so Turbo swaps it in. The action's `fields` are only
|
|
16
|
+
# evaluated when the modal is actually opened (the frame is loaded).
|
|
17
|
+
if params[:fragment]
|
|
18
|
+
render RubyUIAdmin::Views::ActionForm.new(
|
|
19
|
+
resource: @resource,
|
|
20
|
+
action: action,
|
|
21
|
+
action_id: params[:action_id],
|
|
22
|
+
record_ids: @record_ids,
|
|
23
|
+
frame_id: params[:frame_id]
|
|
24
|
+
)
|
|
25
|
+
else
|
|
26
|
+
render Views::Action.new(
|
|
27
|
+
resource: @resource,
|
|
28
|
+
action: action,
|
|
29
|
+
action_id: params[:action_id],
|
|
30
|
+
record_ids: @record_ids
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def run
|
|
36
|
+
action = build_action
|
|
37
|
+
authorize_action!(:act_on, on: authorization_target, policy_class: policy_class)
|
|
38
|
+
|
|
39
|
+
action.records = @records
|
|
40
|
+
action.field_values = field_values(action)
|
|
41
|
+
invoke_handle(action)
|
|
42
|
+
|
|
43
|
+
process_response(action.response)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def set_resource
|
|
49
|
+
resource_class = RubyUIAdmin.resource_manager.find_by_route_key(params[:resource_name])
|
|
50
|
+
raise ActionController::RoutingError, "Unknown resource #{params[:resource_name]}" if resource_class.nil?
|
|
51
|
+
|
|
52
|
+
@resource = resource_class.new.hydrate(user: current_user, params: params)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def set_action_entry
|
|
56
|
+
@entry = @resource.find_action_entry(params[:action_id])
|
|
57
|
+
raise ActionController::RoutingError, "Unknown action #{params[:action_id]}" if @entry.nil?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def set_records
|
|
61
|
+
@record_ids = Array(params[:record_ids]).reject(&:blank?)
|
|
62
|
+
@records = @record_ids.any? ? @resource.base_query.where(id: @record_ids).to_a : []
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def policy_class
|
|
66
|
+
@resource.class.authorization_policy
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def authorization_target
|
|
70
|
+
@records.first || @resource.model_class
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def action_view
|
|
74
|
+
@records.size == 1 ? :show : :index
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_action
|
|
78
|
+
action = @resource.instantiate_action(@entry, view: action_view, records: @records)
|
|
79
|
+
action.controller = self
|
|
80
|
+
action
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Indifferent-access so `handle` can read `fields[:csv_file]` or `fields["csv_file"]`.
|
|
84
|
+
def field_values(action)
|
|
85
|
+
submitted = params[:fields] || {}
|
|
86
|
+
values = action.get_fields.each_with_object({}) do |field, acc|
|
|
87
|
+
acc[field.id.to_s] = submitted[field.id.to_s]
|
|
88
|
+
end
|
|
89
|
+
ActiveSupport::HashWithIndifferentAccess.new(values)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Supports both `handle(query:, fields:, current_user:, resource:, **)` and
|
|
93
|
+
# the positional `handle(args)` style (args[:records], args[:fields], ...).
|
|
94
|
+
def invoke_handle(action)
|
|
95
|
+
kwargs = {
|
|
96
|
+
query: @records,
|
|
97
|
+
records: @records,
|
|
98
|
+
fields: action.field_values,
|
|
99
|
+
current_user: current_user,
|
|
100
|
+
resource: @resource
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
parameters = action.method(:handle).parameters
|
|
104
|
+
keyword_kinds = parameters.map(&:first)
|
|
105
|
+
|
|
106
|
+
if (keyword_kinds & %i[key keyreq keyrest]).any?
|
|
107
|
+
if keyword_kinds.include?(:keyrest)
|
|
108
|
+
action.handle(**kwargs)
|
|
109
|
+
else
|
|
110
|
+
accepted = parameters.select { |(type, _)| %i[key keyreq].include?(type) }.map(&:last)
|
|
111
|
+
action.handle(**kwargs.slice(*accepted))
|
|
112
|
+
end
|
|
113
|
+
else
|
|
114
|
+
action.handle(kwargs)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def process_response(response)
|
|
119
|
+
case response[:type]
|
|
120
|
+
when :download
|
|
121
|
+
download = response[:download]
|
|
122
|
+
return send_data(download[:content], filename: download[:filename])
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
apply_flash(messages_for(response))
|
|
126
|
+
|
|
127
|
+
# 303 See Other so Turbo follows the POST→redirect as a GET visit.
|
|
128
|
+
case response[:type]
|
|
129
|
+
when :redirect
|
|
130
|
+
redirect_to resolve_path(response[:path]), status: :see_other
|
|
131
|
+
else # :reload and default
|
|
132
|
+
redirect_back fallback_location: default_back_path, status: :see_other
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# An action that finishes without setting its own message still confirms it ran (Avo
|
|
137
|
+
# parity) — unless it explicitly opted out with `silent`.
|
|
138
|
+
def messages_for(response)
|
|
139
|
+
messages = Array(response[:messages])
|
|
140
|
+
return messages if messages.any? || response[:silent]
|
|
141
|
+
|
|
142
|
+
[{type: :success, body: I18n.t("ruby_ui_admin.actions.ran_successfully")}]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def apply_flash(messages)
|
|
146
|
+
Array(messages).each do |message|
|
|
147
|
+
key = (message[:type] == :error) ? :alert : :notice
|
|
148
|
+
flash[key] = message[:body]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def resolve_path(path)
|
|
153
|
+
return default_back_path if path.nil?
|
|
154
|
+
|
|
155
|
+
path.respond_to?(:call) ? instance_exec(&path) : path
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def default_back_path
|
|
159
|
+
helpers.public_send("resources_#{@resource.route_key}_path")
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pagy"
|
|
4
|
+
# `pagy/extras/countless` is required at engine boot (see engine.rb) before host initializers.
|
|
5
|
+
|
|
6
|
+
module RubyUIAdmin
|
|
7
|
+
class ApplicationController < ActionController::Base
|
|
8
|
+
include Pagy::Backend
|
|
9
|
+
|
|
10
|
+
protect_from_forgery with: :exception
|
|
11
|
+
|
|
12
|
+
around_action :with_current_context
|
|
13
|
+
before_action :authenticate_admin!
|
|
14
|
+
|
|
15
|
+
helper_method :current_user
|
|
16
|
+
|
|
17
|
+
rescue_from RubyUIAdmin::NotAuthorizedError do |error|
|
|
18
|
+
render plain: "403 Forbidden — #{error.message}", status: :forbidden
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def with_current_context
|
|
24
|
+
RubyUIAdmin::Current.request = request
|
|
25
|
+
RubyUIAdmin::Current.user = current_user
|
|
26
|
+
RubyUIAdmin::Current.true_user = true_user
|
|
27
|
+
with_admin_locale_and_timezone { yield }
|
|
28
|
+
ensure
|
|
29
|
+
RubyUIAdmin::Current.reset
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Applies the configured timezone and locale for the duration of the request.
|
|
33
|
+
def with_admin_locale_and_timezone(&block)
|
|
34
|
+
zone = RubyUIAdmin.configuration.timezone
|
|
35
|
+
locale = RubyUIAdmin.configuration.locale
|
|
36
|
+
|
|
37
|
+
localized = lambda do
|
|
38
|
+
locale ? I18n.with_locale(locale, &block) : block.call
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
zone ? Time.use_zone(zone) { localized.call } : localized.call
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def current_user
|
|
45
|
+
return @current_user if defined?(@current_user)
|
|
46
|
+
|
|
47
|
+
method = RubyUIAdmin.configuration.current_user_method
|
|
48
|
+
@current_user =
|
|
49
|
+
if method
|
|
50
|
+
instance_exec(self, &method)
|
|
51
|
+
else
|
|
52
|
+
# Sensible fallback for Warden/Devise hosts when no method is configured.
|
|
53
|
+
request.env["warden"]&.user
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The real (non-impersonated) user; defaults to `current_user` when no method is set.
|
|
58
|
+
def true_user
|
|
59
|
+
return @true_user if defined?(@true_user)
|
|
60
|
+
|
|
61
|
+
method = RubyUIAdmin.configuration.true_user_method
|
|
62
|
+
@true_user = method ? instance_exec(self, &method) : current_user
|
|
63
|
+
end
|
|
64
|
+
helper_method :true_user
|
|
65
|
+
|
|
66
|
+
def authenticate_admin!
|
|
67
|
+
block = RubyUIAdmin.configuration.authenticate_with
|
|
68
|
+
instance_exec(&block) if block
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Builds an authorization service for the current user.
|
|
72
|
+
def authorization_for(record = nil, policy_class: nil)
|
|
73
|
+
Services::AuthorizationService.new(current_user, record, policy_class: policy_class, true_user: true_user)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Authorizes a rule or raises NotAuthorizedError.
|
|
77
|
+
def authorize_action!(rule, on:, policy_class: nil)
|
|
78
|
+
authorization_for(on, policy_class: policy_class).authorize_action(rule, raise_exception: true)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
class DashboardsController < ApplicationController
|
|
5
|
+
def show
|
|
6
|
+
dashboard_class = RubyUIAdmin.dashboard_manager.find(params[:dashboard_id])
|
|
7
|
+
raise ActionController::RoutingError, "Unknown dashboard #{params[:dashboard_id]}" if dashboard_class.nil?
|
|
8
|
+
|
|
9
|
+
render Views::Dashboard.new(dashboard: dashboard_class.new)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
# In-app documentation browser. Renders the gem's bundled `docs/*.md` files as HTML at
|
|
5
|
+
# `<mount>/docs`. Availability is controlled by `config.docs_enabled` (default `:local`, i.e.
|
|
6
|
+
# development/test only). In local environments it's open (no auth) for convenience; in any other
|
|
7
|
+
# environment (e.g. production, when explicitly enabled) it runs behind the admin's
|
|
8
|
+
# `authenticate_admin!` gate.
|
|
9
|
+
class DocsController < ApplicationController
|
|
10
|
+
skip_before_action :authenticate_admin!, if: -> { Rails.env.local? }
|
|
11
|
+
|
|
12
|
+
# Root of the gem's Markdown documentation.
|
|
13
|
+
DOCS_ROOT = RubyUIAdmin::Engine.root.join("docs").freeze
|
|
14
|
+
|
|
15
|
+
before_action :ensure_docs_enabled!
|
|
16
|
+
|
|
17
|
+
def show
|
|
18
|
+
rel = resolve_relative_path(params[:path])
|
|
19
|
+
return head(:not_found) unless rel
|
|
20
|
+
|
|
21
|
+
abs = File.expand_path(rel, DOCS_ROOT)
|
|
22
|
+
return head(:not_found) unless safe_doc_path?(abs)
|
|
23
|
+
|
|
24
|
+
source = File.read(abs)
|
|
25
|
+
html = render_markdown(source, current_dir: File.dirname(rel))
|
|
26
|
+
|
|
27
|
+
render Views::Docs.new(
|
|
28
|
+
body_html: html,
|
|
29
|
+
title: doc_title(source) || humanize_slug(slug_for(rel)),
|
|
30
|
+
nav_groups: nav_groups,
|
|
31
|
+
current_slug: slug_for(rel)
|
|
32
|
+
)
|
|
33
|
+
rescue MissingDocsDependency => e
|
|
34
|
+
render Views::Docs.new(
|
|
35
|
+
body_html: dependency_help_html(e.message),
|
|
36
|
+
title: "Documentation",
|
|
37
|
+
nav_groups: [],
|
|
38
|
+
current_slug: nil
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def ensure_docs_enabled!
|
|
45
|
+
head :not_found unless RubyUIAdmin.configuration.docs_enabled?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Maps a URL path segment to a `docs/`-relative `.md` file. Returns nil for anything that
|
|
49
|
+
# isn't a plain Markdown file (defense in depth alongside `safe_doc_path?`).
|
|
50
|
+
def resolve_relative_path(path)
|
|
51
|
+
slug = path.presence || "README"
|
|
52
|
+
slug = slug.delete_suffix(".md")
|
|
53
|
+
return nil if slug.include?("\0")
|
|
54
|
+
|
|
55
|
+
"#{slug}.md"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# True only when `abs` is an existing `.md` file inside DOCS_ROOT (blocks `../` traversal).
|
|
59
|
+
def safe_doc_path?(abs)
|
|
60
|
+
root = DOCS_ROOT.to_s
|
|
61
|
+
abs.start_with?("#{root}/") && abs.end_with?(".md") && File.file?(abs)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def slug_for(rel)
|
|
65
|
+
rel.delete_suffix(".md")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# ---- Markdown ----------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def render_markdown(text, current_dir:)
|
|
71
|
+
load_markdown_deps!
|
|
72
|
+
|
|
73
|
+
# No `syntax_highlighter`: kramdown still emits `<pre><code class="language-…">` for fenced
|
|
74
|
+
# blocks (styled by the view's CSS). We skip Rouge on purpose — its kramdown integration uses
|
|
75
|
+
# a deprecated formatter that warns once per code block, flooding the dev log.
|
|
76
|
+
html = Kramdown::Document.new(text, input: "GFM", hard_wrap: false, auto_ids: true).to_html
|
|
77
|
+
rewrite_internal_links(html, current_dir)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def load_markdown_deps!
|
|
81
|
+
require "kramdown"
|
|
82
|
+
require "kramdown-parser-gfm"
|
|
83
|
+
rescue LoadError
|
|
84
|
+
raise MissingDocsDependency, "kramdown"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Rewrites relative `*.md` links so they point at the docs route instead of 404-ing.
|
|
88
|
+
# Absolute (http), anchor-only, and mailto links are left untouched.
|
|
89
|
+
def rewrite_internal_links(html, current_dir)
|
|
90
|
+
fragment = Nokogiri::HTML5.fragment(html)
|
|
91
|
+
|
|
92
|
+
fragment.css("a[href]").each do |node|
|
|
93
|
+
href = node["href"]
|
|
94
|
+
next if href.blank? || href.match?(%r{\A(?:[a-z]+:|//|/|#)})
|
|
95
|
+
|
|
96
|
+
path, anchor = href.split("#", 2)
|
|
97
|
+
next unless path&.end_with?(".md")
|
|
98
|
+
|
|
99
|
+
slug = resolve_link_slug(path, current_dir)
|
|
100
|
+
new_href = doc_href(slug)
|
|
101
|
+
new_href += "##{anchor}" if anchor.present?
|
|
102
|
+
node["href"] = new_href
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
fragment.to_html
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Resolves a relative `.md` link (e.g. `../fields/overview.md`) against the current doc's
|
|
109
|
+
# directory, returning a clean `docs/`-relative slug.
|
|
110
|
+
def resolve_link_slug(path, current_dir)
|
|
111
|
+
base = current_dir == "." ? "/" : "/#{current_dir}"
|
|
112
|
+
File.expand_path(path, base).delete_prefix("/").delete_suffix(".md")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def doc_href(slug)
|
|
116
|
+
slug == "README" ? docs_path : doc_path(path: slug)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# ---- Navigation --------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def nav_groups
|
|
122
|
+
entries = Dir.glob(DOCS_ROOT.join("**/*.md")).sort.map do |abs|
|
|
123
|
+
rel = Pathname.new(abs).relative_path_from(DOCS_ROOT).to_s
|
|
124
|
+
slug = slug_for(rel)
|
|
125
|
+
{
|
|
126
|
+
slug: slug,
|
|
127
|
+
title: doc_title(File.read(abs)) || humanize_slug(slug),
|
|
128
|
+
href: doc_href(slug),
|
|
129
|
+
group: rel.include?("/") ? rel.split("/").first : nil
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
root = entries.select { |e| e[:group].nil? }
|
|
134
|
+
grouped = entries.reject { |e| e[:group].nil? }.group_by { |e| e[:group] }
|
|
135
|
+
|
|
136
|
+
result = []
|
|
137
|
+
result << {label: nil, items: root} if root.any?
|
|
138
|
+
grouped.sort.each { |dir, items| result << {label: humanize_slug(dir), items: items} }
|
|
139
|
+
result
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# ---- Helpers -----------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
# The first level-1 heading of a Markdown document, if any.
|
|
145
|
+
def doc_title(source)
|
|
146
|
+
source[/^\#\s+(.+)$/, 1]&.strip
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def humanize_slug(slug)
|
|
150
|
+
slug.split("/").last.tr("-_", " ").split.map(&:capitalize).join(" ")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def dependency_help_html(gem_name)
|
|
154
|
+
<<~HTML
|
|
155
|
+
<h1>Documentation viewer needs a Markdown renderer</h1>
|
|
156
|
+
<p>Add these to your app's <code>:development</code> group and <code>bundle install</code>:</p>
|
|
157
|
+
<pre><code>gem "kramdown"
|
|
158
|
+
gem "kramdown-parser-gfm"</code></pre>
|
|
159
|
+
<p>(Missing: <code>#{ERB::Util.html_escape(gem_name)}</code>.)</p>
|
|
160
|
+
HTML
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Raised when the Markdown gems aren't installed in the host app.
|
|
164
|
+
class MissingDocsDependency < StandardError; end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
class HomeController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
resources = RubyUIAdmin.resource_manager.navigation_resources
|
|
7
|
+
|
|
8
|
+
# Redirect to the first resource's index, or the configured home path.
|
|
9
|
+
if (home = RubyUIAdmin.configuration.home_path)
|
|
10
|
+
redirect_to home
|
|
11
|
+
elsif resources.any?
|
|
12
|
+
redirect_to helpers.public_send("resources_#{resources.first.route_key}_path")
|
|
13
|
+
else
|
|
14
|
+
render Views::Home.new(resources: resources)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|