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,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module RubyUIAdmin
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ namespace "ruby_ui_admin:install"
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Creates the RubyUI Admin initializer and mounts the engine."
12
+
13
+ class_option :path, type: :string, default: "admin",
14
+ desc: "Path to mount the admin at (default: admin)"
15
+
16
+ def create_initializer
17
+ template "initializer.rb.tt", "config/initializers/ruby_ui_admin.rb"
18
+ end
19
+
20
+ def mount_engine
21
+ route %(mount_ruby_ui_admin at: "/#{options[:path]}")
22
+ end
23
+
24
+ def done
25
+ say "\nRubyUI Admin: initializer created and engine mounted.", :green
26
+ warn_if_ruby_ui_missing
27
+ say "\nThe admin renders your app's RubyUI components + assets, so finish wiring them:", :yellow
28
+ say " 1. Install RubyUI and generate its components (if you haven't). Run phlex:install"
29
+ say " FIRST — RubyUI's installer skips it when phlex-rails is present (it is, via this"
30
+ say " gem) and then aborts on a missing app/components/base.rb:"
31
+ say " bin/rails generate phlex:install"
32
+ say " bin/rails generate ruby_ui:install"
33
+ say " Then generate the components the admin needs (drives ruby_ui:component for you):"
34
+ say " bin/rails generate ruby_ui_admin:components"
35
+ say " 2. Tailwind: extract the admin's classes into your app and @source the result:"
36
+ say " bin/rails ruby_ui_admin:tailwind_source # writes app/assets/tailwind/ruby_ui_admin_classes.html"
37
+ say %( @source "./ruby_ui_admin_classes.html"; # in your Tailwind entry CSS (commit the file))
38
+ say " 3. Stimulus: copy the admin's controllers into your app with"
39
+ say " bin/rails generate ruby_ui_admin:assets"
40
+ say " then add the one import line it prints to app/javascript/application.js."
41
+ say " 4. Set config.head_assets in config/initializers/ruby_ui_admin.rb (see the comment there)."
42
+ say "\nFull guide: docs/getting-started/installation.md", :green
43
+ say "Then generate your first resource:"
44
+ say " bin/rails generate ruby_ui_admin:resource <Model>\n"
45
+ end
46
+
47
+ private
48
+
49
+ # RubyUI's installer aborts (leaving RubyUI::Base ungenerated) if it runs before
50
+ # phlex:install on an app that already has phlex-rails — which every ruby_ui_admin app does.
51
+ # Surface that here so a half-installed RubyUI doesn't show up later as a cryptic Zeitwerk error.
52
+ def warn_if_ruby_ui_missing
53
+ return if File.exist?(Rails.root.join("app/components/ruby_ui/base.rb"))
54
+
55
+ say "\n⚠️ RubyUI doesn't look fully installed (app/components/ruby_ui/base.rb is missing).", :red
56
+ say " RubyUI's `ruby_ui:install` aborts on a missing app/components/base.rb when phlex-rails", :red
57
+ say " is already present (it is, via this gem). Run `bin/rails generate phlex:install` first,", :red
58
+ say " then re-run `bin/rails generate ruby_ui:install`.", :red
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyUIAdmin.configure do |config|
4
+ # The name shown in the sidebar and page titles.
5
+ config.app_name = "<%= Rails.application.class.module_parent_name %> Admin"
6
+
7
+ # Where the engine is mounted (keep in sync with config/routes.rb).
8
+ config.root_path = "/<%= options[:path] %>"
9
+
10
+ # Records per page on index views.
11
+ config.per_page = 24
12
+
13
+ # Resolve the signed-in admin. The block runs in the controller context.
14
+ config.current_user_method { current_user }
15
+
16
+ # Optional gate run before every admin request, e.g.:
17
+ # config.authenticate_with { redirect_to main_app.login_path unless current_user&.admin? }
18
+
19
+ # Authorization adapter: :action_policy (default), :pundit, :cancancan, or a custom adapter
20
+ # class. Set to nil to disable. See docs/authorization/adapters.md.
21
+ config.authorization_client = :action_policy
22
+
23
+ # When true, rules a policy doesn't define (and missing policies) are denied.
24
+ config.explicit_authorization = false
25
+
26
+ # The admin renders your app's RubyUI components, so it uses your app's CSS + JS. Point the
27
+ # layout's <head> at them here (a proc evaluated in the Rails view context). Make sure your
28
+ # Tailwind build covers app/components/ruby_ui/** and the admin views, and that your bundler
29
+ # registers RubyUI's Stimulus controllers + the admin's rua--* controllers.
30
+ # config.head_assets = lambda do
31
+ # safe_join([
32
+ # stylesheet_link_tag("application", "data-turbo-track": "reload"),
33
+ # javascript_importmap_tags
34
+ # ])
35
+ # end
36
+
37
+ # In-app docs browser at <mount>/docs (renders the gem's docs/*.md). Defaults to :local
38
+ # (development/test only, open). Set to true to expose it in production too (behind your auth
39
+ # gate), or false to disable. Needs kramdown + kramdown-parser-gfm in the target environment.
40
+ # config.docs_enabled = :local
41
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module RubyUIAdmin
6
+ module Generators
7
+ # Copies the bundled locale files into the host app so they can be edited/extended.
8
+ class LocalesGenerator < Rails::Generators::Base
9
+ namespace "ruby_ui_admin:locales"
10
+ source_root RubyUIAdmin::Engine.root.to_s
11
+
12
+ desc "Copies RubyUI Admin's locale files into config/locales."
13
+
14
+ def copy_locales
15
+ Dir[File.join(self.class.source_root, "config", "locales", "*.yml")].each do |file|
16
+ relative = File.join("config", "locales", File.basename(file))
17
+ copy_file relative, relative
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module RubyUIAdmin
6
+ module Generators
7
+ class PolicyGenerator < Rails::Generators::NamedBase
8
+ namespace "ruby_ui_admin:policy"
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Generates a RubyUI Admin action_policy policy."
12
+
13
+ def create_policy
14
+ template "policy.rb.tt", File.join("app/ruby_ui_admin/policies", "#{file_path}_policy.rb")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Policies
5
+ class <%= class_name %>Policy < RubyUIAdmin::BasePolicy
6
+ def index? = true
7
+
8
+ def show? = true
9
+
10
+ def create? = true
11
+
12
+ def update? = true
13
+
14
+ def destroy? = true
15
+
16
+ def act_on? = true
17
+
18
+ # Scope records on the index view:
19
+ # relation_scope do |relation|
20
+ # relation
21
+ # end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module RubyUIAdmin
6
+ module Generators
7
+ class ResourceGenerator < Rails::Generators::NamedBase
8
+ namespace "ruby_ui_admin:resource"
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Generates a RubyUI Admin resource (with fields derived from the model)."
12
+
13
+ class_option :model_class, type: :string, desc: "Override the inferred model class name"
14
+
15
+ def create_resource
16
+ template "resource.rb.tt", File.join("app/ruby_ui_admin/resources", "#{singular_name}.rb")
17
+ end
18
+
19
+ private
20
+
21
+ def resource_class_name
22
+ class_name.demodulize
23
+ end
24
+
25
+ def model_class_name
26
+ options[:model_class]
27
+ end
28
+
29
+ def model_klass
30
+ (model_class_name || class_name).safe_constantize
31
+ end
32
+
33
+ # Builds the body of `def fields`, one `field ...` line per column/association.
34
+ def fields_block
35
+ indent = " "
36
+ lines = ["#{indent}field :id, as: :id"]
37
+
38
+ if (model = model_klass)
39
+ model.columns.each do |column|
40
+ next if column.name == "id" || column.name.end_with?("_id")
41
+
42
+ type = column_field_type(column)
43
+ # Keep the index lean: long-form fields (textarea/code) are noisy in a table, so show
44
+ # them only on the record's show/new/edit pages by default. Tweak or drop `only_on:`.
45
+ suffix = hidden_from_index?(type) ? ", only_on: %i[show new edit]" : ""
46
+ lines << "#{indent}field :#{column.name}, as: :#{type}#{suffix}"
47
+ end
48
+
49
+ model.reflect_on_all_associations.each do |association|
50
+ type = association_field_type(association)
51
+ lines << "#{indent}field :#{association.name}, as: :#{type}" if type
52
+ end
53
+ end
54
+
55
+ lines.join("\n")
56
+ rescue
57
+ " field :id, as: :id"
58
+ end
59
+
60
+ def column_field_type(column)
61
+ return :password if column.name.match?(/password/)
62
+ return :text if column.name == "email"
63
+
64
+ {
65
+ string: :text, text: :textarea, integer: :number, float: :number,
66
+ decimal: :number, boolean: :boolean, date: :date, datetime: :date_time,
67
+ timestamp: :date_time, json: :code, jsonb: :code
68
+ }.fetch(column.type, :text)
69
+ end
70
+
71
+ # Long-form field types are hidden from the index by default (still on show/new/edit).
72
+ def hidden_from_index?(type)
73
+ %i[textarea code].include?(type)
74
+ end
75
+
76
+ def association_field_type(association)
77
+ {
78
+ belongs_to: :belongs_to, has_one: :has_one,
79
+ has_many: :has_many, has_and_belongs_to_many: :has_and_belongs_to_many
80
+ }[association.macro]
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Resources
5
+ class <%= resource_class_name %> < RubyUIAdmin::BaseResource
6
+ <% if model_class_name -%>
7
+ self.model_class = <%= model_class_name %>
8
+
9
+ <% end -%>
10
+ def fields
11
+ <%= fields_block %>
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module RubyUIAdmin
6
+ module Generators
7
+ class ScopeGenerator < Rails::Generators::NamedBase
8
+ namespace "ruby_ui_admin:scope"
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Generates a RubyUI Admin named index scope."
12
+
13
+ def create_scope
14
+ template "scope.rb.tt", File.join("app/ruby_ui_admin/scopes", "#{file_path}.rb")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Scopes
5
+ class <%= class_name %> < RubyUIAdmin::Scopes::BaseScope
6
+ self.name = "<%= human_name %>"
7
+ self.scope = -> { query.all }
8
+ # self.default = true
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_policy"
4
+
5
+ module RubyUIAdmin
6
+ module Authorization
7
+ # action_policy-backed adapter (the default). Bridges the admin's authorization onto
8
+ # action_policy: `allowed_to?`, `authorized_scope`, `policy_for`. Rules the policy doesn't
9
+ # define fall back to its default rule (`manage?`), which `RubyUIAdmin::BasePolicy` ties to
10
+ # `explicit_authorization`; a missing policy is decided by `explicit_authorization` too.
11
+ class ActionPolicyAdapter < Adapter
12
+ include ActionPolicy::Behaviour
13
+
14
+ # Declares the authorization context required by policies (resolved via the readers on Adapter).
15
+ authorize :user
16
+ authorize :true_user
17
+
18
+ def authorize_action(rule, record: nil, raise_exception: true)
19
+ return true unless RubyUIAdmin.configuration.authorization_enabled?
20
+
21
+ target = record.nil? ? @record : record
22
+ normalized = normalize_rule(rule)
23
+
24
+ begin
25
+ result = allowed_to?(normalized, target, with: policy_class)
26
+ rescue ActionPolicy::NotFound
27
+ return handle_missing_policy(raise_exception)
28
+ end
29
+
30
+ if !result && raise_exception
31
+ raise NotAuthorizedError, "Not authorized to #{normalized} on #{target.inspect}"
32
+ end
33
+
34
+ result
35
+ end
36
+
37
+ def apply_policy(scope)
38
+ return scope unless RubyUIAdmin.configuration.authorization_enabled?
39
+
40
+ authorized_scope(scope, with: policy_class)
41
+ rescue ActionPolicy::UnknownScopeType,
42
+ ActionPolicy::UnknownNamedScope,
43
+ ActionPolicy::UnrecognizedScopeTarget,
44
+ ActionPolicy::NotFound
45
+ scope
46
+ end
47
+
48
+ def has_rule?(rule, record: nil)
49
+ target = record.nil? ? @record : record
50
+ policy = policy_for(record: target, with: policy_class, allow_nil: true)
51
+ return false if policy.nil?
52
+
53
+ policy.respond_to?(normalize_rule(rule))
54
+ rescue ActionPolicy::NotFound
55
+ false
56
+ end
57
+
58
+ # Whether the policy CLASS explicitly defines this rule as a public method (i.e. not satisfied
59
+ # only by the `manage?` default). Used for field-level authorization.
60
+ def defines_rule?(rule)
61
+ return false if policy_class.nil?
62
+
63
+ policy_class.public_method_defined?(normalize_rule(rule))
64
+ rescue
65
+ false
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Authorization
5
+ # Interface every authorization adapter implements. `Services::AuthorizationService` resolves the
6
+ # configured adapter (`config.authorization_client`) and delegates to it, so call sites stay
7
+ # backend-agnostic. An adapter bridges the admin's rule vocabulary (`index?`/`show?`/`update?`/
8
+ # `destroy?`/`act_on?`/`view_<field>?`, with the `manage?` + `explicit_authorization` fallback)
9
+ # onto a backend (action_policy, Pundit, CanCanCan, …).
10
+ #
11
+ # Subclasses must implement: #authorize_action, #apply_policy, #has_rule?, #defines_rule?.
12
+ # The shared context (#user, #true_user, #record, #policy_class), #allowed? and #normalize_rule
13
+ # live here.
14
+ class Adapter
15
+ attr_reader :user, :record, :policy_class
16
+
17
+ def initialize(user, record = nil, policy_class: nil, true_user: nil)
18
+ @user = user
19
+ @record = record
20
+ @policy_class = policy_class
21
+ @true_user = true_user
22
+ end
23
+
24
+ # The real (impersonating) user. Defaults to `Current.true_user`, falling back to `user`.
25
+ def true_user
26
+ @true_user || RubyUIAdmin::Current.true_user || @user
27
+ end
28
+
29
+ def set_record(record)
30
+ @record = record
31
+ self
32
+ end
33
+
34
+ # Authorizes a single rule. Returns a boolean; raises `NotAuthorizedError` when denied and
35
+ # `raise_exception` is true.
36
+ def authorize_action(_rule, record: nil, raise_exception: true)
37
+ raise NotImplementedError, "#{self.class} must implement #authorize_action"
38
+ end
39
+
40
+ def allowed?(rule, record: nil)
41
+ authorize_action(rule, record: record, raise_exception: false)
42
+ end
43
+
44
+ # Applies the policy's record scope to a relation; returns it unchanged when none applies.
45
+ def apply_policy(_scope)
46
+ raise NotImplementedError, "#{self.class} must implement #apply_policy"
47
+ end
48
+
49
+ # Whether the resolved policy/instance responds to the rule (for the given record).
50
+ def has_rule?(_rule, record: nil)
51
+ raise NotImplementedError, "#{self.class} must implement #has_rule?"
52
+ end
53
+
54
+ # Whether the policy CLASS explicitly defines the rule (field-level auth: an undefined rule
55
+ # means "no opinion" → the field stays visible).
56
+ def defines_rule?(_rule)
57
+ raise NotImplementedError, "#{self.class} must implement #defines_rule?"
58
+ end
59
+
60
+ private
61
+
62
+ def normalize_rule(rule)
63
+ string = rule.to_s
64
+ string.end_with?("?") ? string.to_sym : :"#{string}?"
65
+ end
66
+
67
+ # Shared "no policy / undefined rule" fallback: `explicit_authorization` decides — denied when
68
+ # true (raising if requested), allowed when false. Used by every adapter so the
69
+ # missing-policy/undefined-rule semantics stay identical across backends.
70
+ def handle_missing_policy(raise_exception)
71
+ if RubyUIAdmin.configuration.explicit_authorization
72
+ raise NotAuthorizedError, "No policy found" if raise_exception
73
+ false
74
+ else
75
+ true
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "cancancan"
5
+ rescue LoadError
6
+ raise LoadError, "config.authorization_client = :cancancan requires the `cancancan` gem. Add `gem \"cancancan\"` to your Gemfile."
7
+ end
8
+
9
+ module RubyUIAdmin
10
+ module Authorization
11
+ # CanCanCan-backed adapter. Maps the admin's CRUD rules onto `Ability#can?(action, subject)` and
12
+ # the index scope onto `relation.accessible_by(ability)`.
13
+ #
14
+ # ⚠️ PARTIAL by design (CanCanCan is action/subject-based, not policy-class-based):
15
+ # * No `policy_class` — a single global Ability (`config.cancancan_ability_class`, default
16
+ # `Ability`) decides everything; `authorization_policy` per resource is ignored.
17
+ # * **No field-level authorization** — CanCanCan has no per-rule "is it defined?" concept, so
18
+ # `defines_rule?`/`has_rule?` return false (every field stays visible). Use action_policy or
19
+ # Pundit if you need field-level rules.
20
+ # * **`explicit_authorization` has no effect** — CanCanCan denies whatever the Ability doesn't
21
+ # grant; there's no `manage?`/undefined-rule fallback. Grant access in your Ability.
22
+ # * `true_user` (impersonation) is not passed to the Ability.
23
+ # * The index scope follows the Ability's rules for the action (e.g. `can :read, Post, ...`);
24
+ # with no matching rule it resolves to an empty relation, the CanCanCan way.
25
+ class CanCanCanAdapter < Adapter
26
+ def authorize_action(rule, record: nil, raise_exception: true)
27
+ return true unless RubyUIAdmin.configuration.authorization_enabled?
28
+
29
+ target = record.nil? ? @record : record
30
+ action = cancan_action(rule)
31
+ result = ability.can?(action, target)
32
+
33
+ if !result && raise_exception
34
+ raise NotAuthorizedError, "Not authorized to #{action} on #{target.inspect}"
35
+ end
36
+
37
+ result
38
+ end
39
+
40
+ def apply_policy(scope)
41
+ return scope unless RubyUIAdmin.configuration.authorization_enabled?
42
+
43
+ scope.accessible_by(ability)
44
+ rescue CanCan::Error
45
+ scope
46
+ end
47
+
48
+ # Field-level authorization is not supported by CanCanCan (no per-rule definition concept).
49
+ def has_rule?(_rule, record: nil) = false
50
+
51
+ def defines_rule?(_rule) = false
52
+
53
+ private
54
+
55
+ def ability
56
+ @ability ||= ability_class.new(user)
57
+ end
58
+
59
+ def ability_class
60
+ configured = RubyUIAdmin.configuration.cancancan_ability_class || "Ability"
61
+ configured.is_a?(Class) ? configured : Object.const_get(configured.to_s)
62
+ end
63
+
64
+ # Admin rule (`show?`, `update?`, …) → CanCanCan action (`:show`, `:update`, …).
65
+ def cancan_action(rule)
66
+ rule.to_s.delete_suffix("?").to_sym
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "pundit"
5
+ rescue LoadError
6
+ raise LoadError, "config.authorization_client = :pundit requires the `pundit` gem. Add `gem \"pundit\"` to your Gemfile."
7
+ end
8
+
9
+ module RubyUIAdmin
10
+ module Authorization
11
+ # Pundit-backed adapter. Maps the admin's rules onto a Pundit policy (`policy.show?`), record
12
+ # scopes onto the policy's `Scope`, and uses `respond_to?`/`public_method_defined?` for the
13
+ # field-level "is this rule defined?" check. A rule the policy doesn't define (or a missing
14
+ # policy) falls back to `explicit_authorization`, keeping the same semantics as action_policy.
15
+ #
16
+ # The resource's `authorization_policy` is used as the policy class when given; otherwise Pundit
17
+ # infers it (`Pundit::PolicyFinder`). Pundit policies receive only `user` — `true_user`
18
+ # (impersonation) is not passed to them.
19
+ class PunditAdapter < Adapter
20
+ def authorize_action(rule, record: nil, raise_exception: true)
21
+ return true unless RubyUIAdmin.configuration.authorization_enabled?
22
+
23
+ target = record.nil? ? @record : record
24
+ policy = policy_instance(target)
25
+ normalized = normalize_rule(rule)
26
+
27
+ return handle_missing_policy(raise_exception) if policy.nil? || !policy.respond_to?(normalized)
28
+
29
+ result = !!policy.public_send(normalized)
30
+ if !result && raise_exception
31
+ raise NotAuthorizedError, "Not authorized to #{normalized} on #{target.inspect}"
32
+ end
33
+
34
+ result
35
+ end
36
+
37
+ def apply_policy(scope)
38
+ return scope unless RubyUIAdmin.configuration.authorization_enabled?
39
+
40
+ scope_class = scope_class_for(scope)
41
+ return scope unless scope_class
42
+
43
+ scope_class.new(user, scope).resolve
44
+ rescue Pundit::NotDefinedError
45
+ scope
46
+ end
47
+
48
+ def has_rule?(rule, record: nil)
49
+ target = record.nil? ? @record : record
50
+ policy = policy_instance(target)
51
+ return false if policy.nil?
52
+
53
+ policy.respond_to?(normalize_rule(rule))
54
+ end
55
+
56
+ def defines_rule?(rule)
57
+ return false if policy_class.nil?
58
+
59
+ policy_class.public_method_defined?(normalize_rule(rule))
60
+ rescue
61
+ false
62
+ end
63
+
64
+ private
65
+
66
+ # Explicit `authorization_policy` wins; otherwise Pundit infers the policy class from the
67
+ # target (instance or class). Returns nil when none can be resolved.
68
+ def policy_instance(target)
69
+ klass = policy_class || Pundit::PolicyFinder.new(target).policy
70
+ return nil unless klass
71
+
72
+ klass.new(user, target)
73
+ rescue Pundit::NotDefinedError
74
+ nil
75
+ end
76
+
77
+ def scope_class_for(scope)
78
+ if policy_class&.const_defined?(:Scope)
79
+ policy_class::Scope
80
+ else
81
+ Pundit::PolicyFinder.new(scope).scope
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ # Resolves the authorization adapter from `config.authorization_client`. The value may be a
5
+ # symbol (looked up in REGISTRY), a Class (a custom adapter), or nil (disabled — still resolves
6
+ # to the default adapter, which short-circuits to permissive via `authorization_enabled?`).
7
+ module Authorization
8
+ # Symbol → adapter class, wrapped in a lambda so optional backends (Pundit, CanCanCan) are only
9
+ # referenced when actually selected.
10
+ REGISTRY = {
11
+ action_policy: -> { ActionPolicyAdapter },
12
+ pundit: lambda {
13
+ require "ruby_ui_admin/authorization/pundit_adapter"
14
+ PunditAdapter
15
+ },
16
+ cancancan: lambda {
17
+ require "ruby_ui_admin/authorization/can_can_can_adapter"
18
+ CanCanCanAdapter
19
+ }
20
+ }.freeze
21
+
22
+ def self.adapter_class(client = RubyUIAdmin.configuration.authorization_client)
23
+ return client if client.is_a?(Class)
24
+
25
+ key = (client || :action_policy).to_sym
26
+ resolver = REGISTRY[key]
27
+ unless resolver
28
+ known = REGISTRY.keys.join(", ")
29
+ raise ArgumentError,
30
+ "Unknown authorization_client #{client.inspect}. Use one of: #{known}, an adapter class, or nil."
31
+ end
32
+
33
+ resolver.call
34
+ end
35
+
36
+ def self.build(...)
37
+ adapter_class.new(...)
38
+ end
39
+ end
40
+ end