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.
Files changed (182) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +165 -0
  4. data/app/assets/stylesheets/ruby_ui_admin/application.tailwind.css +111 -0
  5. data/app/components/ruby_ui_admin/ui/base.rb +26 -0
  6. data/app/components/ruby_ui_admin/ui/icon.rb +79 -0
  7. data/app/components/ruby_ui_admin/ui/native_select.rb +37 -0
  8. data/app/components/ruby_ui_admin/ui/native_select_group.rb +18 -0
  9. data/app/components/ruby_ui_admin/ui/native_select_icon.rb +42 -0
  10. data/app/components/ruby_ui_admin/ui/native_select_option.rb +18 -0
  11. data/app/components/ruby_ui_admin/ui/pagination.rb +62 -0
  12. data/app/components/ruby_ui_admin/ui/pagination_content.rb +18 -0
  13. data/app/components/ruby_ui_admin/ui/pagination_ellipsis.rb +45 -0
  14. data/app/components/ruby_ui_admin/ui/pagination_item.rb +32 -0
  15. data/app/components/ruby_ui_admin/ui/select.rb +29 -0
  16. data/app/components/ruby_ui_admin/ui/toast_action.rb +30 -0
  17. data/app/components/ruby_ui_admin/ui/toast_cancel.rb +30 -0
  18. data/app/components/ruby_ui_admin/ui/toast_close.rb +43 -0
  19. data/app/components/ruby_ui_admin/ui/toast_description.rb +21 -0
  20. data/app/components/ruby_ui_admin/ui/toast_icon.rb +69 -0
  21. data/app/components/ruby_ui_admin/ui/toast_item.rb +75 -0
  22. data/app/components/ruby_ui_admin/ui/toast_region.rb +147 -0
  23. data/app/components/ruby_ui_admin/ui/toast_title.rb +21 -0
  24. data/app/components/ruby_ui_admin/ui.rb +12 -0
  25. data/app/components/ruby_ui_admin/views/action.rb +37 -0
  26. data/app/components/ruby_ui_admin/views/action_form.rb +104 -0
  27. data/app/components/ruby_ui_admin/views/action_trigger.rb +148 -0
  28. data/app/components/ruby_ui_admin/views/actions_menu.rb +34 -0
  29. data/app/components/ruby_ui_admin/views/base.rb +301 -0
  30. data/app/components/ruby_ui_admin/views/card_component.rb +69 -0
  31. data/app/components/ruby_ui_admin/views/dashboard.rb +40 -0
  32. data/app/components/ruby_ui_admin/views/docs.rb +91 -0
  33. data/app/components/ruby_ui_admin/views/field_input.rb +156 -0
  34. data/app/components/ruby_ui_admin/views/field_value.rb +301 -0
  35. data/app/components/ruby_ui_admin/views/filter_bar.rb +111 -0
  36. data/app/components/ruby_ui_admin/views/form.rb +151 -0
  37. data/app/components/ruby_ui_admin/views/home.rb +35 -0
  38. data/app/components/ruby_ui_admin/views/index.rb +324 -0
  39. data/app/components/ruby_ui_admin/views/path_helpers.rb +36 -0
  40. data/app/components/ruby_ui_admin/views/rails_helpers.rb +15 -0
  41. data/app/components/ruby_ui_admin/views/show.rb +77 -0
  42. data/app/components/ruby_ui_admin/views/show_fields.rb +37 -0
  43. data/app/components/ruby_ui_admin/views/show_tab.rb +58 -0
  44. data/app/components/ruby_ui_admin/views/structure_renderer.rb +148 -0
  45. data/app/components/ruby_ui_admin/views/translation.rb +12 -0
  46. data/app/controllers/ruby_ui_admin/actions_controller.rb +162 -0
  47. data/app/controllers/ruby_ui_admin/application_controller.rb +81 -0
  48. data/app/controllers/ruby_ui_admin/dashboards_controller.rb +12 -0
  49. data/app/controllers/ruby_ui_admin/docs_controller.rb +166 -0
  50. data/app/controllers/ruby_ui_admin/home_controller.rb +18 -0
  51. data/app/controllers/ruby_ui_admin/resources_controller.rb +259 -0
  52. data/config/locales/ruby_ui_admin.en.yml +59 -0
  53. data/config/locales/ruby_ui_admin.pt-BR.yml +59 -0
  54. data/config/routes/dynamic_routes.rb +11 -0
  55. data/config/routes.rb +23 -0
  56. data/docs/README.md +53 -0
  57. data/docs/actions/overview.md +112 -0
  58. data/docs/authorization/action-policy.md +72 -0
  59. data/docs/authorization/adapters.md +101 -0
  60. data/docs/authorization/explicit-mode.md +45 -0
  61. data/docs/authorization/field-authorization.md +53 -0
  62. data/docs/authorization/policies.md +62 -0
  63. data/docs/authorization/scopes.md +48 -0
  64. data/docs/customization/controllers.md +62 -0
  65. data/docs/customization/ejecting.md +55 -0
  66. data/docs/customization/javascript.md +161 -0
  67. data/docs/customization/theming-rubyui.md +80 -0
  68. data/docs/dashboards/overview.md +65 -0
  69. data/docs/fields/overview.md +94 -0
  70. data/docs/filters/overview.md +73 -0
  71. data/docs/generators-and-tasks/overview.md +42 -0
  72. data/docs/getting-started/authentication.md +98 -0
  73. data/docs/getting-started/configuration.md +174 -0
  74. data/docs/getting-started/installation.md +201 -0
  75. data/docs/getting-started/internationalization.md +50 -0
  76. data/docs/getting-started/practical-guide.md +310 -0
  77. data/docs/resources/associations.md +66 -0
  78. data/docs/resources/index-customization.md +68 -0
  79. data/docs/resources/overview.md +50 -0
  80. data/docs/resources/scopes.md +54 -0
  81. data/docs/resources/tabs-panels.md +65 -0
  82. data/lib/generators/ruby_ui_admin/action/action_generator.rb +21 -0
  83. data/lib/generators/ruby_ui_admin/action/templates/action.rb.tt +24 -0
  84. data/lib/generators/ruby_ui_admin/assets/assets_generator.rb +71 -0
  85. data/lib/generators/ruby_ui_admin/card/card_generator.rb +28 -0
  86. data/lib/generators/ruby_ui_admin/card/templates/chart_card.rb.tt +14 -0
  87. data/lib/generators/ruby_ui_admin/card/templates/metric_card.rb.tt +14 -0
  88. data/lib/generators/ruby_ui_admin/card/templates/partial_card.rb.tt +13 -0
  89. data/lib/generators/ruby_ui_admin/components/components_generator.rb +43 -0
  90. data/lib/generators/ruby_ui_admin/controller/controller_generator.rb +30 -0
  91. data/lib/generators/ruby_ui_admin/controller/templates/controller.rb.tt +24 -0
  92. data/lib/generators/ruby_ui_admin/dashboard/dashboard_generator.rb +18 -0
  93. data/lib/generators/ruby_ui_admin/dashboard/templates/dashboard.rb.tt +14 -0
  94. data/lib/generators/ruby_ui_admin/eject/eject_generator.rb +55 -0
  95. data/lib/generators/ruby_ui_admin/filter/filter_generator.rb +28 -0
  96. data/lib/generators/ruby_ui_admin/filter/templates/boolean_filter.rb.tt +13 -0
  97. data/lib/generators/ruby_ui_admin/filter/templates/select_filter.rb.tt +19 -0
  98. data/lib/generators/ruby_ui_admin/filter/templates/text_filter.rb.tt +13 -0
  99. data/lib/generators/ruby_ui_admin/install/install_generator.rb +62 -0
  100. data/lib/generators/ruby_ui_admin/install/templates/initializer.rb.tt +41 -0
  101. data/lib/generators/ruby_ui_admin/locales/locales_generator.rb +22 -0
  102. data/lib/generators/ruby_ui_admin/policy/policy_generator.rb +18 -0
  103. data/lib/generators/ruby_ui_admin/policy/templates/policy.rb.tt +24 -0
  104. data/lib/generators/ruby_ui_admin/resource/resource_generator.rb +84 -0
  105. data/lib/generators/ruby_ui_admin/resource/templates/resource.rb.tt +15 -0
  106. data/lib/generators/ruby_ui_admin/scope/scope_generator.rb +18 -0
  107. data/lib/generators/ruby_ui_admin/scope/templates/scope.rb.tt +11 -0
  108. data/lib/ruby_ui_admin/authorization/action_policy_adapter.rb +69 -0
  109. data/lib/ruby_ui_admin/authorization/adapter.rb +80 -0
  110. data/lib/ruby_ui_admin/authorization/can_can_can_adapter.rb +70 -0
  111. data/lib/ruby_ui_admin/authorization/pundit_adapter.rb +86 -0
  112. data/lib/ruby_ui_admin/authorization.rb +40 -0
  113. data/lib/ruby_ui_admin/base_action.rb +211 -0
  114. data/lib/ruby_ui_admin/base_dashboard.rb +70 -0
  115. data/lib/ruby_ui_admin/base_policy.rb +38 -0
  116. data/lib/ruby_ui_admin/base_resource.rb +503 -0
  117. data/lib/ruby_ui_admin/cards/base_card.rb +59 -0
  118. data/lib/ruby_ui_admin/cards/chart_card.rb +16 -0
  119. data/lib/ruby_ui_admin/cards/metric_card.rb +22 -0
  120. data/lib/ruby_ui_admin/cards/partial_card.rb +14 -0
  121. data/lib/ruby_ui_admin/configuration.rb +177 -0
  122. data/lib/ruby_ui_admin/current.rb +20 -0
  123. data/lib/ruby_ui_admin/dashboard_manager.rb +52 -0
  124. data/lib/ruby_ui_admin/engine.rb +80 -0
  125. data/lib/ruby_ui_admin/execution_context.rb +36 -0
  126. data/lib/ruby_ui_admin/fields/association_field.rb +67 -0
  127. data/lib/ruby_ui_admin/fields/badge_field.rb +19 -0
  128. data/lib/ruby_ui_admin/fields/base_field.rb +208 -0
  129. data/lib/ruby_ui_admin/fields/belongs_to_field.rb +49 -0
  130. data/lib/ruby_ui_admin/fields/boolean_field.rb +13 -0
  131. data/lib/ruby_ui_admin/fields/boolean_group_field.rb +54 -0
  132. data/lib/ruby_ui_admin/fields/code_field.rb +18 -0
  133. data/lib/ruby_ui_admin/fields/date_field.rb +34 -0
  134. data/lib/ruby_ui_admin/fields/date_time_field.rb +37 -0
  135. data/lib/ruby_ui_admin/fields/field_manager.rb +27 -0
  136. data/lib/ruby_ui_admin/fields/file_field.rb +67 -0
  137. data/lib/ruby_ui_admin/fields/files_field.rb +62 -0
  138. data/lib/ruby_ui_admin/fields/has_and_belongs_to_many_field.rb +9 -0
  139. data/lib/ruby_ui_admin/fields/has_many_field.rb +24 -0
  140. data/lib/ruby_ui_admin/fields/has_one_field.rb +13 -0
  141. data/lib/ruby_ui_admin/fields/hidden_field.rb +14 -0
  142. data/lib/ruby_ui_admin/fields/id_field.rb +19 -0
  143. data/lib/ruby_ui_admin/fields/key_value_field.rb +41 -0
  144. data/lib/ruby_ui_admin/fields/number_field.rb +13 -0
  145. data/lib/ruby_ui_admin/fields/password_field.rb +21 -0
  146. data/lib/ruby_ui_admin/fields/record_link_field.rb +45 -0
  147. data/lib/ruby_ui_admin/fields/select_field.rb +71 -0
  148. data/lib/ruby_ui_admin/fields/status_field.rb +21 -0
  149. data/lib/ruby_ui_admin/fields/text_field.rb +14 -0
  150. data/lib/ruby_ui_admin/fields/textarea_field.rb +13 -0
  151. data/lib/ruby_ui_admin/fields/url_field.rb +21 -0
  152. data/lib/ruby_ui_admin/filters/base_filter.rb +81 -0
  153. data/lib/ruby_ui_admin/filters/boolean_filter.rb +9 -0
  154. data/lib/ruby_ui_admin/filters/multiple_select_filter.rb +11 -0
  155. data/lib/ruby_ui_admin/filters/select_filter.rb +9 -0
  156. data/lib/ruby_ui_admin/filters/text_filter.rb +9 -0
  157. data/lib/ruby_ui_admin/menu/builder.rb +94 -0
  158. data/lib/ruby_ui_admin/resource_manager.rb +82 -0
  159. data/lib/ruby_ui_admin/scopes/base_scope.rb +107 -0
  160. data/lib/ruby_ui_admin/services/authorization_service.rb +38 -0
  161. data/lib/ruby_ui_admin/structure.rb +40 -0
  162. data/lib/ruby_ui_admin/tailwind_source.rb +65 -0
  163. data/lib/ruby_ui_admin/version.rb +5 -0
  164. data/lib/ruby_ui_admin/view.rb +34 -0
  165. data/lib/ruby_ui_admin.rb +110 -0
  166. data/lib/tasks/ruby_ui_admin.rake +71 -0
  167. data/public/ruby-ui-admin-assets/application.css +2 -0
  168. data/public/ruby-ui-admin-assets/controllers/index.js +36 -0
  169. data/public/ruby-ui-admin-assets/controllers/rua--bulk-select_controller.js +19 -0
  170. data/public/ruby-ui-admin-assets/controllers/rua--confirm_controller.js +51 -0
  171. data/public/ruby-ui-admin-assets/controllers/rua--dialog_controller.js +55 -0
  172. data/public/ruby-ui-admin-assets/controllers/rua--row-link_controller.js +16 -0
  173. data/public/ruby-ui-admin-assets/controllers/rua--tabs_controller.js +36 -0
  174. data/public/ruby-ui-admin-assets/controllers/ruby_ui--combobox_controller.js +197 -0
  175. data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_content_controller.js +7 -0
  176. data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_controller.js +15 -0
  177. data/public/ruby-ui-admin-assets/controllers/ruby_ui--sidebar_controller.js +67 -0
  178. data/public/ruby-ui-admin-assets/controllers/ruby_ui--toast_controller.js +151 -0
  179. data/public/ruby-ui-admin-assets/controllers/ruby_ui--toaster_controller.js +314 -0
  180. data/public/ruby-ui-admin-assets/vendor/stimulus.js +2567 -0
  181. data/public/ruby-ui-admin-assets/vendor/turbo.js +7178 -0
  182. 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