ruby_ui_admin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +165 -0
- data/app/assets/stylesheets/ruby_ui_admin/application.tailwind.css +111 -0
- data/app/components/ruby_ui_admin/ui/base.rb +26 -0
- data/app/components/ruby_ui_admin/ui/icon.rb +79 -0
- data/app/components/ruby_ui_admin/ui/native_select.rb +37 -0
- data/app/components/ruby_ui_admin/ui/native_select_group.rb +18 -0
- data/app/components/ruby_ui_admin/ui/native_select_icon.rb +42 -0
- data/app/components/ruby_ui_admin/ui/native_select_option.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination.rb +62 -0
- data/app/components/ruby_ui_admin/ui/pagination_content.rb +18 -0
- data/app/components/ruby_ui_admin/ui/pagination_ellipsis.rb +45 -0
- data/app/components/ruby_ui_admin/ui/pagination_item.rb +32 -0
- data/app/components/ruby_ui_admin/ui/select.rb +29 -0
- data/app/components/ruby_ui_admin/ui/toast_action.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_cancel.rb +30 -0
- data/app/components/ruby_ui_admin/ui/toast_close.rb +43 -0
- data/app/components/ruby_ui_admin/ui/toast_description.rb +21 -0
- data/app/components/ruby_ui_admin/ui/toast_icon.rb +69 -0
- data/app/components/ruby_ui_admin/ui/toast_item.rb +75 -0
- data/app/components/ruby_ui_admin/ui/toast_region.rb +147 -0
- data/app/components/ruby_ui_admin/ui/toast_title.rb +21 -0
- data/app/components/ruby_ui_admin/ui.rb +12 -0
- data/app/components/ruby_ui_admin/views/action.rb +37 -0
- data/app/components/ruby_ui_admin/views/action_form.rb +104 -0
- data/app/components/ruby_ui_admin/views/action_trigger.rb +148 -0
- data/app/components/ruby_ui_admin/views/actions_menu.rb +34 -0
- data/app/components/ruby_ui_admin/views/base.rb +301 -0
- data/app/components/ruby_ui_admin/views/card_component.rb +69 -0
- data/app/components/ruby_ui_admin/views/dashboard.rb +40 -0
- data/app/components/ruby_ui_admin/views/docs.rb +91 -0
- data/app/components/ruby_ui_admin/views/field_input.rb +156 -0
- data/app/components/ruby_ui_admin/views/field_value.rb +301 -0
- data/app/components/ruby_ui_admin/views/filter_bar.rb +111 -0
- data/app/components/ruby_ui_admin/views/form.rb +151 -0
- data/app/components/ruby_ui_admin/views/home.rb +35 -0
- data/app/components/ruby_ui_admin/views/index.rb +324 -0
- data/app/components/ruby_ui_admin/views/path_helpers.rb +36 -0
- data/app/components/ruby_ui_admin/views/rails_helpers.rb +15 -0
- data/app/components/ruby_ui_admin/views/show.rb +77 -0
- data/app/components/ruby_ui_admin/views/show_fields.rb +37 -0
- data/app/components/ruby_ui_admin/views/show_tab.rb +58 -0
- data/app/components/ruby_ui_admin/views/structure_renderer.rb +148 -0
- data/app/components/ruby_ui_admin/views/translation.rb +12 -0
- data/app/controllers/ruby_ui_admin/actions_controller.rb +162 -0
- data/app/controllers/ruby_ui_admin/application_controller.rb +81 -0
- data/app/controllers/ruby_ui_admin/dashboards_controller.rb +12 -0
- data/app/controllers/ruby_ui_admin/docs_controller.rb +166 -0
- data/app/controllers/ruby_ui_admin/home_controller.rb +18 -0
- data/app/controllers/ruby_ui_admin/resources_controller.rb +259 -0
- data/config/locales/ruby_ui_admin.en.yml +59 -0
- data/config/locales/ruby_ui_admin.pt-BR.yml +59 -0
- data/config/routes/dynamic_routes.rb +11 -0
- data/config/routes.rb +23 -0
- data/docs/README.md +53 -0
- data/docs/actions/overview.md +112 -0
- data/docs/authorization/action-policy.md +72 -0
- data/docs/authorization/adapters.md +101 -0
- data/docs/authorization/explicit-mode.md +45 -0
- data/docs/authorization/field-authorization.md +53 -0
- data/docs/authorization/policies.md +62 -0
- data/docs/authorization/scopes.md +48 -0
- data/docs/customization/controllers.md +62 -0
- data/docs/customization/ejecting.md +55 -0
- data/docs/customization/javascript.md +161 -0
- data/docs/customization/theming-rubyui.md +80 -0
- data/docs/dashboards/overview.md +65 -0
- data/docs/fields/overview.md +94 -0
- data/docs/filters/overview.md +73 -0
- data/docs/generators-and-tasks/overview.md +42 -0
- data/docs/getting-started/authentication.md +98 -0
- data/docs/getting-started/configuration.md +174 -0
- data/docs/getting-started/installation.md +201 -0
- data/docs/getting-started/internationalization.md +50 -0
- data/docs/getting-started/practical-guide.md +310 -0
- data/docs/resources/associations.md +66 -0
- data/docs/resources/index-customization.md +68 -0
- data/docs/resources/overview.md +50 -0
- data/docs/resources/scopes.md +54 -0
- data/docs/resources/tabs-panels.md +65 -0
- data/lib/generators/ruby_ui_admin/action/action_generator.rb +21 -0
- data/lib/generators/ruby_ui_admin/action/templates/action.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/assets/assets_generator.rb +71 -0
- data/lib/generators/ruby_ui_admin/card/card_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/card/templates/chart_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/metric_card.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/card/templates/partial_card.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/components/components_generator.rb +43 -0
- data/lib/generators/ruby_ui_admin/controller/controller_generator.rb +30 -0
- data/lib/generators/ruby_ui_admin/controller/templates/controller.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/dashboard/dashboard_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/dashboard/templates/dashboard.rb.tt +14 -0
- data/lib/generators/ruby_ui_admin/eject/eject_generator.rb +55 -0
- data/lib/generators/ruby_ui_admin/filter/filter_generator.rb +28 -0
- data/lib/generators/ruby_ui_admin/filter/templates/boolean_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/filter/templates/select_filter.rb.tt +19 -0
- data/lib/generators/ruby_ui_admin/filter/templates/text_filter.rb.tt +13 -0
- data/lib/generators/ruby_ui_admin/install/install_generator.rb +62 -0
- data/lib/generators/ruby_ui_admin/install/templates/initializer.rb.tt +41 -0
- data/lib/generators/ruby_ui_admin/locales/locales_generator.rb +22 -0
- data/lib/generators/ruby_ui_admin/policy/policy_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/policy/templates/policy.rb.tt +24 -0
- data/lib/generators/ruby_ui_admin/resource/resource_generator.rb +84 -0
- data/lib/generators/ruby_ui_admin/resource/templates/resource.rb.tt +15 -0
- data/lib/generators/ruby_ui_admin/scope/scope_generator.rb +18 -0
- data/lib/generators/ruby_ui_admin/scope/templates/scope.rb.tt +11 -0
- data/lib/ruby_ui_admin/authorization/action_policy_adapter.rb +69 -0
- data/lib/ruby_ui_admin/authorization/adapter.rb +80 -0
- data/lib/ruby_ui_admin/authorization/can_can_can_adapter.rb +70 -0
- data/lib/ruby_ui_admin/authorization/pundit_adapter.rb +86 -0
- data/lib/ruby_ui_admin/authorization.rb +40 -0
- data/lib/ruby_ui_admin/base_action.rb +211 -0
- data/lib/ruby_ui_admin/base_dashboard.rb +70 -0
- data/lib/ruby_ui_admin/base_policy.rb +38 -0
- data/lib/ruby_ui_admin/base_resource.rb +503 -0
- data/lib/ruby_ui_admin/cards/base_card.rb +59 -0
- data/lib/ruby_ui_admin/cards/chart_card.rb +16 -0
- data/lib/ruby_ui_admin/cards/metric_card.rb +22 -0
- data/lib/ruby_ui_admin/cards/partial_card.rb +14 -0
- data/lib/ruby_ui_admin/configuration.rb +177 -0
- data/lib/ruby_ui_admin/current.rb +20 -0
- data/lib/ruby_ui_admin/dashboard_manager.rb +52 -0
- data/lib/ruby_ui_admin/engine.rb +80 -0
- data/lib/ruby_ui_admin/execution_context.rb +36 -0
- data/lib/ruby_ui_admin/fields/association_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/badge_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/base_field.rb +208 -0
- data/lib/ruby_ui_admin/fields/belongs_to_field.rb +49 -0
- data/lib/ruby_ui_admin/fields/boolean_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/boolean_group_field.rb +54 -0
- data/lib/ruby_ui_admin/fields/code_field.rb +18 -0
- data/lib/ruby_ui_admin/fields/date_field.rb +34 -0
- data/lib/ruby_ui_admin/fields/date_time_field.rb +37 -0
- data/lib/ruby_ui_admin/fields/field_manager.rb +27 -0
- data/lib/ruby_ui_admin/fields/file_field.rb +67 -0
- data/lib/ruby_ui_admin/fields/files_field.rb +62 -0
- data/lib/ruby_ui_admin/fields/has_and_belongs_to_many_field.rb +9 -0
- data/lib/ruby_ui_admin/fields/has_many_field.rb +24 -0
- data/lib/ruby_ui_admin/fields/has_one_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/hidden_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/id_field.rb +19 -0
- data/lib/ruby_ui_admin/fields/key_value_field.rb +41 -0
- data/lib/ruby_ui_admin/fields/number_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/password_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/record_link_field.rb +45 -0
- data/lib/ruby_ui_admin/fields/select_field.rb +71 -0
- data/lib/ruby_ui_admin/fields/status_field.rb +21 -0
- data/lib/ruby_ui_admin/fields/text_field.rb +14 -0
- data/lib/ruby_ui_admin/fields/textarea_field.rb +13 -0
- data/lib/ruby_ui_admin/fields/url_field.rb +21 -0
- data/lib/ruby_ui_admin/filters/base_filter.rb +81 -0
- data/lib/ruby_ui_admin/filters/boolean_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/multiple_select_filter.rb +11 -0
- data/lib/ruby_ui_admin/filters/select_filter.rb +9 -0
- data/lib/ruby_ui_admin/filters/text_filter.rb +9 -0
- data/lib/ruby_ui_admin/menu/builder.rb +94 -0
- data/lib/ruby_ui_admin/resource_manager.rb +82 -0
- data/lib/ruby_ui_admin/scopes/base_scope.rb +107 -0
- data/lib/ruby_ui_admin/services/authorization_service.rb +38 -0
- data/lib/ruby_ui_admin/structure.rb +40 -0
- data/lib/ruby_ui_admin/tailwind_source.rb +65 -0
- data/lib/ruby_ui_admin/version.rb +5 -0
- data/lib/ruby_ui_admin/view.rb +34 -0
- data/lib/ruby_ui_admin.rb +110 -0
- data/lib/tasks/ruby_ui_admin.rake +71 -0
- data/public/ruby-ui-admin-assets/application.css +2 -0
- data/public/ruby-ui-admin-assets/controllers/index.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/rua--bulk-select_controller.js +19 -0
- data/public/ruby-ui-admin-assets/controllers/rua--confirm_controller.js +51 -0
- data/public/ruby-ui-admin-assets/controllers/rua--dialog_controller.js +55 -0
- data/public/ruby-ui-admin-assets/controllers/rua--row-link_controller.js +16 -0
- data/public/ruby-ui-admin-assets/controllers/rua--tabs_controller.js +36 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--combobox_controller.js +197 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_content_controller.js +7 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sheet_controller.js +15 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--sidebar_controller.js +67 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toast_controller.js +151 -0
- data/public/ruby-ui-admin-assets/controllers/ruby_ui--toaster_controller.js +314 -0
- data/public/ruby-ui-admin-assets/vendor/stimulus.js +2567 -0
- data/public/ruby-ui-admin-assets/vendor/turbo.js +7178 -0
- metadata +379 -0
|
@@ -0,0 +1,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.
|