flexi_admin 0.0.5 → 0.0.6
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 +4 -4
- data/README.md +254 -18
- data/lib/flexi_admin/components/helpers/action_helper.rb +21 -0
- data/lib/flexi_admin/components/helpers/frontend_helper.rb +23 -0
- data/lib/flexi_admin/components/helpers/link_helper.rb +35 -2
- data/lib/flexi_admin/components/helpers/resource_helper.rb +79 -14
- data/lib/flexi_admin/components/helpers/url_helper.rb +15 -0
- data/lib/flexi_admin/components/nav/breadcrumbs_component.html.slim +13 -0
- data/lib/flexi_admin/components/nav/breadcrumbs_component.rb +86 -0
- data/lib/flexi_admin/components/resource/autocomplete_component.html.slim +19 -7
- data/lib/flexi_admin/components/resource/autocomplete_component.rb +41 -11
- data/lib/flexi_admin/components/resource/button_select_component.html.slim +2 -2
- data/lib/flexi_admin/components/resource/button_select_component.rb +8 -1
- data/lib/flexi_admin/components/resource/filter_component.html.slim +47 -0
- data/lib/flexi_admin/components/resource/filter_component.rb +51 -0
- data/lib/flexi_admin/components/resource/form_component.rb +16 -1
- data/lib/flexi_admin/components/resource/form_element_component.rb +1 -1
- data/lib/flexi_admin/components/resource/form_mixin.rb +186 -42
- data/lib/flexi_admin/components/resource/link_action_component.html.slim +2 -2
- data/lib/flexi_admin/components/resource/link_action_component.rb +3 -2
- data/lib/flexi_admin/components/resource/view_component.html.slim +14 -4
- data/lib/flexi_admin/components/resource/view_component.rb +11 -2
- data/lib/flexi_admin/components/resources/bulk_action/button_component.rb +8 -1
- data/lib/flexi_admin/components/resources/bulk_action/modal_component.html.slim +21 -15
- data/lib/flexi_admin/components/resources/bulk_action/modal_component.rb +16 -8
- data/lib/flexi_admin/components/resources/bulk_action/row_action_button_component.html.slim +16 -0
- data/lib/flexi_admin/components/resources/bulk_action/row_action_button_component.rb +78 -0
- data/lib/flexi_admin/components/resources/bulk_action/row_actions_dropdown_component.html.slim +15 -0
- data/lib/flexi_admin/components/resources/bulk_action/row_actions_dropdown_component.rb +33 -0
- data/lib/flexi_admin/components/resources/grid_view/card_component.html.slim +4 -1
- data/lib/flexi_admin/components/resources/grid_view/card_component.rb +4 -1
- data/lib/flexi_admin/components/resources/grid_view/grid_component.html.slim +3 -1
- data/lib/flexi_admin/components/resources/grid_view/grid_component.rb +4 -2
- data/lib/flexi_admin/components/resources/grid_view_component.rb +7 -2
- data/lib/flexi_admin/components/resources/index_page_component.html.slim +5 -4
- data/lib/flexi_admin/components/resources/index_page_component.rb +1 -0
- data/lib/flexi_admin/components/resources/list_view/table_component.html.slim +1 -1
- data/lib/flexi_admin/components/resources/list_view_component.rb +2 -1
- data/lib/flexi_admin/components/resources/pagination_component.html.slim +51 -39
- data/lib/flexi_admin/components/resources/pagination_component.rb +52 -47
- data/lib/flexi_admin/components/resources/switch_view_component.html.slim +2 -2
- data/lib/flexi_admin/components/resources/view_component.html.slim +15 -6
- data/lib/flexi_admin/components/shared/medium_component.html.slim +1 -1
- data/lib/flexi_admin/components/shared/table/header_item_component.html.slim +1 -1
- data/lib/flexi_admin/components/shared/table/header_item_component.rb +59 -61
- data/lib/flexi_admin/components/shared/tabs_component.html.slim +26 -0
- data/lib/flexi_admin/components/shared/tabs_component.rb +68 -0
- data/lib/flexi_admin/components.rb +11 -3
- data/lib/flexi_admin/config.rb +21 -3
- data/lib/flexi_admin/controllers/modals_controller.rb +4 -2
- data/lib/flexi_admin/controllers/resources_controller.rb +246 -51
- data/lib/flexi_admin/engine.rb +4 -0
- data/lib/flexi_admin/helpers/application_helper.rb +3 -0
- data/lib/flexi_admin/javascript/controllers/add_row_controller.js +118 -0
- data/lib/flexi_admin/javascript/controllers/autocomplete_controller.js +17 -1
- data/lib/flexi_admin/javascript/controllers/bulk_action_controller.js +192 -33
- data/lib/flexi_admin/javascript/controllers/copy_to_clipboard_controller.js +16 -0
- data/lib/flexi_admin/javascript/controllers/datalist_controller.js +1 -1
- data/lib/flexi_admin/javascript/controllers/delete_controller.js +40 -0
- data/lib/flexi_admin/javascript/controllers/expandable_field_controller.js +15 -0
- data/lib/flexi_admin/javascript/controllers/filter_auto_submit_controller.js +93 -0
- data/lib/flexi_admin/javascript/controllers/index.js +18 -0
- data/lib/flexi_admin/javascript/controllers/pagination_controller.js +8 -0
- data/lib/flexi_admin/javascript/controllers/tabs_controller.js +24 -0
- data/lib/flexi_admin/models/concerns/application_resource.rb +6 -0
- data/lib/flexi_admin/models/concerns/parentable.rb +3 -0
- data/lib/flexi_admin/models/context_params.rb +35 -10
- data/lib/flexi_admin/models/resources/context.rb +4 -0
- data/lib/flexi_admin/routes.rb +7 -1
- data/lib/flexi_admin/services/create_resource.rb +6 -6
- data/lib/flexi_admin/version.rb +1 -1
- data/lib/flexi_admin/views/shared/_reload_frame.slim +19 -0
- data/lib/flexi_admin.rb +0 -3
- metadata +171 -10
|
@@ -2,19 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
module FlexiAdmin::Controllers::ResourcesController
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
DEFAULT_CONTROLLER_PARAMS_KEYS = %i[controller action subdomain commit button].freeze
|
|
5
7
|
# rescue_from RuntimeError, with: :handle_runtime_error
|
|
6
8
|
|
|
7
9
|
included do
|
|
10
|
+
helper FlexiAdmin::Helpers::ApplicationHelper
|
|
8
11
|
before_action :context_params
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
if defined?(CanCan::AccessDenied)
|
|
14
|
+
rescue_from CanCan::AccessDenied do |exception|
|
|
15
|
+
flash[:error] = FlexiAdmin::Models::Toast.new(exception.message)
|
|
16
|
+
respond_to do |format|
|
|
17
|
+
format.html do
|
|
18
|
+
render 'shared/not_authorized'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
format.turbo_stream do
|
|
22
|
+
render_toasts
|
|
23
|
+
end
|
|
18
24
|
end
|
|
19
25
|
end
|
|
20
26
|
end
|
|
@@ -29,20 +35,32 @@ module FlexiAdmin::Controllers::ResourcesController
|
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
def render_index(resources, target: nil)
|
|
32
|
-
target
|
|
38
|
+
target = target || (fa_sorting_active? ? resource_class.model_name.plural : context_params.frame)
|
|
39
|
+
|
|
33
40
|
respond_to do |format|
|
|
34
41
|
format.html do
|
|
35
|
-
component_class =
|
|
36
|
-
puts
|
|
37
|
-
render component_class.new(resources, context_params
|
|
42
|
+
component_class = namespaced_class('namespace', resource_class.name, "IndexPageComponent")
|
|
43
|
+
puts("component_class: #{component_class}") if defined?(Rails) && Rails.env.development?
|
|
44
|
+
render component_class.new(resources, context_params: context_params,
|
|
45
|
+
scope: resource_class.model_name.plural)
|
|
38
46
|
end
|
|
39
47
|
format.turbo_stream do
|
|
40
|
-
component_class =
|
|
41
|
-
render turbo_stream: turbo_stream.replace(target, component_class.new(resources, context_params
|
|
48
|
+
component_class = namespaced_class('namespace', resource_class.name, "ResourcesComponent")
|
|
49
|
+
render turbo_stream: turbo_stream.replace(target, component_class.new(resources, context_params: context_params,
|
|
50
|
+
scope: resource_class.model_name.plural,
|
|
51
|
+
parent: parent_instance))
|
|
42
52
|
end
|
|
43
53
|
end
|
|
44
54
|
end
|
|
45
55
|
|
|
56
|
+
def reload_frame(scope = nil)
|
|
57
|
+
scope ||= context_params.reload_frame.presence || context_params.scope&.tr('/', '_') || resource_class.model_name.plural
|
|
58
|
+
render turbo_stream: [
|
|
59
|
+
turbo_stream.append('system', partial: 'shared/reload_frame', locals: { scope: scope, referrer: request.referrer }),
|
|
60
|
+
turbo_stream.append('toasts', partial: 'shared/toasts')
|
|
61
|
+
]
|
|
62
|
+
end
|
|
63
|
+
|
|
46
64
|
# Deprecated
|
|
47
65
|
def reload_page
|
|
48
66
|
render turbo_stream: [
|
|
@@ -52,17 +70,66 @@ module FlexiAdmin::Controllers::ResourcesController
|
|
|
52
70
|
end
|
|
53
71
|
|
|
54
72
|
def redirect_to_path(path)
|
|
55
|
-
render turbo_stream: turbo_stream.append('system', partial: 'shared/redirect', locals: { path: })
|
|
73
|
+
render turbo_stream: turbo_stream.append('system', partial: 'shared/redirect', locals: { path: path })
|
|
56
74
|
end
|
|
57
75
|
|
|
76
|
+
# Prefix for the per-scope cookie that remembers a user's page-size choice.
|
|
77
|
+
PER_PAGE_COOKIE_PREFIX = "fa_per_page_"
|
|
78
|
+
|
|
58
79
|
def context_params
|
|
59
|
-
@context_params ||= FlexiAdmin::Models::ContextParams.new(
|
|
80
|
+
@context_params ||= FlexiAdmin::Models::ContextParams.new(remembered_per_page_params)
|
|
60
81
|
end
|
|
61
82
|
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
# Admin users work through long lists and were re-picking the same page size
|
|
84
|
+
# on every visit, because `fa_per_page` only ever lived in the URL. Persist
|
|
85
|
+
# the choice per scope in a cookie: an explicit `fa_per_page` is recorded,
|
|
86
|
+
# and a request without one inherits whatever that scope was last set to.
|
|
87
|
+
def remembered_per_page_params
|
|
88
|
+
permitted = context_permitted_params
|
|
89
|
+
cookie_key = per_page_cookie_key
|
|
90
|
+
|
|
91
|
+
chosen = permitted[FlexiAdmin::Models::ContextParams::MAP[:per_page]]
|
|
92
|
+
if chosen.present?
|
|
93
|
+
cookies[cookie_key] = { value: chosen.to_s, expires: 1.year.from_now } if valid_per_page?(chosen)
|
|
94
|
+
return permitted
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
remembered = cookies[cookie_key]
|
|
98
|
+
return permitted unless valid_per_page?(remembered)
|
|
99
|
+
|
|
100
|
+
permitted.merge(FlexiAdmin::Models::ContextParams::MAP[:per_page] => remembered)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Keyed by scope where one is given (several scopes can render on one page),
|
|
104
|
+
# falling back to the controller for a plain index.
|
|
105
|
+
def per_page_cookie_key
|
|
106
|
+
scope = params[FlexiAdmin::Models::ContextParams::MAP[:scope]].presence || controller_path
|
|
107
|
+
"#{PER_PAGE_COOKIE_PREFIX}#{scope.to_s.parameterize(separator: '_')}"
|
|
64
108
|
end
|
|
65
109
|
|
|
110
|
+
# A cookie is user-writable, so never let it widen a query beyond the sizes
|
|
111
|
+
# the app itself offers.
|
|
112
|
+
def valid_per_page?(value)
|
|
113
|
+
return false if value.blank?
|
|
114
|
+
|
|
115
|
+
FlexiAdmin::Config.configuration.paginate_per_options.map(&:to_s).include?(value.to_s)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def context_permitted_params
|
|
119
|
+
@context_permitted_params ||= begin
|
|
120
|
+
permitted_keys = FlexiAdmin::Models::ContextParams.permitted_params_keys.dup
|
|
121
|
+
|
|
122
|
+
array_param_keys = params.select { |_k, v| v.is_a?(Array) }.keys
|
|
123
|
+
array_params = array_param_keys.map { |k| { k => [] } }
|
|
124
|
+
scalar_params = params.except(:controller, *DEFAULT_CONTROLLER_PARAMS_KEYS).keys - array_param_keys
|
|
125
|
+
|
|
126
|
+
params.permit(
|
|
127
|
+
*permitted_keys,
|
|
128
|
+
*scalar_params,
|
|
129
|
+
*array_params
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
66
133
|
|
|
67
134
|
def handle_runtime_error(error)
|
|
68
135
|
return BugTracker.notify(error) if true
|
|
@@ -73,18 +140,29 @@ module FlexiAdmin::Controllers::ResourcesController
|
|
|
73
140
|
end
|
|
74
141
|
|
|
75
142
|
def create
|
|
76
|
-
authorize! :create, resource_class
|
|
143
|
+
authorize! :create, resource_class if defined?(CanCan)
|
|
77
144
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
145
|
+
result = nil
|
|
146
|
+
if respond_to?(:create_service)
|
|
147
|
+
Rails.logger.debug "ResourcesController: #{__method__} using custom controller create_service."
|
|
148
|
+
result = create_service(resource: parent_instance, params: resource_params)
|
|
149
|
+
else
|
|
150
|
+
create_service = begin
|
|
151
|
+
class_name = namespaced_class('namespace', resource_class.model_name.plural.camelize, "Services", "Create")
|
|
152
|
+
Rails.logger.debug "ResourcesController: #{__method__} using class_name: #{class_name}"
|
|
153
|
+
class_name
|
|
154
|
+
rescue NameError
|
|
155
|
+
Rails.logger.debug "ResourcesController: #{__method__} using generic FlexiAdmin::Services::CreateResource"
|
|
156
|
+
FlexiAdmin::Services::CreateResource
|
|
157
|
+
end
|
|
83
158
|
|
|
84
|
-
|
|
159
|
+
result = create_service.run(resource_class:, params: resource_params)
|
|
160
|
+
end
|
|
85
161
|
|
|
86
162
|
if result.valid?
|
|
87
|
-
|
|
163
|
+
namespace = FlexiAdmin::Config.configuration.namespace
|
|
164
|
+
path_segments = namespace.present? ? [namespace.to_sym, result.resource] : [result.resource]
|
|
165
|
+
redirect_to_path polymorphic_path(path_segments)
|
|
88
166
|
else
|
|
89
167
|
render_new_resource_form(result.resource)
|
|
90
168
|
end
|
|
@@ -92,12 +170,12 @@ module FlexiAdmin::Controllers::ResourcesController
|
|
|
92
170
|
|
|
93
171
|
def show
|
|
94
172
|
@resource = resource_class.find(params[:id])
|
|
95
|
-
authorize! :show, @resource
|
|
173
|
+
authorize! :show, @resource if defined?(CanCan)
|
|
96
174
|
|
|
97
175
|
respond_to do |format|
|
|
98
176
|
format.html do
|
|
99
|
-
component ||=
|
|
100
|
-
render component.new(@resource, context_params:, scope: resource_class.
|
|
177
|
+
component ||= namespaced_class('namespace', resource_class.name, "Show", "PageComponent")
|
|
178
|
+
render component.new(@resource, context_params:, scope: resource_class.model_name.plural)
|
|
101
179
|
end
|
|
102
180
|
format.turbo_stream do
|
|
103
181
|
render turbo_stream: turbo_stream.replace(context_params.frame)
|
|
@@ -107,30 +185,63 @@ module FlexiAdmin::Controllers::ResourcesController
|
|
|
107
185
|
|
|
108
186
|
def edit
|
|
109
187
|
@resource = resource_class.find(params[:id])
|
|
110
|
-
authorize! :update, @resource
|
|
188
|
+
authorize! :update, @resource if defined?(CanCan)
|
|
111
189
|
|
|
112
190
|
render_edit_resource_form(disabled: disabled?(context_params.form_disabled))
|
|
113
191
|
end
|
|
114
192
|
|
|
115
193
|
def update
|
|
116
194
|
@resource = resource_class.find(params[:id])
|
|
117
|
-
authorize! :update, @resource
|
|
195
|
+
authorize! :update, @resource if defined?(CanCan)
|
|
118
196
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
197
|
+
result = nil
|
|
198
|
+
if result = try(:update_service, resource: @resource, params: resource_params)
|
|
199
|
+
Rails.logger.debug "ResourcesController: #{__method__} using custom controller update_service."
|
|
200
|
+
result = update_service(resource: @resource, params: resource_params)
|
|
201
|
+
else
|
|
202
|
+
update_service = begin
|
|
203
|
+
class_name = namespaced_class('namespace', 'namespace',"#{resource_class.model_name.plural.camelize}", "Services", "Update")
|
|
204
|
+
Rails.logger.debug "ResourcesController: #{__method__} using class_name: #{class_name}"
|
|
205
|
+
|
|
206
|
+
class_name
|
|
207
|
+
rescue NameError
|
|
208
|
+
Rails.logger.debug "ResourcesController: #{__method__} using generic FlexiAdmin::Services::UpdateResource"
|
|
209
|
+
FlexiAdmin::Services::UpdateResource
|
|
210
|
+
end
|
|
124
211
|
|
|
125
|
-
|
|
212
|
+
result = update_service.run(resource: @resource, params: resource_params)
|
|
213
|
+
end
|
|
126
214
|
|
|
127
215
|
if result.valid?
|
|
128
216
|
render_edit_resource_form(disabled: disabled?(true))
|
|
129
217
|
else
|
|
218
|
+
Rails.logger.debug "ResourcesController: #{__method__} errors: #{result.errors.respond_to?(:full_messages) ? result.errors.full_messages : result.errors}"
|
|
130
219
|
render_edit_resource_form(disabled: disabled?(false))
|
|
131
220
|
end
|
|
132
221
|
end
|
|
133
222
|
|
|
223
|
+
def destroy
|
|
224
|
+
@resource = resource_class.find(params[:id])
|
|
225
|
+
authorize! :destroy, @resource if defined?(CanCan)
|
|
226
|
+
|
|
227
|
+
if @resource.destroy
|
|
228
|
+
success_message = I18n.t('flexi_admin.messages.destroy.success',
|
|
229
|
+
resource: resource_class.model_name.human)
|
|
230
|
+
flash[:success] = FlexiAdmin::Models::Toast.new(success_message)
|
|
231
|
+
|
|
232
|
+
namespace = FlexiAdmin::Config.configuration.namespace
|
|
233
|
+
path_segments = namespace.present? ? [namespace.to_sym, resource_class] : [resource_class]
|
|
234
|
+
index_path = polymorphic_path(path_segments)
|
|
235
|
+
|
|
236
|
+
redirect_to_path index_path
|
|
237
|
+
else
|
|
238
|
+
error_message = I18n.t('flexi_admin.messages.destroy.failure',
|
|
239
|
+
resource: resource_class.model_name.human)
|
|
240
|
+
flash[:error] = FlexiAdmin::Models::Toast.new(error_message)
|
|
241
|
+
render_toasts
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
134
245
|
def render_edit_resource_form(disabled: true)
|
|
135
246
|
respond_to do |format|
|
|
136
247
|
format.html do
|
|
@@ -159,28 +270,58 @@ module FlexiAdmin::Controllers::ResourcesController
|
|
|
159
270
|
# Unscoped is needed to get the resources that are not deleted, archived, etc.
|
|
160
271
|
# It should be ok, since we control the ids in the frontend
|
|
161
272
|
@resources = resource_class.unscoped.where(id: ids)
|
|
162
|
-
authorize! :edit, @resources
|
|
163
273
|
|
|
164
|
-
|
|
274
|
+
authorize! :edit, @resources.presence&.base_class || resource_class if defined?(CanCan)
|
|
275
|
+
|
|
276
|
+
# TODO: improve this
|
|
277
|
+
params.merge!(current_user: current_user)
|
|
278
|
+
bulk_processor = if params[:processor].gsub('-', '/').camelize.include?(FlexiAdmin::Config.configuration.module_namespace)
|
|
279
|
+
namespaced_class(params[:processor].gsub('-', '/').camelize, "Processor").new(@resources, params)
|
|
280
|
+
else
|
|
281
|
+
namespaced_class(FlexiAdmin::Config.configuration.module_namespace, params[:processor].gsub('-', '/').camelize, "Processor").new(@resources, params)
|
|
282
|
+
end
|
|
283
|
+
|
|
165
284
|
result = bulk_processor.perform
|
|
166
285
|
|
|
167
286
|
redirect_to_path result.path and return if result.result == :redirect
|
|
168
287
|
|
|
169
288
|
flash[result.result] = FlexiAdmin::Models::Toast.new(result.message)
|
|
170
289
|
|
|
171
|
-
|
|
290
|
+
if result.redirect_to.present?
|
|
291
|
+
redirect_to_path(result.redirect_to)
|
|
292
|
+
elsif result.try(:reload) == :page || context_params.reload == :page
|
|
293
|
+
reload_page
|
|
294
|
+
else
|
|
295
|
+
reload_frame
|
|
296
|
+
end
|
|
172
297
|
end
|
|
173
298
|
|
|
174
|
-
def autocomplete(includes: nil)
|
|
175
|
-
base_query =
|
|
176
|
-
|
|
299
|
+
def autocomplete(includes: nil, joins: nil, order: nil)
|
|
300
|
+
base_query = if context_params.params[:custom_scope].present?
|
|
301
|
+
Rails.logger.debug "Autocomplete: custom scope: #{context_params.params[:custom_scope]}"
|
|
302
|
+
apply_named_custom_scope(resource_class.with_parent(parent_instance), context_params.params[:custom_scope])
|
|
303
|
+
else
|
|
304
|
+
Rails.logger.debug "Autocomplete: no custom scope, using parent instance"
|
|
305
|
+
resource_class.with_parent(parent_instance)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# A focused-but-empty field asks for the default suggestions rather than
|
|
309
|
+
# running a fulltext search for "".
|
|
310
|
+
suggesting = params[:q].blank?
|
|
311
|
+
base_query = base_query.fulltext(params[:q]) unless suggesting
|
|
312
|
+
base_query = base_query.joins(joins) if joins.present?
|
|
177
313
|
base_query = base_query.includes(includes) if includes.present?
|
|
314
|
+
base_query = base_query.order(order) if order.present?
|
|
315
|
+
Rails.logger.debug "Autocomplete: included #{includes.present? ? includes : "none"}"
|
|
316
|
+
Rails.logger.debug "Autocomplete: base_query SQL #{base_query.to_sql}"
|
|
317
|
+
|
|
178
318
|
results_count = base_query.count
|
|
179
|
-
results = base_query.limit(100)
|
|
319
|
+
results = base_query.limit(suggesting ? FlexiAdmin::Config.configuration.autocomplete_suggestions : 100)
|
|
320
|
+
Rails.logger.debug "Autocomplete: base_query results after fulltext search: #{base_query.count}"
|
|
180
321
|
|
|
181
322
|
render FlexiAdmin::Components::Shared::Autocomplete::ResultsComponent.new(results:,
|
|
182
|
-
|
|
183
|
-
|
|
323
|
+
results_count:,
|
|
324
|
+
context_params:), layout: false
|
|
184
325
|
end
|
|
185
326
|
|
|
186
327
|
def datalist
|
|
@@ -189,22 +330,25 @@ module FlexiAdmin::Controllers::ResourcesController
|
|
|
189
330
|
.order(*context_params.params[:ac_fields].map(&:to_sym))
|
|
190
331
|
.pluck(*context_params.params[:ac_fields].map(&:to_sym))
|
|
191
332
|
.uniq
|
|
333
|
+
|
|
192
334
|
render FlexiAdmin::Components::Shared::Autocomplete::ResultsComponent.new(results:,
|
|
193
|
-
|
|
335
|
+
context_params:), layout: false
|
|
194
336
|
end
|
|
195
337
|
|
|
196
338
|
private
|
|
197
339
|
|
|
198
340
|
def parent_instance
|
|
199
341
|
@parent_instance ||= locate_resource(context_params.parent)
|
|
342
|
+
Rails.logger.debug "Parent instance: #{@parent_instance.present? ? format("@parent_instance: %s##{@parent_instance.id}", @parent_instance.class.name) : 'nil'}"
|
|
343
|
+
@parent_instance
|
|
200
344
|
end
|
|
201
345
|
|
|
202
346
|
def edit_form_component_instance(disabled)
|
|
203
|
-
|
|
347
|
+
namespaced_class('namespace', "#{@resource.class.name}", "Show", "EditFormComponent").new(@resource, disabled: disabled?(disabled))
|
|
204
348
|
end
|
|
205
349
|
|
|
206
350
|
def new_form_component_instance(resource)
|
|
207
|
-
"#{resource.class.name}
|
|
351
|
+
namespaced_class('namespace', "#{resource.class.name}", "NewFormComponent").new(resource, parent: parent_instance)
|
|
208
352
|
end
|
|
209
353
|
|
|
210
354
|
def locate_resource(encoded_gid)
|
|
@@ -219,13 +363,30 @@ module FlexiAdmin::Controllers::ResourcesController
|
|
|
219
363
|
end
|
|
220
364
|
|
|
221
365
|
def resource_class
|
|
222
|
-
|
|
366
|
+
controller_path = self.class.name.deconstantize
|
|
367
|
+
config_namespace = FlexiAdmin::Config.configuration.namespace&.camelize
|
|
368
|
+
|
|
369
|
+
if config_namespace.present? && controller_path.start_with?(config_namespace)
|
|
370
|
+
if controller_path == config_namespace
|
|
371
|
+
controller_name.classify.constantize
|
|
372
|
+
else
|
|
373
|
+
module_path = controller_path.sub(/^#{Regexp.escape(config_namespace)}::/, '')
|
|
374
|
+
class_name = "#{module_path}::#{controller_name.classify}"
|
|
375
|
+
Object.const_get(class_name)
|
|
376
|
+
end
|
|
377
|
+
else
|
|
378
|
+
controller_name.classify.constantize
|
|
379
|
+
end
|
|
223
380
|
end
|
|
224
381
|
|
|
225
382
|
def fa_sorted?
|
|
226
383
|
fa_sort.present? && fa_order.present? && fa_order != "default"
|
|
227
384
|
end
|
|
228
385
|
|
|
386
|
+
def fa_sorting_active?
|
|
387
|
+
fa_sort.present? && fa_order.present?
|
|
388
|
+
end
|
|
389
|
+
|
|
229
390
|
def fa_sort
|
|
230
391
|
context_params[:sort]
|
|
231
392
|
end
|
|
@@ -235,6 +396,40 @@ module FlexiAdmin::Controllers::ResourcesController
|
|
|
235
396
|
end
|
|
236
397
|
|
|
237
398
|
def disabled?(form_disabled = false)
|
|
238
|
-
|
|
399
|
+
if defined?(CanCan)
|
|
400
|
+
!current_ability&.can?(:update, resource_class) || form_disabled
|
|
401
|
+
else
|
|
402
|
+
form_disabled
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def namespaced_class(*segments)
|
|
407
|
+
config_namespace = FlexiAdmin::Config.configuration.namespace&.camelize
|
|
408
|
+
|
|
409
|
+
modules = segments.compact.flat_map do |segment|
|
|
410
|
+
if segment == 'namespace'
|
|
411
|
+
config_namespace
|
|
412
|
+
elsif segment.include?('::')
|
|
413
|
+
segment.split('::')
|
|
414
|
+
else
|
|
415
|
+
segment
|
|
416
|
+
end
|
|
417
|
+
end.compact
|
|
418
|
+
|
|
419
|
+
modules.join("::").constantize
|
|
420
|
+
rescue NameError => e
|
|
421
|
+
Rails.logger.warn "Failed to find class: #{modules.join("::")}"
|
|
422
|
+
raise NameError, "Failed to find class: #{modules.join("::")}"
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def apply_named_custom_scope(scoped_resource_class, scope_name)
|
|
426
|
+
scope_name = scope_name.to_sym
|
|
427
|
+
|
|
428
|
+
unless scoped_resource_class.respond_to?(:fa_allowed_scopes) && scoped_resource_class.fa_allowed_scopes.include?(scope_name)
|
|
429
|
+
raise ArgumentError, "Scope '#{scope_name}' is not whitelisted for #{scoped_resource_class}. " \
|
|
430
|
+
"Add it to fa_allowed_scopes in the model."
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
scoped_resource_class.public_send(scope_name)
|
|
239
434
|
end
|
|
240
435
|
end
|
data/lib/flexi_admin/engine.rb
CHANGED
|
@@ -4,6 +4,10 @@ module FlexiAdmin
|
|
|
4
4
|
class Engine < ::Rails::Engine
|
|
5
5
|
isolate_namespace FlexiAdmin
|
|
6
6
|
|
|
7
|
+
initializer "flexi_admin.locale" do
|
|
8
|
+
config.i18n.load_path += Dir[root.join('config', 'locales', '*.yml')]
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
initializer "flexi_admin.assets" do |app|
|
|
8
12
|
app.config.assets.precompile += %w[flexi_admin_main.scss flexi_admin.js]
|
|
9
13
|
app.config.assets.paths << root.join("app/flexi_admin/javascript")
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = {
|
|
5
|
+
container: String,
|
|
6
|
+
templateSelector: { type: String, default: '.dynamic-rows > .form-row' }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
// Initialize remove button visibility on page load
|
|
11
|
+
const containerSelector = this.containerValue || this.element.dataset.addRowContainerValue
|
|
12
|
+
if (containerSelector) {
|
|
13
|
+
const container = document.querySelector(containerSelector)
|
|
14
|
+
if (container) {
|
|
15
|
+
this.updateRemoveButtonsVisibility(container)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
add(event) {
|
|
21
|
+
event.preventDefault()
|
|
22
|
+
|
|
23
|
+
const button = event.target.closest('button')
|
|
24
|
+
const containerSelector = button?.dataset.addRowContainerValue || this.containerValue
|
|
25
|
+
const templateSelector = button?.dataset.addRowTemplateSelectorValue || this.templateSelectorValue
|
|
26
|
+
|
|
27
|
+
// Store the container selector on the controller element for future use
|
|
28
|
+
if (containerSelector && !this.containerValue) {
|
|
29
|
+
this.element.dataset.addRowContainerValue = containerSelector
|
|
30
|
+
this.containerValue = containerSelector
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const container = document.querySelector(containerSelector)
|
|
34
|
+
if (!container) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Get only direct children matching the template selector
|
|
39
|
+
const templates = Array.from(container.children).filter(child =>
|
|
40
|
+
child.matches(templateSelector)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if (templates.length === 0) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lastTemplate = templates[templates.length - 1]
|
|
48
|
+
const clone = lastTemplate.cloneNode(true)
|
|
49
|
+
|
|
50
|
+
// Mark the cloned row as removable
|
|
51
|
+
clone.dataset.removable = 'true'
|
|
52
|
+
|
|
53
|
+
const inputs = clone.querySelectorAll('input, select, textarea')
|
|
54
|
+
inputs.forEach(input => {
|
|
55
|
+
if (input.name) {
|
|
56
|
+
input.name = input.name.replace(/\[(\d+)\]/g, (match, num) => {
|
|
57
|
+
return `[${parseInt(num) + 1}]`
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
// Clear values for new row
|
|
61
|
+
if (input.tagName === 'SELECT') {
|
|
62
|
+
input.selectedIndex = 0
|
|
63
|
+
} else {
|
|
64
|
+
input.value = ''
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
input.classList.remove('is-invalid')
|
|
68
|
+
const feedback = input.closest('.field-wrapper')?.querySelector('.invalid-feedback')
|
|
69
|
+
if (feedback) feedback.remove()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const labels = clone.querySelectorAll('label')
|
|
73
|
+
labels.forEach(label => {
|
|
74
|
+
if (label.textContent) {
|
|
75
|
+
label.textContent = label.textContent.replace(/(\d+)/g, (match, num) => {
|
|
76
|
+
return parseInt(num) + 1
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
container.appendChild(clone)
|
|
82
|
+
this.updateRemoveButtonsVisibility(container)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
remove(event) {
|
|
86
|
+
event.preventDefault()
|
|
87
|
+
|
|
88
|
+
const button = event.target.closest('button')
|
|
89
|
+
const templateSelector = button?.dataset.addRowTemplateSelectorValue || this.templateSelectorValue || '.form-row'
|
|
90
|
+
|
|
91
|
+
const row = event.target.closest(templateSelector)
|
|
92
|
+
if (row) {
|
|
93
|
+
const container = row.parentElement
|
|
94
|
+
const rows = container.querySelectorAll(templateSelector)
|
|
95
|
+
|
|
96
|
+
// Prevent removing the last row
|
|
97
|
+
if (rows.length <= 1) {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
row.remove()
|
|
102
|
+
this.updateRemoveButtonsVisibility(container)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
updateRemoveButtonsVisibility(container) {
|
|
107
|
+
const templateSelector = this.templateSelectorValue || '.form-row'
|
|
108
|
+
const rows = Array.from(container.children).filter(child =>
|
|
109
|
+
child.matches(templateSelector)
|
|
110
|
+
)
|
|
111
|
+
const removeButtons = container.querySelectorAll('button[data-action*="add-row#remove"]')
|
|
112
|
+
|
|
113
|
+
const shouldShow = rows.length > 1
|
|
114
|
+
removeButtons.forEach(button => {
|
|
115
|
+
button.style.display = shouldShow ? '' : 'none'
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -30,6 +30,17 @@ export default class extends Controller {
|
|
|
30
30
|
this.hideResults();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Opening an empty field used to show nothing at all, so a user faced with a
|
|
34
|
+
// long list had to guess a search term before seeing a single option. Focus
|
|
35
|
+
// now asks the backend for its default suggestions.
|
|
36
|
+
focus() {
|
|
37
|
+
if (this.inputTarget.disabled) return;
|
|
38
|
+
if (this.inputTarget.value.length > 0) return;
|
|
39
|
+
|
|
40
|
+
this.listTarget.classList.remove("d-none");
|
|
41
|
+
this._search("");
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
keyup(event) {
|
|
34
45
|
if (this.inputTarget.value.length > 0) {
|
|
35
46
|
this.listTarget.classList.remove("d-none");
|
|
@@ -45,7 +56,7 @@ export default class extends Controller {
|
|
|
45
56
|
|
|
46
57
|
this.timeout = setTimeout(() => {
|
|
47
58
|
this._search(this.inputTarget.value);
|
|
48
|
-
},
|
|
59
|
+
}, 300);
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
hideResults() {
|
|
@@ -118,6 +129,11 @@ export default class extends Controller {
|
|
|
118
129
|
const url = new URL(path, window.location.origin);
|
|
119
130
|
url.searchParams.set("q", string);
|
|
120
131
|
|
|
132
|
+
// An app may mark one record as the one the user is working on, so the
|
|
133
|
+
// backend can float it to the top of an otherwise arbitrary list.
|
|
134
|
+
const priority = this.inputTarget.dataset.autocompletePriorityValue;
|
|
135
|
+
if (priority) url.searchParams.set("ac_priority", priority);
|
|
136
|
+
|
|
121
137
|
try {
|
|
122
138
|
this.loadingIconTarget.classList.remove("d-none"); // Show loading icon
|
|
123
139
|
const response = await fetch(url.toString(), {
|