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,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
# Central configuration object, configured through `RubyUIAdmin.configure`.
|
|
5
|
+
class Configuration
|
|
6
|
+
# Branding / app
|
|
7
|
+
attr_accessor :app_name
|
|
8
|
+
attr_accessor :root_path
|
|
9
|
+
attr_accessor :home_path
|
|
10
|
+
attr_accessor :timezone
|
|
11
|
+
attr_accessor :locale
|
|
12
|
+
|
|
13
|
+
# Pagination
|
|
14
|
+
attr_accessor :per_page
|
|
15
|
+
attr_accessor :per_page_steps
|
|
16
|
+
|
|
17
|
+
# Authentication
|
|
18
|
+
attr_writer :current_user_method
|
|
19
|
+
attr_writer :authenticate_with
|
|
20
|
+
# Name of a host route helper (resolved via `main_app`) for signing out. When set, a
|
|
21
|
+
# sign-out item renders at the bottom of the sidebar. e.g. :destroy_user_session_path
|
|
22
|
+
attr_accessor :sign_out_path_name
|
|
23
|
+
# HTTP method used by the sign-out item (Devise uses :delete).
|
|
24
|
+
attr_accessor :sign_out_method
|
|
25
|
+
|
|
26
|
+
# Authorization
|
|
27
|
+
attr_accessor :authorization_client
|
|
28
|
+
attr_accessor :explicit_authorization
|
|
29
|
+
attr_accessor :raise_error_on_missing_policy
|
|
30
|
+
|
|
31
|
+
# Ability class for the CanCanCan adapter (`authorization_client = :cancancan`). A class or its
|
|
32
|
+
# name; defaults to the conventional `Ability`. Ignored by other adapters.
|
|
33
|
+
attr_accessor :cancancan_ability_class
|
|
34
|
+
|
|
35
|
+
# Resource discovery
|
|
36
|
+
attr_accessor :resources
|
|
37
|
+
attr_accessor :model_resource_mapping
|
|
38
|
+
|
|
39
|
+
# Menus
|
|
40
|
+
attr_writer :main_menu
|
|
41
|
+
attr_writer :profile_menu
|
|
42
|
+
|
|
43
|
+
# When true (default), the layout loads the bundled JS (tabs, dialogs). The admin
|
|
44
|
+
# works without it (progressive enhancement); set to false to opt out.
|
|
45
|
+
attr_accessor :javascript
|
|
46
|
+
|
|
47
|
+
# Head assets (stylesheet + JS tags) for the admin layout. The admin renders the host's
|
|
48
|
+
# RubyUI components, so the host owns the CSS (Tailwind build) and JS (bundler/importmap that
|
|
49
|
+
# registers the RubyUI Stimulus controllers + ours). Set this to a proc that returns the
|
|
50
|
+
# `<head>` asset markup; it's evaluated in the Rails view context, so it can call helpers like
|
|
51
|
+
# `stylesheet_link_tag` / `javascript_importmap_tags`. When nil, the layout renders no assets.
|
|
52
|
+
attr_accessor :head_assets
|
|
53
|
+
|
|
54
|
+
# When true, clicking an index row navigates to the record's show page (needs JS).
|
|
55
|
+
attr_accessor :click_row_to_view_record
|
|
56
|
+
|
|
57
|
+
# When true, non-active tabs on the show view load their content lazily (fetched via JS
|
|
58
|
+
# when the tab is first opened, with a spinner) instead of all rendering up front. Needs JS;
|
|
59
|
+
# without JS the active tab still renders and the others show a "requires JavaScript" notice.
|
|
60
|
+
attr_accessor :lazy_tabs
|
|
61
|
+
|
|
62
|
+
# Controls the in-app docs browser mounted at `<mount>/docs` (renders the gem's `docs/*.md`).
|
|
63
|
+
# Accepts:
|
|
64
|
+
# :local (default) — mounted in development/test only
|
|
65
|
+
# true — mounted in all environments, including production (where it sits behind
|
|
66
|
+
# the admin authentication gate; locally it stays open)
|
|
67
|
+
# false — never mounted
|
|
68
|
+
# a callable — evaluated at boot/route-draw time; truthy enables it
|
|
69
|
+
# Needs `kramdown` + `kramdown-parser-gfm` available in the running environment's bundle.
|
|
70
|
+
attr_accessor :docs_enabled
|
|
71
|
+
|
|
72
|
+
# Global pagination options, e.g. `{ type: :countless }` (or a proc returning it) to
|
|
73
|
+
# paginate without a COUNT query across all resources. Per-resource `self.countless`
|
|
74
|
+
# can still opt in individually.
|
|
75
|
+
attr_writer :pagination
|
|
76
|
+
|
|
77
|
+
def initialize
|
|
78
|
+
@app_name = "RubyUI Admin"
|
|
79
|
+
@root_path = "/admin"
|
|
80
|
+
@home_path = nil
|
|
81
|
+
@timezone = "UTC"
|
|
82
|
+
@locale = nil
|
|
83
|
+
|
|
84
|
+
@per_page = 24
|
|
85
|
+
@per_page_steps = [12, 24, 48, 72]
|
|
86
|
+
|
|
87
|
+
@current_user_method = nil
|
|
88
|
+
@true_user_method = nil
|
|
89
|
+
@authenticate_with = nil
|
|
90
|
+
@sign_out_path_name = nil
|
|
91
|
+
@sign_out_method = :delete
|
|
92
|
+
|
|
93
|
+
@authorization_client = :action_policy
|
|
94
|
+
@explicit_authorization = false
|
|
95
|
+
@raise_error_on_missing_policy = false
|
|
96
|
+
@cancancan_ability_class = nil
|
|
97
|
+
|
|
98
|
+
@resources = nil
|
|
99
|
+
@model_resource_mapping = {}
|
|
100
|
+
|
|
101
|
+
@main_menu = nil
|
|
102
|
+
@profile_menu = nil
|
|
103
|
+
|
|
104
|
+
@javascript = true
|
|
105
|
+
@head_assets = nil
|
|
106
|
+
@click_row_to_view_record = false
|
|
107
|
+
@lazy_tabs = false
|
|
108
|
+
@docs_enabled = :local
|
|
109
|
+
@pagination = {}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Resolved pagination options (calls the proc form if given).
|
|
113
|
+
def pagination
|
|
114
|
+
@pagination.respond_to?(:call) ? @pagination.call : (@pagination || {})
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Whether pagination should run without a COUNT query globally.
|
|
118
|
+
def countless_pagination?
|
|
119
|
+
pagination[:type].to_s == "countless"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Resolve the current user from the host app context (usually a controller).
|
|
123
|
+
def current_user_method(&block)
|
|
124
|
+
if block_given?
|
|
125
|
+
@current_user_method = block
|
|
126
|
+
else
|
|
127
|
+
@current_user_method
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Resolve the real (non-impersonated) user. Defaults to `current_user` when unset.
|
|
132
|
+
def true_user_method(&block)
|
|
133
|
+
if block_given?
|
|
134
|
+
@true_user_method = block
|
|
135
|
+
else
|
|
136
|
+
@true_user_method
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def authenticate_with(&block)
|
|
141
|
+
if block_given?
|
|
142
|
+
@authenticate_with = block
|
|
143
|
+
else
|
|
144
|
+
@authenticate_with
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def main_menu(&block)
|
|
149
|
+
if block_given?
|
|
150
|
+
@main_menu = block
|
|
151
|
+
else
|
|
152
|
+
@main_menu
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def profile_menu(&block)
|
|
157
|
+
if block_given?
|
|
158
|
+
@profile_menu = block
|
|
159
|
+
else
|
|
160
|
+
@profile_menu
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def authorization_enabled?
|
|
165
|
+
!authorization_client.nil?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Whether the in-app docs browser is available in the current environment.
|
|
169
|
+
def docs_enabled?
|
|
170
|
+
case docs_enabled
|
|
171
|
+
when nil, :local then Rails.env.local?
|
|
172
|
+
when Proc then !!docs_enabled.call
|
|
173
|
+
else !!docs_enabled
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/current_attributes"
|
|
4
|
+
|
|
5
|
+
module RubyUIAdmin
|
|
6
|
+
# Thread/request-scoped state, accessible globally as RubyUIAdmin::Current.user etc.
|
|
7
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
8
|
+
attribute :user
|
|
9
|
+
attribute :true_user
|
|
10
|
+
attribute :request
|
|
11
|
+
attribute :view_context
|
|
12
|
+
attribute :resource_manager
|
|
13
|
+
|
|
14
|
+
def params
|
|
15
|
+
request.params
|
|
16
|
+
rescue
|
|
17
|
+
{}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/class/subclasses"
|
|
4
|
+
|
|
5
|
+
module RubyUIAdmin
|
|
6
|
+
# Discovers dashboards defined under RubyUIAdmin::Dashboards in
|
|
7
|
+
# app/ruby_ui_admin/dashboards.
|
|
8
|
+
class DashboardManager
|
|
9
|
+
def boot
|
|
10
|
+
@dashboards = nil
|
|
11
|
+
self
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def dashboards
|
|
15
|
+
@dashboards ||= fetch_dashboards
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def find(id)
|
|
19
|
+
dashboards.find { |dashboard| dashboard.id == id.to_s }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def any?
|
|
23
|
+
dashboards.any?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def fetch_dashboards
|
|
29
|
+
eager_load_dashboards
|
|
30
|
+
|
|
31
|
+
# Dedupe by name and re-resolve the constant: in development, `descendants` retains
|
|
32
|
+
# stale class versions from previous Zeitwerk reloads (same name), which would show
|
|
33
|
+
# duplicates. Resolving by name keeps only the current class.
|
|
34
|
+
RubyUIAdmin::BaseDashboard.descendants
|
|
35
|
+
.select { |dashboard| !dashboard.abstract? && dashboard.to_s.start_with?("RubyUIAdmin::Dashboards::") }
|
|
36
|
+
.map(&:to_s).uniq
|
|
37
|
+
.filter_map { |name| name.safe_constantize }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def eager_load_dashboards
|
|
41
|
+
return unless defined?(::Rails)
|
|
42
|
+
|
|
43
|
+
dir = Rails.root.join("app", "ruby_ui_admin", "dashboards")
|
|
44
|
+
return unless Dir.exist?(dir)
|
|
45
|
+
|
|
46
|
+
loader = Rails.autoloaders.main
|
|
47
|
+
loader.eager_load_dir(dir) if loader.respond_to?(:eager_load_dir)
|
|
48
|
+
rescue NameError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
require "phlex-rails"
|
|
5
|
+
require "action_policy"
|
|
6
|
+
require "pagy"
|
|
7
|
+
# Loaded at engine boot (before host initializers) so hosts that freeze `Pagy::DEFAULT`
|
|
8
|
+
# don't hit a FrozenError when this extra is first required.
|
|
9
|
+
require "pagy/extras/countless"
|
|
10
|
+
require "turbo-rails"
|
|
11
|
+
require "tailwind_merge"
|
|
12
|
+
|
|
13
|
+
module RubyUIAdmin
|
|
14
|
+
# URL prefix where the bundled stylesheet is served (outside the engine mount).
|
|
15
|
+
ASSETS_MOUNT_PATH = "/ruby-ui-admin-assets"
|
|
16
|
+
|
|
17
|
+
class Engine < ::Rails::Engine
|
|
18
|
+
isolate_namespace RubyUIAdmin
|
|
19
|
+
|
|
20
|
+
# Register inflections as early as possible, before autoload paths are set.
|
|
21
|
+
# The `UI` acronym is needed by BOTH the autoloader (so `ruby_ui_admin/` and
|
|
22
|
+
# `ui/` map to RubyUIAdmin / UI) AND by Rails routing/controller resolution
|
|
23
|
+
# (which camelizes "ruby_ui_admin/resources" via ActiveSupport::Inflector).
|
|
24
|
+
initializer "ruby_ui_admin.inflections", before: :set_autoload_paths do
|
|
25
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
26
|
+
inflect.acronym "UI"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Rails.autoloaders.each do |loader|
|
|
30
|
+
loader.inflector.inflect(
|
|
31
|
+
"ruby_ui_admin" => "RubyUIAdmin",
|
|
32
|
+
"ui" => "UI",
|
|
33
|
+
"dsl" => "DSL"
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Discover host-defined resources/actions/filters/policies in `app/ruby_ui_admin`,
|
|
39
|
+
# namespaced under RubyUIAdmin.
|
|
40
|
+
initializer "ruby_ui_admin.autoload" do |app|
|
|
41
|
+
host_dir = Rails.root.join("app", "ruby_ui_admin").to_s
|
|
42
|
+
|
|
43
|
+
ActiveSupport::Dependencies.autoload_paths.delete(host_dir)
|
|
44
|
+
|
|
45
|
+
if Dir.exist?(host_dir)
|
|
46
|
+
Rails.autoloaders.main.push_dir(host_dir, namespace: RubyUIAdmin)
|
|
47
|
+
app.config.watchable_dirs[host_dir] = [:rb]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Reboot the registry whenever the app reloads in development.
|
|
52
|
+
config.to_prepare do
|
|
53
|
+
RubyUIAdmin.reset_resource_manager
|
|
54
|
+
RubyUIAdmin.boot
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Provide a `mount_ruby_ui_admin` routing helper.
|
|
58
|
+
initializer "ruby_ui_admin.routing" do
|
|
59
|
+
ActionDispatch::Routing::Mapper.include(Module.new {
|
|
60
|
+
def mount_ruby_ui_admin(at: RubyUIAdmin.configuration.root_path, **options, &block)
|
|
61
|
+
mount RubyUIAdmin::Engine, at:, as: "ruby_ui_admin", **options
|
|
62
|
+
|
|
63
|
+
if block
|
|
64
|
+
scope at do
|
|
65
|
+
RubyUIAdmin::Engine.routes.draw(&block)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
})
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Serve the bundled, precompiled stylesheet at a fixed URL, independent of the
|
|
73
|
+
# host's asset pipeline.
|
|
74
|
+
config.app_middleware.use(
|
|
75
|
+
Rack::Static,
|
|
76
|
+
urls: [RubyUIAdmin::ASSETS_MOUNT_PATH],
|
|
77
|
+
root: RubyUIAdmin::Engine.root.join("public").to_s
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
# Evaluates a value that may be a literal or a Proc. When it's a Proc, it's
|
|
5
|
+
# instance_exec'd with all the passed keyword arguments available as readers.
|
|
6
|
+
#
|
|
7
|
+
# ExecutionContext.new(target: -> { record.name }, record: post).handle
|
|
8
|
+
class ExecutionContext
|
|
9
|
+
def initialize(**args)
|
|
10
|
+
@target = args.delete(:target)
|
|
11
|
+
# When present, unknown methods (view/url helpers like `link_to`, `main_app`,
|
|
12
|
+
# `*_path`) are delegated to this Rails view context inside the block.
|
|
13
|
+
@view_context = args.delete(:view_context)
|
|
14
|
+
@args = args
|
|
15
|
+
args.each do |key, value|
|
|
16
|
+
define_singleton_method(key) { value }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def handle
|
|
21
|
+
return @target unless @target.respond_to?(:call)
|
|
22
|
+
|
|
23
|
+
instance_exec(&@target)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
27
|
+
return super unless @view_context.respond_to?(name)
|
|
28
|
+
|
|
29
|
+
@view_context.public_send(name, *args, **kwargs, &block)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def respond_to_missing?(name, include_private = false)
|
|
33
|
+
(@view_context&.respond_to?(name)) || super
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Fields
|
|
5
|
+
# Base for has_one / has_many / has_and_belongs_to_many fields. Association
|
|
6
|
+
# fields are shown on the show view only by default.
|
|
7
|
+
class AssociationField < BaseField
|
|
8
|
+
def default_hidden_views
|
|
9
|
+
%i[index new edit]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# The association read from the record — `for_attribute:` overrides the field id,
|
|
13
|
+
# e.g. `field :discussion, as: :has_many, for_attribute: :comments`.
|
|
14
|
+
def association_name
|
|
15
|
+
options[:for_attribute] || id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Read the association (honoring `for_attribute:`); a computed block still wins.
|
|
19
|
+
def value(record, view_context: nil)
|
|
20
|
+
return super if block
|
|
21
|
+
|
|
22
|
+
record&.respond_to?(association_name) ? record.public_send(association_name) : nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reflection
|
|
26
|
+
resource&.model_class&.reflect_on_association(association_name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def target_model
|
|
30
|
+
options[:model] || reflection&.klass
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Resource to link associated records through (`use_resource:` overrides the default
|
|
34
|
+
# model→resource lookup done by the view).
|
|
35
|
+
def use_resource
|
|
36
|
+
options[:use_resource]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Applies a `scope:` (Symbol naming a relation method, or a lambda with `query`) to a
|
|
40
|
+
# relation value. No-op for nil or no scope.
|
|
41
|
+
def scoped(relation)
|
|
42
|
+
scope = options[:scope]
|
|
43
|
+
return relation if relation.nil? || scope.nil?
|
|
44
|
+
return relation.public_send(scope) if scope.is_a?(Symbol)
|
|
45
|
+
|
|
46
|
+
ExecutionContext.new(target: scope, query: relation, resource: resource).handle
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def display_label(record)
|
|
50
|
+
return nil if record.nil?
|
|
51
|
+
|
|
52
|
+
if record.respond_to?(:to_label)
|
|
53
|
+
record.to_label
|
|
54
|
+
elsif record.respond_to?(:name)
|
|
55
|
+
record.name
|
|
56
|
+
elsif record.respond_to?(:title)
|
|
57
|
+
record.title
|
|
58
|
+
else
|
|
59
|
+
"#{record.model_name.human} ##{record.id}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Association values aren't writable through a simple setter here.
|
|
64
|
+
def fill_value(record, value); end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Fields
|
|
5
|
+
# Renders the value as a colored badge. Map values to variants with
|
|
6
|
+
# `options: { active: :success, pending: :warning, ... }`.
|
|
7
|
+
class BadgeField < BaseField
|
|
8
|
+
register_as :badge
|
|
9
|
+
|
|
10
|
+
DEFAULT_VARIANT = :gray
|
|
11
|
+
|
|
12
|
+
def variant_for(record)
|
|
13
|
+
mapping = options[:options] || {}
|
|
14
|
+
key = value(record)
|
|
15
|
+
(mapping[key] || mapping[key.to_s] || mapping[key.to_s.to_sym] || DEFAULT_VARIANT).to_sym
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUIAdmin
|
|
4
|
+
module Fields
|
|
5
|
+
# Base class for all field types. Fields are pure data/metadata objects; the
|
|
6
|
+
# actual rendering lives in Phlex components (app/components/ruby_ui_admin/fields).
|
|
7
|
+
class BaseField
|
|
8
|
+
attr_reader :id, :options, :block
|
|
9
|
+
attr_accessor :resource
|
|
10
|
+
|
|
11
|
+
# Registers the field under a DSL symbol, e.g. `register_as :text`.
|
|
12
|
+
def self.register_as(key)
|
|
13
|
+
@field_type = key.to_sym
|
|
14
|
+
Fields.register(key, self)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.field_type
|
|
18
|
+
@field_type
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(id, resource: nil, **options, &block)
|
|
22
|
+
@id = id.to_sym
|
|
23
|
+
@resource = resource
|
|
24
|
+
@options = options
|
|
25
|
+
@block = block
|
|
26
|
+
|
|
27
|
+
@name = options[:name]
|
|
28
|
+
@required = options.fetch(:required, false)
|
|
29
|
+
@readonly = options.fetch(:readonly, false)
|
|
30
|
+
@sortable = options.fetch(:sortable, false)
|
|
31
|
+
@filterable = options.fetch(:filterable, false)
|
|
32
|
+
@link_to_record = options.fetch(:link_to_record, false)
|
|
33
|
+
@format_using = options[:format_using]
|
|
34
|
+
@help = options[:help]
|
|
35
|
+
@description = options[:description]
|
|
36
|
+
@visible = options[:visible]
|
|
37
|
+
@placeholder = options[:placeholder]
|
|
38
|
+
@default = options[:default]
|
|
39
|
+
@only_on = normalize_views(options[:only_on])
|
|
40
|
+
@hide_on = normalize_views(options[:hide_on])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def type
|
|
44
|
+
self.class.field_type
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def name
|
|
48
|
+
return @name if @name
|
|
49
|
+
|
|
50
|
+
model = resource&.model_class
|
|
51
|
+
# Uses the model's i18n attribute name (activerecord.attributes.*),
|
|
52
|
+
# falling back to a humanized id.
|
|
53
|
+
model.respond_to?(:human_attribute_name) ? model.human_attribute_name(id) : id.to_s.humanize
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def help = @help
|
|
57
|
+
def description = @description
|
|
58
|
+
def placeholder = @placeholder
|
|
59
|
+
def required? = !!@required
|
|
60
|
+
def readonly? = !!@readonly
|
|
61
|
+
def sortable? = !!@sortable
|
|
62
|
+
def filterable? = !!@filterable
|
|
63
|
+
def link_to_record? = !!@link_to_record
|
|
64
|
+
|
|
65
|
+
# The underlying attribute name on the model (overridable by subclasses,
|
|
66
|
+
# e.g. belongs_to appends `_id`).
|
|
67
|
+
def database_id
|
|
68
|
+
id
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Raw value read from the record (or computed via a block). When a `view_context`
|
|
72
|
+
# is given, the computed block can use view/url helpers (`link_to`, `main_app`, …).
|
|
73
|
+
def value(record, view_context: nil)
|
|
74
|
+
return nil if record.nil?
|
|
75
|
+
|
|
76
|
+
if block
|
|
77
|
+
ExecutionContext.new(
|
|
78
|
+
target: block,
|
|
79
|
+
record: record,
|
|
80
|
+
resource: resource,
|
|
81
|
+
current_user: resource&.user,
|
|
82
|
+
view_context: view_context
|
|
83
|
+
).handle
|
|
84
|
+
elsif record.respond_to?(id)
|
|
85
|
+
record.public_send(id)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Value formatted for display.
|
|
90
|
+
def formatted_value(record, view_context: nil)
|
|
91
|
+
raw = value(record, view_context: view_context)
|
|
92
|
+
return raw if @format_using.nil?
|
|
93
|
+
|
|
94
|
+
ExecutionContext.new(
|
|
95
|
+
target: @format_using,
|
|
96
|
+
value: raw,
|
|
97
|
+
record: record,
|
|
98
|
+
resource: resource,
|
|
99
|
+
current_user: resource&.user,
|
|
100
|
+
view_context: view_context
|
|
101
|
+
).handle
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Assigns a submitted value back onto the record.
|
|
105
|
+
def fill_value(record, value)
|
|
106
|
+
setter = "#{database_id}="
|
|
107
|
+
record.public_send(setter, value) if record.respond_to?(setter)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Which form param this field reads/writes.
|
|
111
|
+
def permitted_param
|
|
112
|
+
database_id
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The strong-parameters fragment for this field (scalar by default; fields with
|
|
116
|
+
# hash/array values override, e.g. boolean_group -> { name => {} }, files -> { name => [] }).
|
|
117
|
+
def permit_param
|
|
118
|
+
permitted_param
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# All strong-params fragments this field contributes. Defaults to a single `permit_param`;
|
|
122
|
+
# fields that read extra inputs (e.g. a file field's remove checkbox) return more.
|
|
123
|
+
def permit_params
|
|
124
|
+
[permit_param]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Applies the submitted (permitted) `attributes` to the record for this field. Default just
|
|
128
|
+
# assigns `permitted_param`; fields with extra inputs override (e.g. file remove).
|
|
129
|
+
def fill(record, attributes)
|
|
130
|
+
key = permitted_param.to_s
|
|
131
|
+
fill_value(record, attributes[key]) if attributes.key?(key)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def has_default?
|
|
135
|
+
!@default.nil?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The field's default for a new record (literal or a proc evaluated with
|
|
139
|
+
# `record`, `resource` and `current_user` available).
|
|
140
|
+
def default_value(record = nil)
|
|
141
|
+
return @default unless @default.respond_to?(:call)
|
|
142
|
+
|
|
143
|
+
ExecutionContext.new(
|
|
144
|
+
target: @default,
|
|
145
|
+
record: record,
|
|
146
|
+
resource: resource,
|
|
147
|
+
current_user: resource&.user
|
|
148
|
+
).handle
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Conditional visibility (`visible: -> { current_user.admin? }`), evaluated with
|
|
152
|
+
# `view`/`record`/`resource`/`current_user`. A literal is used as-is. Defaults to visible.
|
|
153
|
+
def visible?(view: nil, record: nil)
|
|
154
|
+
return true if @visible.nil?
|
|
155
|
+
|
|
156
|
+
result =
|
|
157
|
+
if @visible.respond_to?(:call)
|
|
158
|
+
ExecutionContext.new(
|
|
159
|
+
target: @visible,
|
|
160
|
+
view: View.wrap(view),
|
|
161
|
+
record: record,
|
|
162
|
+
resource: resource,
|
|
163
|
+
current_user: resource&.user
|
|
164
|
+
).handle
|
|
165
|
+
else
|
|
166
|
+
@visible
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
!!result
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def visible_in_view?(view)
|
|
173
|
+
view = view.to_sym
|
|
174
|
+
return @only_on.include?(view) if @only_on
|
|
175
|
+
return !@hide_on.include?(view) if @hide_on
|
|
176
|
+
|
|
177
|
+
!default_hidden_views.include?(view)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Views where a field type is hidden unless `only_on`/`hide_on` say otherwise.
|
|
181
|
+
# Overridden by heavy/association fields (e.g. has_many hides everywhere but show).
|
|
182
|
+
def default_hidden_views
|
|
183
|
+
[]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Whether the field can be sorted, and the column/expression to sort by.
|
|
187
|
+
# `sortable: true` sorts by the database column; `sortable: ->{...}` is custom.
|
|
188
|
+
def sort_lambda
|
|
189
|
+
@sortable.respond_to?(:call) ? @sortable : nil
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
def normalize_views(value)
|
|
195
|
+
return nil if value.nil?
|
|
196
|
+
|
|
197
|
+
# `:forms` = new+edit; `:display` = index+show (view groups).
|
|
198
|
+
Array(value).flat_map do |v|
|
|
199
|
+
case v.to_sym
|
|
200
|
+
when :forms then %i[new edit]
|
|
201
|
+
when :display then %i[index show]
|
|
202
|
+
else v.to_sym
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|