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,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUIAdmin
4
+ class ResourcesController < ApplicationController
5
+ before_action :set_resource
6
+ before_action :set_record, only: %i[show edit update destroy]
7
+ before_action :alias_model_params, only: %i[create update]
8
+
9
+ def index
10
+ authorize_action!(:index, on: model_class, policy_class: policy_class)
11
+
12
+ query = authorization_for(model_class, policy_class: policy_class).apply_policy(@resource.base_query)
13
+ query = apply_scope(query)
14
+ query = apply_filters(query)
15
+ query = apply_sorting(query)
16
+
17
+ limit = RubyUIAdmin.configuration.per_page
18
+ countless = @resource.class.countless || RubyUIAdmin.configuration.countless_pagination?
19
+ @pagy, @records =
20
+ if countless
21
+ pagy_countless(query, limit: limit)
22
+ else
23
+ pagy(query, limit: limit)
24
+ end
25
+
26
+ render Views::Index.new(
27
+ resource: @resource,
28
+ records: @records,
29
+ pagy: @pagy,
30
+ filters: @resource.get_filters,
31
+ filter_values: filter_params,
32
+ scopes: @resource.get_scopes,
33
+ current_scope_key: current_scope&.key,
34
+ scope_param: params[:scope],
35
+ remove_scope_all: @resource.remove_scope_all?,
36
+ query_params: request.query_parameters,
37
+ sort_by: params[:sort_by],
38
+ sort_direction: sort_direction
39
+ )
40
+ end
41
+
42
+ def show
43
+ authorize_action!(:show, on: @record, policy_class: policy_class)
44
+ @resource.hydrate(record: @record, view: :show)
45
+
46
+ # `?tab=N&fragment=1` returns just one tab's content (no page chrome) for lazy tab loading,
47
+ # so that tab's fields/associations are only evaluated when the tab is actually opened.
48
+ if params[:fragment] && params[:tab]
49
+ render Views::ShowTab.new(resource: @resource, record: @record, tab_index: params[:tab].to_i)
50
+ else
51
+ render Views::Show.new(resource: @resource, record: @record)
52
+ end
53
+ end
54
+
55
+ def new
56
+ @record = @resource.new_record
57
+ authorize_action!(:create, on: @record, policy_class: policy_class)
58
+ @resource.hydrate(record: @record, view: :new)
59
+ @resource.apply_defaults(@record)
60
+
61
+ render Views::Form.new(resource: @resource, record: @record, view: :new)
62
+ end
63
+
64
+ def create
65
+ @record = @resource.new_record
66
+ authorize_action!(:create, on: @record, policy_class: policy_class)
67
+ @resource.hydrate(record: @record, view: :new)
68
+ @resource.apply_defaults(@record)
69
+ fill_record(@record, :new)
70
+
71
+ if @record.save
72
+ create_success_action
73
+ else
74
+ create_fail_action
75
+ end
76
+ end
77
+
78
+ def edit
79
+ authorize_action!(:update, on: @record, policy_class: policy_class)
80
+ @resource.hydrate(record: @record, view: :edit)
81
+
82
+ render Views::Form.new(resource: @resource, record: @record, view: :edit)
83
+ end
84
+
85
+ def update
86
+ authorize_action!(:update, on: @record, policy_class: policy_class)
87
+ fill_record(@record, :edit)
88
+
89
+ if @record.save
90
+ update_success_action
91
+ else
92
+ update_fail_action
93
+ end
94
+ end
95
+
96
+ def destroy
97
+ authorize_action!(:destroy, on: @record, policy_class: policy_class)
98
+
99
+ if @record.destroy
100
+ destroy_success_action
101
+ else
102
+ destroy_fail_action
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ # ---- Lifecycle hooks (override in a per-resource controller) ----
109
+
110
+ def create_success_action
111
+ redirect_to after_create_path, notice: create_success_message, status: :see_other
112
+ end
113
+
114
+ # Flash messages for the lifecycle hooks. Overridable; also available to host
115
+ # per-resource controllers.
116
+ def create_success_message = "#{resource_label} was successfully created."
117
+
118
+ def update_success_message = "#{resource_label} was successfully updated."
119
+
120
+ def destroy_success_message = "#{resource_label} was successfully destroyed."
121
+
122
+ def create_fail_action
123
+ @resource.hydrate(record: @record, view: :new)
124
+ render Views::Form.new(resource: @resource, record: @record, view: :new), status: :unprocessable_entity
125
+ end
126
+
127
+ def update_success_action
128
+ redirect_to after_update_path, notice: update_success_message, status: :see_other
129
+ end
130
+
131
+ def update_fail_action
132
+ @resource.hydrate(record: @record, view: :edit)
133
+ render Views::Form.new(resource: @resource, record: @record, view: :edit), status: :unprocessable_entity
134
+ end
135
+
136
+ def destroy_success_action
137
+ redirect_to after_destroy_path, notice: destroy_success_message, status: :see_other
138
+ end
139
+
140
+ def destroy_fail_action
141
+ redirect_to record_path(@record), alert: "#{resource_label} could not be destroyed.", status: :see_other
142
+ end
143
+
144
+ def after_create_path = record_path(@record)
145
+
146
+ def after_update_path = record_path(@record)
147
+
148
+ def after_destroy_path = resources_index_path
149
+
150
+ def set_resource
151
+ resource_class = RubyUIAdmin.resource_manager.find_by_route_key(params[:resource_name])
152
+ raise ActionController::RoutingError, "Unknown resource #{params[:resource_name]}" if resource_class.nil?
153
+
154
+ @resource = resource_class.new.hydrate(user: current_user, params: params)
155
+ end
156
+
157
+ def set_record
158
+ @record = @resource.find_record(params[:id])
159
+ end
160
+
161
+ def model_class
162
+ @resource.model_class
163
+ end
164
+
165
+ def policy_class
166
+ @resource.class.authorization_policy
167
+ end
168
+
169
+ def resource_label
170
+ @resource.model_class.model_name.human
171
+ end
172
+
173
+ # This engine submits form params under `params[:record]`. Mirror them under the model's
174
+ # param key (e.g. `params[:user]`) too, so host controllers that re-read params by the
175
+ # model key keep working.
176
+ def alias_model_params
177
+ return unless params[:record].present?
178
+
179
+ key = model_class.model_name.param_key
180
+ params[key] ||= params[:record]
181
+ end
182
+
183
+ def fill_record(record, view)
184
+ # Hydrate the resource with the record so field-level authorization is evaluated
185
+ # against it (only authorized fields are permitted/filled).
186
+ @resource.hydrate(record: record, view: view)
187
+
188
+ fields = @resource.get_fields(view: view).reject(&:readonly?)
189
+ attributes = params.fetch(:record, ActionController::Parameters.new).permit(*fields.flat_map(&:permit_params))
190
+
191
+ fields.each { |field| field.fill(record, attributes) }
192
+ end
193
+
194
+ def filter_params
195
+ raw = params[:filters]
196
+ return {} if raw.blank?
197
+
198
+ raw.respond_to?(:to_unsafe_h) ? raw.to_unsafe_h : raw.to_h
199
+ end
200
+
201
+ # The selected scope: explicit `?scope=` wins; `scope=all` clears it; otherwise the
202
+ # resource's default scope (if any) applies.
203
+ def current_scope
204
+ key = params[:scope]
205
+ return nil if key == "all"
206
+ return @resource.find_scope(key) if key.present?
207
+
208
+ @resource.default_scope_entry
209
+ end
210
+
211
+ def apply_scope(query)
212
+ scope = current_scope
213
+ scope ? scope.apply(query) : query
214
+ end
215
+
216
+ def apply_filters(query)
217
+ submitted = filter_params
218
+ @resource.get_filters.each do |filter|
219
+ key = filter.param_key
220
+ # Use the filter's `default` only when its param wasn't submitted at all (so an
221
+ # explicitly-cleared filter stays cleared).
222
+ value = submitted.key?(key) ? submitted[key] : filter.default
223
+ next if value.blank?
224
+
225
+ query = filter.apply(request, query, value)
226
+ end
227
+ query
228
+ end
229
+
230
+ def apply_sorting(query)
231
+ sort_by = params[:sort_by]
232
+ return query if sort_by.blank?
233
+
234
+ field = @resource.find_field(sort_by)
235
+ return query unless field&.sortable?
236
+
237
+ if (custom = field.sort_lambda)
238
+ # Custom sort: `sortable: -> { query.reorder(...) }` with `query`, `direction`
239
+ # and `resource` available as readers.
240
+ ExecutionContext.new(target: custom, query: query, direction: sort_direction, resource: @resource).handle
241
+ else
242
+ query.reorder(field.database_id => sort_direction)
243
+ end
244
+ end
245
+
246
+ def sort_direction
247
+ params[:sort_direction] == "desc" ? :desc : :asc
248
+ end
249
+
250
+ # Path helpers resolving the dynamic per-resource routes.
251
+ def resources_index_path
252
+ helpers.public_send("resources_#{@resource.route_key}_path")
253
+ end
254
+
255
+ def record_path(record)
256
+ helpers.public_send("resources_#{@resource.singular_route_key}_path", record)
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,59 @@
1
+ en:
2
+ ruby_ui_admin:
3
+ actions:
4
+ new: "New %{model}"
5
+ show: "Show"
6
+ edit: "Edit"
7
+ destroy: "Delete"
8
+ create: "Create"
9
+ update: "Update"
10
+ cancel: "Cancel"
11
+ ran_successfully: "Action ran successfully."
12
+ nav:
13
+ sign_out: "Sign out"
14
+ index:
15
+ actions: "Actions"
16
+ empty: "No records found."
17
+ filter: "Filter"
18
+ clear: "Clear"
19
+ all: "All"
20
+ with_selected: "With selected:"
21
+ search: "Search…"
22
+ selected_suffix: "selected"
23
+ no_results: "No results."
24
+ confirm:
25
+ title: "Are you sure?"
26
+ message: "This action cannot be undone."
27
+ destroy_title: "Delete record"
28
+ destroy: "Delete “%{model}”? This action cannot be undone."
29
+ cancel: "Cancel"
30
+ confirm: "Delete"
31
+ form:
32
+ errors: "Please fix the following errors:"
33
+ fields:
34
+ file:
35
+ remove: "Remove current file"
36
+ booleans:
37
+ "true": "Yes"
38
+ "false": "No"
39
+ associations:
40
+ count:
41
+ one: "%{count} item"
42
+ other: "%{count} items"
43
+ more: "+%{count} more"
44
+ tabs:
45
+ loading: "Loading…"
46
+ requires_js: "This content requires JavaScript."
47
+ pagination:
48
+ showing: "Showing %{from}–%{to} of %{count}"
49
+ page: "Page %{page} of %{pages}"
50
+ page_only: "Page %{page}"
51
+ previous: "Previous"
52
+ next: "Next"
53
+ # Locale-aware formats used by the date/date_time fields (no explicit `format:`).
54
+ time:
55
+ formats:
56
+ ruby_ui_admin: "%Y-%m-%d %H:%M"
57
+ date:
58
+ formats:
59
+ ruby_ui_admin: "%Y-%m-%d"
@@ -0,0 +1,59 @@
1
+ "pt-BR":
2
+ ruby_ui_admin:
3
+ actions:
4
+ new: "Novo %{model}"
5
+ show: "Ver"
6
+ edit: "Editar"
7
+ destroy: "Excluir"
8
+ create: "Criar"
9
+ update: "Atualizar"
10
+ cancel: "Cancelar"
11
+ ran_successfully: "Ação executada com sucesso."
12
+ nav:
13
+ sign_out: "Sair"
14
+ index:
15
+ actions: "Ações"
16
+ empty: "Nenhum registro encontrado."
17
+ filter: "Filtrar"
18
+ clear: "Limpar"
19
+ all: "Todos"
20
+ with_selected: "Com selecionados:"
21
+ search: "Pesquisar…"
22
+ selected_suffix: "selecionados"
23
+ no_results: "Nenhum resultado."
24
+ confirm:
25
+ title: "Tem certeza?"
26
+ message: "Esta ação não pode ser desfeita."
27
+ destroy_title: "Excluir registro"
28
+ destroy: "Excluir “%{model}”? Esta ação não pode ser desfeita."
29
+ cancel: "Cancelar"
30
+ confirm: "Excluir"
31
+ form:
32
+ errors: "Corrija os seguintes erros:"
33
+ fields:
34
+ file:
35
+ remove: "Remover arquivo atual"
36
+ booleans:
37
+ "true": "Sim"
38
+ "false": "Não"
39
+ associations:
40
+ count:
41
+ one: "%{count} item"
42
+ other: "%{count} itens"
43
+ more: "+%{count} mais"
44
+ tabs:
45
+ loading: "Carregando…"
46
+ requires_js: "Este conteúdo requer JavaScript."
47
+ pagination:
48
+ showing: "Exibindo %{from}–%{to} de %{count}"
49
+ page: "Página %{page} de %{pages}"
50
+ page_only: "Página %{page}"
51
+ previous: "Anterior"
52
+ next: "Próximo"
53
+ # Formatos locale-aware usados pelos campos date/date_time (sem `format:` explícito).
54
+ time:
55
+ formats:
56
+ ruby_ui_admin: "%d/%m/%Y %H:%M"
57
+ date:
58
+ formats:
59
+ ruby_ui_admin: "%d/%m/%Y"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Draws RESTful routes for every registered resource, e.g.:
4
+ # resources_posts_path, new_resources_post_path, edit_resources_post_path, ...
5
+ # The generic ResourcesController reads params[:resource_name] to resolve the resource.
6
+ RubyUIAdmin.resource_manager.resources.each do |resource|
7
+ resources resource.route_key,
8
+ controller: RubyUIAdmin.controller_for(resource),
9
+ as: "resources_#{resource.route_key}",
10
+ defaults: {resource_name: resource.route_key}
11
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyUIAdmin::Engine.routes.draw do
4
+ root to: "home#index"
5
+
6
+ # In-app docs browser: renders the gem's `docs/*.md` as HTML at `<mount>/docs`.
7
+ # Gated by `config.docs_enabled` (default `:local` — development/test only). When enabled in
8
+ # production it sits behind the admin authentication gate (see DocsController).
9
+ if RubyUIAdmin.configuration.docs_enabled?
10
+ get "/docs", to: "docs#show", as: :docs
11
+ get "/docs/*path", to: "docs#show", as: :doc, format: false
12
+ end
13
+
14
+ get "/dashboards/:dashboard_id", to: "dashboards#show", as: :dashboard
15
+
16
+ # Custom actions (declared before the dynamic resource routes so the 3-segment
17
+ # action path takes precedence). GET renders the action form, POST runs it.
18
+ get "/:resource_name/actions/:action_id", to: "actions#show", as: :resource_action
19
+ post "/:resource_name/actions/:action_id", to: "actions#run"
20
+
21
+ # Per-resource RESTful routes, generated from the resource registry.
22
+ draw(:dynamic_routes)
23
+ end
data/docs/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # RubyUI Admin — Documentation
2
+
3
+ > The README covers the essentials. This folder holds the full documentation, split by topic.
4
+
5
+ > **Read these inside your app.** The admin can serve this documentation rendered as HTML at
6
+ > `<mount>/docs` (e.g. `/admin/docs`). By default (`config.docs_enabled = :local`) it's mounted in
7
+ > development/test only; set `config.docs_enabled = true` to expose it in production too (where it
8
+ > runs behind the admin's auth gate). Add `kramdown` + `kramdown-parser-gfm` to the bundle of the
9
+ > target environment — see
10
+ > [Configuration › In-app docs browser](getting-started/configuration.md#in-app-docs-browser).
11
+
12
+ ## Getting started
13
+ - [Practical guide: a working admin from scratch](getting-started/practical-guide.md) — copy‑paste, start here
14
+ - [Installation](getting-started/installation.md)
15
+ - [Configuration](getting-started/configuration.md)
16
+ - [Authentication](getting-started/authentication.md)
17
+ - [Internationalization (i18n)](getting-started/internationalization.md)
18
+
19
+ ## Resources
20
+ - [Overview](resources/overview.md)
21
+ - [Tabs & panels](resources/tabs-panels.md)
22
+ - [Index customization](resources/index-customization.md)
23
+ - [Associations](resources/associations.md)
24
+ - [Scopes](resources/scopes.md)
25
+
26
+ ## Fields
27
+ - [Overview & field catalog](fields/overview.md)
28
+
29
+ ## Actions
30
+ - [Overview](actions/overview.md)
31
+
32
+ ## Filters
33
+ - [Overview & types](filters/overview.md)
34
+
35
+ ## Dashboards & cards
36
+ - [Overview](dashboards/overview.md)
37
+
38
+ ## Authorization
39
+ - [Authorization adapters (action_policy / Pundit / CanCanCan / custom)](authorization/adapters.md)
40
+ - [action_policy integration](authorization/action-policy.md)
41
+ - [Policies](authorization/policies.md)
42
+ - [Authorization scopes](authorization/scopes.md)
43
+ - [Field-level authorization](authorization/field-authorization.md)
44
+ - [Explicit authorization mode](authorization/explicit-mode.md)
45
+
46
+ ## Customization
47
+ - [Theming with RubyUI](customization/theming-rubyui.md)
48
+ - [JavaScript](customization/javascript.md)
49
+ - [Custom controllers & lifecycle hooks](customization/controllers.md)
50
+ - [Ejecting & customizing](customization/ejecting.md)
51
+
52
+ ## Generators & rake tasks
53
+ - [Overview](generators-and-tasks/overview.md)
@@ -0,0 +1,112 @@
1
+ # Actions — Overview
2
+
3
+ Custom actions run code against one or more records (or standalone). Define them in
4
+ `app/ruby_ui_admin/actions` under `RubyUIAdmin::Actions`, then attach them to a resource.
5
+
6
+ ```ruby
7
+ module RubyUIAdmin
8
+ module Actions
9
+ class PublishPosts < RubyUIAdmin::BaseAction
10
+ self.name = "Publish"
11
+ self.message = "Publish the selected posts?"
12
+ self.confirm_button_label = "Publish"
13
+
14
+ def handle(query:, fields:, current_user:, **)
15
+ query.each { |post| post.update!(published: true) }
16
+ succeed "Published #{query.size} post(s)."
17
+ end
18
+ end
19
+ end
20
+ end
21
+ ```
22
+
23
+ Attach to a resource:
24
+
25
+ ```ruby
26
+ def actions
27
+ action RubyUIAdmin::Actions::PublishPosts
28
+ action RubyUIAdmin::Actions::ImportPosts, arguments: {source: "csv"}
29
+ end
30
+ ```
31
+
32
+ ## Class options
33
+
34
+ | Option | Purpose |
35
+ |---|---|
36
+ | `self.name =` | Button / page label |
37
+ | `self.message =` | Text (HTML allowed) shown on the confirmation page. Accepts a lambda evaluated with `records`/`record`/`resource`/`current_user`, e.g. `-> { "Publish #{records.size} posts?" }` |
38
+ | `self.confirm_button_label =` / `self.cancel_button_label =` | Button labels |
39
+ | `self.standalone =` | `true` for actions that need no record selection (shown on the index) |
40
+ | `self.no_confirmation =` | Skip the confirmation page (planned) |
41
+ | `self.visible =` | Lambda controlling where the action appears. `view` responds to both `view == :show` and predicates `view.show?` / `view.index?` (also `new?`/`edit?`/`form?`/`display?`), plus `resource`/`record` |
42
+
43
+ ## Form fields
44
+
45
+ Declare inputs the same way as resource fields; submitted values arrive in `fields`, a
46
+ hash with **indifferent access** (read by symbol or string). A `file`/`files` field turns the
47
+ action form into a multipart upload automatically:
48
+
49
+ ```ruby
50
+ def fields
51
+ field :reason, as: :text, required: true
52
+ field :notify, as: :boolean
53
+ field :csv_file, as: :file
54
+ end
55
+
56
+ def handle(args)
57
+ upload = args[:fields][:csv_file] # symbol or string both work
58
+ # ...
59
+ end
60
+ ```
61
+
62
+ ## The `handle` method
63
+
64
+ Two signatures are supported:
65
+
66
+ ```ruby
67
+ def handle(query:, fields:, current_user:, resource:, **); end # keyword style
68
+ def handle(args); end # args[:records], args[:fields], args[:current_user], args[:resource]
69
+ ```
70
+
71
+ `query` / `args[:records]` is the array of selected records (empty for standalone actions).
72
+
73
+ ## Response helpers
74
+
75
+ Call these inside `handle`:
76
+
77
+ - `succeed(text)`, `error(text)`, `inform(text)`, `warn(text)` — flash messages
78
+ - `redirect_to(path)` — a String or a lambda (evaluated in the controller, so engine route
79
+ helpers like `resources_posts_path` are available)
80
+
81
+ Route helpers are also available **directly inside `handle`**: bare `*_path`/`*_url` calls
82
+ resolve against the engine routes first, then the host app. Use `main_app.*` for host routes
83
+ and `ruby_ui_admin.*` for engine routes explicitly:
84
+
85
+ ```ruby
86
+ redirect_to resources_posts_path # engine route, bare
87
+ succeed main_app.user_url(record) # host route
88
+ ```
89
+ - `reload` — re-render the originating page (default)
90
+ - `download(content, filename)` — send a file
91
+ - `keep_modal_open` — (planned, for the modal UI)
92
+
93
+ ## Authorization
94
+
95
+ Actions are gated by the resource policy's `act_on?` rule. If it returns false the action
96
+ is hidden and running it is forbidden.
97
+
98
+ Action buttons open an inline **modal** with the form (falling back to a dedicated
99
+ confirmation page when JavaScript is disabled — see [JavaScript](../customization/javascript.md)).
100
+
101
+ ## Bulk actions
102
+
103
+ Non-`standalone` actions become **bulk actions** on the index: a checkbox column appears
104
+ (with a “select all”), and each action runs against the checked rows.
105
+
106
+ - **With JS**: check rows, click the action — the selected ids are injected into the action
107
+ modal, which submits them to the action.
108
+ - **Without JS**: the checkbox selection is submitted to the action's confirmation page (via
109
+ the HTML `form` attribute, so no nested forms), where you confirm and run.
110
+
111
+ `standalone` actions ignore selection and run on their own; record actions also remain
112
+ available on each record's show view.
@@ -0,0 +1,72 @@
1
+ # Authorization with action_policy
2
+
3
+ `ruby_ui_admin` authorizes every request through [action_policy](https://actionpolicy.evilmartians.io)
4
+ by default, using a small set of conventional rule names per resource.
5
+
6
+ > action_policy is the **default adapter**. The backend is pluggable — Pundit, CanCanCan and custom
7
+ > adapters are also supported via `config.authorization_client`. See
8
+ > [Authorization adapters](adapters.md).
9
+
10
+ ## Policies
11
+
12
+ Admin policies live in `app/ruby_ui_admin/policies` (`RubyUIAdmin::Policies::*`) and inherit
13
+ `RubyUIAdmin::BasePolicy`:
14
+
15
+ ```ruby
16
+ module RubyUIAdmin
17
+ module Policies
18
+ class PostPolicy < RubyUIAdmin::BasePolicy
19
+ def index? = true
20
+ def show? = true
21
+ def create? = true
22
+ def update? = true
23
+ def destroy? = user.admin?
24
+
25
+ relation_scope do |relation|
26
+ next relation if user.admin?
27
+ relation.where(published: true)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ ```
33
+
34
+ Attach a policy to a resource with `self.authorization_policy = RubyUIAdmin::Policies::PostPolicy`.
35
+
36
+ ## Rule mapping
37
+
38
+ | Controller action | Rule | Target |
39
+ |---|---|---|
40
+ | index | `index?` | model class |
41
+ | show | `show?` | record |
42
+ | new / create | `create?` | record |
43
+ | edit / update | `update?` | record |
44
+ | destroy | `destroy?` | record |
45
+
46
+ Custom actions are authorized with `act_on?`. Individual fields can be hidden per user with
47
+ `view_<field>?` (and per-view `index_/show_/edit_<field>?`) — see
48
+ [Field-level authorization](field-authorization.md). Association attach/detach rules are
49
+ added in a later phase.
50
+
51
+ ## Scopes
52
+
53
+ The index query is filtered through the policy's `relation_scope`, so each user only sees
54
+ the records they're allowed to. If a policy defines no scope, the relation is returned
55
+ unchanged.
56
+
57
+ ## `explicit_authorization`
58
+
59
+ - `false` (default): a rule the policy doesn't define is **allowed** (falls back to the
60
+ `manage?` default rule on `BasePolicy`).
61
+ - `true`: undefined rules and missing policies are **denied** — a strict, deny-by-default mode.
62
+
63
+ ```ruby
64
+ RubyUIAdmin.configure { |c| c.explicit_authorization = true }
65
+ ```
66
+
67
+ ## Context
68
+
69
+ `BasePolicy` declares `authorize :user` (from `config.current_user_method`) and
70
+ `authorize :true_user` (from `config.true_user_method`, falling back to the current user) —
71
+ so rules can gate on the real user behind an impersonation. Any other context (e.g. a tenant)
72
+ can be layered in via a custom base policy.