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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class TextareaField < BaseField
6
+ register_as :textarea
7
+
8
+ def rows
9
+ options[:rows] || 4
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Fields
5
+ class UrlField < BaseField
6
+ register_as :url
7
+
8
+ # Optional display text; defaults to the URL itself.
9
+ def link_text(record)
10
+ text = options[:text]
11
+ return ExecutionContext.new(target: text, record: record, resource: resource).handle if text.respond_to?(:call)
12
+
13
+ text || value(record)
14
+ end
15
+
16
+ def target
17
+ options[:target] || "_blank"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Filters
5
+ # Base class for filters. Subclasses implement `apply(request, query, value)`
6
+ # and (for select-style filters) `options`.
7
+ class BaseFilter
8
+ class << self
9
+ # Class-level `self.name =` while still falling back to the real class
10
+ # name (so Zeitwerk/Rails keep working when no display name is set).
11
+ def name=(value)
12
+ @display_name = value
13
+ end
14
+
15
+ def name
16
+ @display_name || super
17
+ end
18
+
19
+ def empty_message=(value)
20
+ @empty_message = value
21
+ end
22
+
23
+ def empty_message
24
+ defined?(@empty_message) ? @empty_message : nil
25
+ end
26
+
27
+ # Inherited down the chain (subclasses of SelectFilter are :select, etc.).
28
+ def filter_type
29
+ return @filter_type if defined?(@filter_type) && @filter_type
30
+ return superclass.filter_type if superclass.respond_to?(:filter_type)
31
+
32
+ nil
33
+ end
34
+
35
+ def register_type(type)
36
+ @filter_type = type.to_sym
37
+ end
38
+ end
39
+
40
+ attr_reader :arguments
41
+
42
+ def initialize(arguments: {})
43
+ @arguments = arguments || {}
44
+ end
45
+
46
+ def name
47
+ self.class.name
48
+ end
49
+
50
+ def type
51
+ self.class.filter_type
52
+ end
53
+
54
+ def empty_message
55
+ self.class.empty_message
56
+ end
57
+
58
+ # Unique key used in the query string, e.g.
59
+ # RubyUIAdmin::Filters::PublishedFilter -> "published_filter".
60
+ # Uses `to_s` (not the overridable `name`) to get the real class name.
61
+ def param_key
62
+ self.class.to_s.sub(/^RubyUIAdmin::Filters::/, "").gsub("::", "_").underscore
63
+ end
64
+
65
+ # Override in subclasses.
66
+ def apply(request, query, value)
67
+ query
68
+ end
69
+
70
+ # Override in select/multiple-select filters.
71
+ def options
72
+ {}
73
+ end
74
+
75
+ # Override to provide a default selected value.
76
+ def default
77
+ nil
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Filters
5
+ class BooleanFilter < BaseFilter
6
+ register_type :boolean
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Filters
5
+ # A filter offering several checkboxes; the submitted value is an Array of the
6
+ # checked option keys. Subclasses define `options` and `apply(request, query, value)`.
7
+ class MultipleSelectFilter < BaseFilter
8
+ register_type :multiple_select
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Filters
5
+ class SelectFilter < BaseFilter
6
+ register_type :select
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Filters
5
+ class TextFilter < BaseFilter
6
+ register_type :text
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Menu
5
+ # A leaf linking to a resource's index. Carries the resolved resource so the view
6
+ # can build the path through the dynamic routes.
7
+ ResourceItem = Struct.new(:resource, :label, keyword_init: true) do
8
+ def type = :resource
9
+ end
10
+
11
+ # A leaf linking to an arbitrary path.
12
+ LinkItem = Struct.new(:label, :path, keyword_init: true) do
13
+ def type = :link
14
+ end
15
+
16
+ # A leaf linking to a dashboard.
17
+ DashboardItem = Struct.new(:dashboard, :label, keyword_init: true) do
18
+ def type = :dashboard
19
+ end
20
+
21
+ # A titled group of items.
22
+ SectionItem = Struct.new(:label, :icon, :items, keyword_init: true) do
23
+ def type = :section
24
+ end
25
+
26
+ # Evaluates a `config.main_menu` block into a tree of menu items. The block is
27
+ # instance_exec'd against a Builder, so `section`/`resource`/`link`/... are DSL calls.
28
+ #
29
+ # config.main_menu = -> do
30
+ # section "Tables", icon: "table" do
31
+ # resource :user
32
+ # resource :buyer, label: "Buyers"
33
+ # link "Docs", "https://example.com"
34
+ # end
35
+ # end
36
+ class Builder
37
+ def self.build(&block)
38
+ builder = new
39
+ builder.instance_exec(&block)
40
+ builder.items
41
+ end
42
+
43
+ attr_reader :items
44
+
45
+ def initialize
46
+ @items = []
47
+ end
48
+
49
+ def section(label = nil, icon: nil, &block)
50
+ children = self.class.build(&block)
51
+ @items << SectionItem.new(label: label, icon: icon, items: children)
52
+ end
53
+ alias_method :group, :section
54
+
55
+ def resource(name, label: nil, **)
56
+ resource = RubyUIAdmin.resource_manager.find_by_route_key(name.to_s)
57
+ @items << ResourceItem.new(resource: resource, label: label) if resource
58
+ end
59
+ alias_method :resources, :resource
60
+
61
+ def link(label, path = nil, **args)
62
+ @items << LinkItem.new(label: label, path: path || args[:path])
63
+ end
64
+ alias_method :link_to, :link
65
+
66
+ def dashboard(id, label: nil)
67
+ dash = RubyUIAdmin.dashboard_manager.dashboards.find { |d| d.id.to_s == id.to_s }
68
+ @items << DashboardItem.new(dashboard: dash, label: label) if dash
69
+ end
70
+
71
+ def all_resources(except: [])
72
+ excluded = Array(except).map(&:to_s)
73
+ RubyUIAdmin.resource_manager.navigation_resources.each do |rc|
74
+ next if excluded.include?(rc.route_key)
75
+
76
+ @items << ResourceItem.new(resource: rc, label: nil)
77
+ end
78
+ end
79
+
80
+ def all_dashboards(except: [])
81
+ excluded = Array(except).map(&:to_s)
82
+ RubyUIAdmin.dashboard_manager.dashboards.each do |dash|
83
+ next if excluded.include?(dash.id.to_s)
84
+
85
+ @items << DashboardItem.new(dashboard: dash, label: nil)
86
+ end
87
+ end
88
+
89
+ # Available inside the menu block.
90
+ def current_user = RubyUIAdmin::Current.user
91
+ def params = RubyUIAdmin::Current.params
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/class/subclasses"
4
+
5
+ module RubyUIAdmin
6
+ # Discovers and indexes the host app's resources (defined under
7
+ # `RubyUIAdmin::Resources` in `app/ruby_ui_admin/resources`).
8
+ class ResourceManager
9
+ def boot
10
+ @resources = nil
11
+ self
12
+ end
13
+
14
+ def resources
15
+ @resources ||= fetch_resources
16
+ end
17
+
18
+ # All resources visible in the sidebar navigation.
19
+ def navigation_resources
20
+ resources.select { |resource| resource.visible_on_sidebar }
21
+ end
22
+
23
+ # Find a resource class by its route key, e.g. "buyers".
24
+ def find_by_route_key(key)
25
+ key = key.to_s
26
+ resources.find { |resource| resource.route_key == key } ||
27
+ resources.find { |resource| resource.resource_name == key.singularize }
28
+ end
29
+
30
+ # Find the resource class for a given model class.
31
+ def find_for_model(model_class)
32
+ mapped = RubyUIAdmin.configuration.model_resource_mapping[model_class.to_s]
33
+ return mapped.to_s.constantize if mapped
34
+
35
+ resources.find { |resource| resource.model_class == model_class }
36
+ end
37
+
38
+ private
39
+
40
+ def fetch_resources
41
+ eager_load_resources!
42
+
43
+ selected = RubyUIAdmin::BaseResource.descendants.select do |resource|
44
+ next false if resource.abstract?
45
+
46
+ # Only real, named resources count — excludes anonymous subclasses (e.g.
47
+ # those built in tests) that would otherwise pollute the registry.
48
+ name = resource.name
49
+ name && (RubyUIAdmin.configuration.resources.present? || name.start_with?("RubyUIAdmin::Resources::"))
50
+ end
51
+
52
+ # Dedupe by name and re-resolve the constant: in development, `descendants` retains
53
+ # stale class versions from previous Zeitwerk reloads (same name) — keep the current one.
54
+ selected.map(&:name).uniq.filter_map { |name| name.safe_constantize }
55
+ end
56
+
57
+ def eager_load_resources!
58
+ configured = RubyUIAdmin.configuration.resources
59
+
60
+ if configured.present?
61
+ Array(configured).each { |name| name.to_s.constantize }
62
+ elsif defined?(::Rails)
63
+ eager_load_resources_dir
64
+ end
65
+ rescue NameError
66
+ nil
67
+ end
68
+
69
+ def eager_load_resources_dir
70
+ dir = Rails.root.join("app", "ruby_ui_admin", "resources")
71
+ return unless Dir.exist?(dir)
72
+
73
+ loader = Rails.autoloaders.main
74
+ if loader.respond_to?(:eager_load_dir)
75
+ loader.eager_load_dir(dir)
76
+ else
77
+ # Fallback for older Zeitwerk: reference the namespace to trigger loading.
78
+ loader.eager_load_namespace(RubyUIAdmin::Resources) if defined?(RubyUIAdmin::Resources)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Scopes
5
+ # Base class for named index scopes. A scope narrows the index query
6
+ # via a `scope` lambda and shows up as a tab above the table.
7
+ #
8
+ # class Published < RubyUIAdmin::Scopes::BaseScope
9
+ # self.name = "Published"
10
+ # self.scope = -> { query.where(published: true) }
11
+ # self.default = true
12
+ # end
13
+ class BaseScope
14
+ class << self
15
+ # Class-level `self.name =` with fallback to the real class name.
16
+ def name=(value)
17
+ @display_name = value
18
+ end
19
+
20
+ def name
21
+ @display_name || super
22
+ end
23
+
24
+ def scope=(value)
25
+ @scope = value
26
+ end
27
+
28
+ def scope
29
+ @scope if defined?(@scope)
30
+ end
31
+
32
+ def description=(value)
33
+ @description = value
34
+ end
35
+
36
+ def description
37
+ @description if defined?(@description)
38
+ end
39
+
40
+ def default=(value)
41
+ @default = value
42
+ end
43
+
44
+ def default?
45
+ !!(@default if defined?(@default))
46
+ end
47
+
48
+ def visible=(value)
49
+ @visible = value
50
+ end
51
+
52
+ def visible_setting
53
+ defined?(@visible) ? @visible : true
54
+ end
55
+
56
+ # URL key, e.g. RubyUIAdmin::Scopes::PublishedPosts -> "published_posts".
57
+ def scope_key
58
+ to_s.sub(/^RubyUIAdmin::Scopes::/, "").gsub("::", "_").underscore
59
+ end
60
+ end
61
+
62
+ attr_reader :params
63
+
64
+ # `default_override` (set via `scope Klass, default: true` on the resource) wins over
65
+ # the scope class's own `self.default`.
66
+ def initialize(params: {}, default_override: nil)
67
+ @params = params || {}
68
+ @default_override = default_override
69
+ end
70
+
71
+ def name
72
+ self.class.name
73
+ end
74
+
75
+ def key
76
+ self.class.scope_key
77
+ end
78
+
79
+ def description
80
+ self.class.description
81
+ end
82
+
83
+ def default?
84
+ return !!@default_override unless @default_override.nil?
85
+
86
+ self.class.default?
87
+ end
88
+
89
+ # Applies the scope to the query. A Symbol calls the model's named scope/class method;
90
+ # a lambda is run with `query` and `params` available.
91
+ def apply(query)
92
+ scope = self.class.scope
93
+ return query if scope.nil?
94
+ return query.public_send(scope) if scope.is_a?(Symbol)
95
+
96
+ ExecutionContext.new(target: scope, query: query, params: params).handle
97
+ end
98
+
99
+ def visible?(user: nil)
100
+ setting = self.class.visible_setting
101
+ return setting unless setting.respond_to?(:call)
102
+
103
+ ExecutionContext.new(target: setting, user: user, params: params).handle
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ module Services
5
+ # Facade over the configured authorization adapter (`config.authorization_client`, default
6
+ # action_policy). Resolves the adapter and delegates, so controllers/views/resources stay
7
+ # backend-agnostic. The backend logic lives in `RubyUIAdmin::Authorization::*Adapter`.
8
+ class AuthorizationService
9
+ def initialize(user, record = nil, policy_class: nil, true_user: nil)
10
+ @adapter = RubyUIAdmin::Authorization.build(user, record, policy_class: policy_class, true_user: true_user)
11
+ end
12
+
13
+ def authorize_action(rule, record: nil, raise_exception: true)
14
+ @adapter.authorize_action(rule, record: record, raise_exception: raise_exception)
15
+ end
16
+
17
+ def allowed?(rule, record: nil) = @adapter.allowed?(rule, record: record)
18
+
19
+ def apply_policy(scope) = @adapter.apply_policy(scope)
20
+
21
+ def has_rule?(rule, record: nil) = @adapter.has_rule?(rule, record: record)
22
+
23
+ def defines_rule?(rule) = @adapter.defines_rule?(rule)
24
+
25
+ def true_user = @adapter.true_user
26
+
27
+ def set_record(record)
28
+ @adapter.set_record(record)
29
+ self
30
+ end
31
+
32
+ # Context readers (kept for backward compatibility with any caller reading them).
33
+ def user = @adapter.user
34
+ def record = @adapter.record
35
+ def policy_class = @adapter.policy_class
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ # Layout containers produced by the resource DSL (`panel`, `tabs`, `tab`).
5
+ # They hold child items (fields or nested containers) for structured rendering
6
+ # on the show/form views.
7
+ module Structure
8
+ class Panel
9
+ attr_reader :name, :items
10
+
11
+ def initialize(name = nil)
12
+ @name = name
13
+ @items = []
14
+ end
15
+ end
16
+
17
+ class TabGroup
18
+ attr_reader :tabs
19
+
20
+ def initialize
21
+ @tabs = []
22
+ end
23
+
24
+ # Containers expose their children through `items` for uniform traversal.
25
+ def items
26
+ @tabs
27
+ end
28
+ end
29
+
30
+ class Tab
31
+ attr_reader :name, :description, :items
32
+
33
+ def initialize(name, description: nil)
34
+ @name = name
35
+ @description = description
36
+ @items = []
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module RubyUIAdmin
6
+ # Extracts the Tailwind class candidates used by the admin's Phlex views into a single file the
7
+ # host app can `@source` from its Tailwind build. This solves the "gem path changes per
8
+ # environment" problem: instead of pointing `@source` at the gem's `.rb` files (whose absolute
9
+ # path differs between dev/CI/deploy), the host writes the class inventory into its OWN tree (a
10
+ # stable, relative path) and re-runs on upgrade. It scans the same literals Tailwind would, so the
11
+ # generated utilities match the previous `@source "<gem>/**/*.rb"` approach.
12
+ #
13
+ # Driven by `rake ruby_ui_admin:tailwind_source`.
14
+ module TailwindSource
15
+ VIEWS_GLOB = "app/components/ruby_ui_admin/**/*.rb"
16
+
17
+ module_function
18
+
19
+ # All class candidates across the engine's view files, deduped and sorted.
20
+ def class_candidates
21
+ Dir[RubyUIAdmin::Engine.root.join(VIEWS_GLOB)].sort
22
+ .flat_map { |file| extract(File.read(file)) }
23
+ .uniq.sort
24
+ end
25
+
26
+ # Pulls Tailwind-ish class tokens out of a chunk of Ruby source: every double-quoted string
27
+ # literal (and heredoc `CLASSES` body) is split into tokens, with `#{…}` interpolations dropped
28
+ # so the literal fragments around them survive. Non-class words are filtered out by
29
+ # `class_token?`. Only double quotes are scanned — the views write every `class:` with them, and
30
+ # scanning single quotes would let stray apostrophes in comments span across real code.
31
+ def extract(ruby_source)
32
+ strings = ruby_source.scan(/"([^"]*)"/).flatten
33
+ strings += ruby_source.scan(/<<~?CLASSES\n(.*?)\n\s*CLASSES/m).flatten
34
+
35
+ strings
36
+ .map { |string| string.gsub(/#\{[^}]*\}/, " ") }
37
+ .flat_map { |string| string.split(/\s+/) }
38
+ .select { |token| class_token?(token) }
39
+ .uniq
40
+ end
41
+
42
+ # Heuristic: a Tailwind utility candidate. Must start with a letter (optionally `!`/`-` for
43
+ # important/negative) and may carry variants/values (`hover:`, `/10`, `[356px]`). Requiring a
44
+ # letter start drops embedded SVG path coordinates (e.g. `0-7.07l-1.72`), which start with a
45
+ # digit and would otherwise be scanned as junk — no real Tailwind utility starts with a digit.
46
+ def class_token?(token)
47
+ token.match?(%r{\A!?-?[a-z][a-z0-9:_/\[\]().,%#-]*\z})
48
+ end
49
+
50
+ # The scannable file body (valid HTML so any Tailwind extractor picks it up).
51
+ def html
52
+ <<~HTML
53
+ <!-- Generated by `rake ruby_ui_admin:tailwind_source`. Do not edit — re-run after upgrading ruby_ui_admin. -->
54
+ <div class="#{class_candidates.join(" ")}"></div>
55
+ HTML
56
+ end
57
+
58
+ # Writes the inventory to `dest` and returns the path.
59
+ def write(dest)
60
+ FileUtils.mkdir_p(File.dirname(dest))
61
+ File.write(dest, html)
62
+ dest
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ # A small value object for the current view, so user lambdas (`visible:`, action
5
+ # `self.visible`/`self.message`) can write either `view == :show` or `view.show?`.
6
+ # It behaves like its underlying Symbol for comparison.
7
+ class View
8
+ def self.wrap(value)
9
+ value.is_a?(View) ? value : new(value)
10
+ end
11
+
12
+ def initialize(name)
13
+ @name = name&.to_sym
14
+ end
15
+
16
+ def to_sym = @name
17
+ def to_s = @name.to_s
18
+ def inspect = "#<RubyUIAdmin::View #{@name.inspect}>"
19
+
20
+ def index? = @name == :index
21
+ def show? = @name == :show
22
+ def new? = @name == :new
23
+ def edit? = @name == :edit
24
+ def form? = @name == :new || @name == :edit # new + edit
25
+ def display? = @name == :index || @name == :show # index + show
26
+
27
+ def ==(other)
28
+ other.respond_to?(:to_sym) ? @name == other.to_sym : false
29
+ end
30
+ alias_method :eql?, :==
31
+
32
+ def hash = @name.hash
33
+ end
34
+ end