activeadmin_favorites 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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +17 -0
  3. data/CODE_OF_CONDUCT.md +31 -0
  4. data/CONTRIBUTING.md +29 -0
  5. data/GOVERNANCE.md +29 -0
  6. data/LICENSE.md +21 -0
  7. data/README.md +146 -0
  8. data/SECURITY.md +27 -0
  9. data/activeadmin_favorites.gemspec +68 -0
  10. data/app/assets/controllers/activeadmin_favorites/personalization_controller.js +199 -0
  11. data/app/models/active_admin/favorites/application_record.rb +9 -0
  12. data/app/models/active_admin/favorites/combined_favorite.rb +9 -0
  13. data/app/models/active_admin/favorites/favorite/macro_builder.rb +43 -0
  14. data/app/models/active_admin/favorites/favorite/macro_catalog.rb +118 -0
  15. data/app/models/active_admin/favorites/favorite/macro_resolver.rb +86 -0
  16. data/app/models/active_admin/favorites/favorite/macro_validator.rb +87 -0
  17. data/app/models/active_admin/favorites/favorite/query_params.rb +41 -0
  18. data/app/models/active_admin/favorites/favorite/range_detector.rb +54 -0
  19. data/app/models/active_admin/favorites/favorite.rb +339 -0
  20. data/app/models/active_admin/favorites/filters_favorite.rb +8 -0
  21. data/app/models/active_admin/favorites/lens_favorite.rb +9 -0
  22. data/app/models/active_admin/favorites/view_lens/layout.rb +86 -0
  23. data/app/models/active_admin/favorites/view_lens_default.rb +40 -0
  24. data/app/models/active_admin/favorites.rb +6 -0
  25. data/app/views/active_admin/favorites/_lens_catalog_data.html.erb +9 -0
  26. data/app/views/active_admin/favorites/_macro_fields.html.erb +53 -0
  27. data/app/views/active_admin/favorites/_personalization_trigger.html.erb +69 -0
  28. data/app/views/active_admin/resource/index.html.arb +99 -0
  29. data/app/views/active_admin/resource/show.html.arb +13 -0
  30. data/config/locales/activeadmin_favorites.en.yml +60 -0
  31. data/config/locales/activeadmin_favorites.host.yml.example +17 -0
  32. data/config/polyrun_coverage.yml +8 -0
  33. data/db/migrate/20260709100000_create_active_admin_view_lens_defaults.rb +24 -0
  34. data/db/migrate/20260709130000_create_admin_favorites.rb +33 -0
  35. data/lib/activeadmin/favorites/configuration.rb +31 -0
  36. data/lib/activeadmin/favorites/engine.rb +75 -0
  37. data/lib/activeadmin/favorites/hash_coercion.rb +34 -0
  38. data/lib/activeadmin/favorites/install.rb +121 -0
  39. data/lib/activeadmin/favorites/migration_helpers.rb +34 -0
  40. data/lib/activeadmin/favorites/migration_support.rb +19 -0
  41. data/lib/activeadmin/favorites/register_favorites.rb +272 -0
  42. data/lib/activeadmin/favorites/register_view_lens_preferences.rb +98 -0
  43. data/lib/activeadmin/favorites/user_record.rb +26 -0
  44. data/lib/activeadmin/favorites/version.rb +7 -0
  45. data/lib/activeadmin/favorites/view_lens/applicator.rb +150 -0
  46. data/lib/activeadmin/favorites/view_lens/catalog.rb +108 -0
  47. data/lib/activeadmin/favorites/view_lens/current.rb +28 -0
  48. data/lib/activeadmin/favorites/view_lens/patches.rb +95 -0
  49. data/lib/activeadmin/favorites/view_lens/resource_dsl.rb +44 -0
  50. data/lib/activeadmin/favorites/view_lens.rb +8 -0
  51. data/lib/activeadmin/favorites.rb +30 -0
  52. data/lib/activeadmin_favorites.rb +5 -0
  53. data/lib/generators/activeadmin_favorites/install/templates/favorite_policy.rb +56 -0
  54. data/lib/generators/activeadmin_favorites/install/templates/initializer.rb +16 -0
  55. data/lib/generators/activeadmin_favorites/install/templates/locale.en.yml +60 -0
  56. data/lib/generators/activeadmin_favorites/install_generator.rb +37 -0
  57. data/lib/tasks/activeadmin_favorites_tasks.rake +39 -0
  58. data/sig/activeadmin/favorites.rbs +5 -0
  59. metadata +418 -0
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module Favorites
5
+ module RegisterFavorites
6
+ module_function
7
+
8
+ def call
9
+ return if resource_registered?
10
+
11
+ ActiveAdmin.register Favorite, as: "favorites" do
12
+ menu label: proc { I18n.t("active_admin.favorites.menu") }, priority: 2
13
+
14
+ actions :index, :new, :create, :edit, :update, :destroy
15
+
16
+ config.filters = false
17
+ config.sort_order = "created_at_desc"
18
+
19
+ permit_params :name, :note, :path, :query_string, :resource_key, :published, :macros_json,
20
+ :use_advanced_macros, :kind, :type, :action, :include_layout, layout: {}
21
+
22
+ controller do
23
+ helper_method :current_favorites_user, :favorites_kind_tab_active?
24
+
25
+ def current_favorites_user
26
+ send(ActiveAdmin::Favorites.config.current_user_method)
27
+ end
28
+
29
+ def build_new_resource
30
+ kind = params[:kind].presence || "filters"
31
+ resource = Favorite.sti_class_for_kind(kind).new
32
+ assign_new_favorite_defaults(resource)
33
+ resource
34
+ end
35
+
36
+ def build_resource
37
+ get_resource_ivar || begin
38
+ kind = params.dig(:favorite, :kind).presence || params[:kind].presence || "filters"
39
+ record = Favorite.sti_class_for_kind(kind).new
40
+ record = assign_attributes(record, resource_params)
41
+ authorize_resource!(record)
42
+ set_resource_ivar(record)
43
+ end
44
+ end
45
+
46
+ def create
47
+ build_resource
48
+ assign_favorite_owner(resource)
49
+ apply_macros_from_params(resource) if macro_form_submitted?
50
+ apply_layout_from_params(resource)
51
+
52
+ if resource.errors.any? || !resource.save
53
+ render :new, status: :unprocessable_entity
54
+ else
55
+ redirect_to admin_favorites_path(kind: resource.kind), notice: create_notice_for(resource)
56
+ end
57
+ end
58
+
59
+ def update
60
+ resource.assign_attributes(
61
+ permitted_params[:favorite].except(
62
+ :macros_json, :use_advanced_macros, :include_layout, :layout, :kind,
63
+ :path, :query_string, :resource_key, :action
64
+ )
65
+ )
66
+ apply_macros_from_params(resource) if macro_form_submitted?
67
+ apply_layout_from_params(resource)
68
+
69
+ if resource.errors.any? || !resource.save
70
+ render :edit, status: :unprocessable_entity
71
+ else
72
+ redirect_to admin_favorites_path(kind: resource.kind), notice: I18n.t("active_admin.favorites.updated")
73
+ end
74
+ end
75
+
76
+ def scoped_collection
77
+ association = ActiveAdmin::Favorites.config.user_association_name
78
+ scope = super.includes(association)
79
+ kind = params[:kind].presence
80
+ return scope if kind.blank?
81
+ return scope unless Favorite::KINDS.include?(kind)
82
+
83
+ scope.merge(Favorite.sti_class_for_kind(kind).all)
84
+ end
85
+
86
+ private
87
+
88
+ def assign_new_favorite_defaults(resource)
89
+ if params[:path].present?
90
+ resource.path = params[:path]
91
+ resource.query_string = Favorite.sanitize_query_string(params[:query_string])
92
+ resource.resource_key = params[:resource_key]
93
+ end
94
+ resource.action = params[:action_name] if params[:action_name].present?
95
+ resource.action ||= "index"
96
+ resource.kind = params[:kind] if params[:kind].present?
97
+
98
+ if resource.layout.blank? && resource.resource_key.present?
99
+ default = ViewLensDefault.for_user(
100
+ current_favorites_user,
101
+ resource_key: resource.resource_key,
102
+ action: resource.action
103
+ )
104
+ resource.layout = default.layout if default
105
+ end
106
+
107
+ if resource.name.blank? && resource.resource_key.present?
108
+ resource.name = Favorite.suggested_name(
109
+ resource_key: resource.resource_key,
110
+ query_string: resource.query_string,
111
+ kind: resource.kind
112
+ )
113
+ end
114
+
115
+ if ActiveModel::Type::Boolean.new.cast(params[:include_layout])
116
+ resource.layout = layout_from_params || resource.layout
117
+ if resource.kind == "filters" && resource.query_string.present? && resource.layout.present?
118
+ resource = resource.promote_to_kind!("combined")
119
+ end
120
+ end
121
+ end
122
+
123
+ def assign_favorite_owner(resource)
124
+ resource.public_send(:"#{ActiveAdmin::Favorites.config.user_foreign_key}=", current_favorites_user.id)
125
+ end
126
+
127
+ def apply_macros_from_params(favorite)
128
+ favorite.use_advanced_macros = params.dig(:favorite, :use_advanced_macros)
129
+ favorite.macros_json = params.dig(:favorite, :macros_json)
130
+ favorite.assign_macros_from_form!(
131
+ template_assignments: params[:macro_template_assignments],
132
+ macros_json: favorite.macros_json,
133
+ use_advanced_macros: favorite.use_advanced_macros
134
+ )
135
+ end
136
+
137
+ def apply_layout_from_params(favorite)
138
+ return favorite unless ActiveModel::Type::Boolean.new.cast(params.dig(:favorite, :include_layout))
139
+
140
+ favorite.layout = layout_from_params || favorite.layout
141
+ if favorite.kind == "filters" && favorite.query_string.present? && favorite.layout.present?
142
+ favorite = favorite.promote_to_kind!("combined")
143
+ set_resource_ivar(favorite)
144
+ end
145
+ favorite
146
+ end
147
+
148
+ def layout_from_params
149
+ raw_layout = params.dig(:favorite, :layout)
150
+ return if raw_layout.blank?
151
+
152
+ parsed =
153
+ if raw_layout.is_a?(String)
154
+ JSON.parse(raw_layout)
155
+ else
156
+ raw_layout
157
+ end
158
+ ViewLens::Layout.normalize(parsed)
159
+ rescue JSON::ParserError
160
+ nil
161
+ end
162
+
163
+ def macro_form_submitted?
164
+ params[:macro_form_submitted].present?
165
+ end
166
+
167
+ def favorites_kind_tab_active?(kind)
168
+ if kind.nil?
169
+ params[:kind].blank?
170
+ else
171
+ params[:kind] == kind
172
+ end
173
+ end
174
+
175
+ def create_notice_for(resource)
176
+ case resource.kind
177
+ when "lens"
178
+ I18n.t("active_admin.favorites.lens_created")
179
+ when "combined"
180
+ I18n.t("active_admin.favorites.combined_created")
181
+ else
182
+ I18n.t("active_admin.favorites.created")
183
+ end
184
+ end
185
+ end
186
+
187
+ index title: proc { I18n.t("active_admin.favorites.title") } do
188
+ nav class: "flex flex-wrap gap-3 mb-4", "aria-label": I18n.t("active_admin.favorites.kind_navigation") do
189
+ link_to I18n.t("active_admin.favorites.kinds.all"),
190
+ admin_favorites_path,
191
+ class: favorites_kind_tab_active?(nil) ? "font-semibold underline" : "text-gray-600 dark:text-gray-300"
192
+ Favorite::KINDS.each do |kind|
193
+ link_to I18n.t("active_admin.favorites.kinds.#{kind}"),
194
+ admin_favorites_path(kind: kind),
195
+ class: favorites_kind_tab_active?(kind) ? "font-semibold underline" : "text-gray-600 dark:text-gray-300"
196
+ end
197
+ end
198
+
199
+ column :name do |favorite|
200
+ link_to favorite.name, favorite.open_url, class: "no-underline font-medium"
201
+ end
202
+ column I18n.t("active_admin.favorites.kind_column"), &:kind_label
203
+ column :resource, &:resource_label
204
+ column I18n.t("active_admin.favorites.filters_column") do |favorite|
205
+ (favorite.kind == "lens") ? "—" : favorite.filter_summary
206
+ end
207
+ column I18n.t("active_admin.favorites.visibility_column") do |favorite|
208
+ if favorite.published?
209
+ I18n.t("active_admin.favorites.visibility.published", time: l(favorite.published_at, default: "—"))
210
+ else
211
+ I18n.t("active_admin.favorites.visibility.mine")
212
+ end
213
+ end
214
+ column I18n.t("active_admin.favorites.creator_column") do |favorite|
215
+ next "—" if favorite.owned_by?(current_favorites_user)
216
+
217
+ owner = favorite.public_send(ActiveAdmin::Favorites.config.user_association_name)
218
+ label_method = ActiveAdmin::Favorites.config.user_label_method
219
+ owner&.public_send(label_method) || "—"
220
+ end
221
+ actions defaults: false do |favorite|
222
+ item I18n.t("active_admin.favorites.open"), favorite.open_url, class: "member_link"
223
+ if authorized?(:update, favorite)
224
+ item I18n.t("active_admin.edit"), edit_admin_favorite_path(favorite), class: "member_link"
225
+ item I18n.t("active_admin.delete"), admin_favorite_path(favorite),
226
+ class: "member_link",
227
+ method: :delete,
228
+ data: {confirm: I18n.t("active_admin.delete_confirmation")}
229
+ end
230
+ end
231
+ end
232
+
233
+ form do |f|
234
+ f.semantic_errors(*f.object.errors.attribute_names)
235
+ f.inputs I18n.t("active_admin.favorites.form_panel") do
236
+ f.input :name, input_html: {autofocus: true}
237
+ f.input :note, as: :text, input_html: {rows: 3}
238
+ f.input :published, as: :boolean,
239
+ label: I18n.t("active_admin.favorites.published")
240
+ if f.object.new_record?
241
+ f.input :path, as: :hidden
242
+ f.input :query_string, as: :hidden
243
+ f.input :resource_key, as: :hidden
244
+ f.input :kind, as: :hidden
245
+ f.input :action, as: :hidden
246
+ end
247
+ if f.object.new_record? && f.object.kind == "filters"
248
+ f.input :include_layout, as: :boolean,
249
+ label: I18n.t("active_admin.favorites.include_layout")
250
+ f.input :layout, as: :hidden,
251
+ input_html: {value: (f.object.layout || {}).to_json}
252
+ end
253
+ end
254
+
255
+ if f.object.kind != "lens"
256
+ render partial: "active_admin/favorites/macro_fields", locals: {f: f}
257
+ end
258
+
259
+ f.actions
260
+ end
261
+
262
+ config.remove_action_item :new
263
+ end
264
+ end
265
+
266
+ def resource_registered?
267
+ ActiveAdmin.application.namespace(ActiveAdmin::Favorites.config.namespace_name)
268
+ .resources.any? { |resource| resource.resource_class == Favorite }
269
+ end
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module Favorites
5
+ module RegisterViewLensPreferences
6
+ PAGE_NAME = "ViewLensPreferences"
7
+
8
+ module_function
9
+
10
+ def parse_layout_param(raw_layout)
11
+ parsed =
12
+ if raw_layout.is_a?(String)
13
+ JSON.parse(raw_layout)
14
+ elsif raw_layout.respond_to?(:to_unsafe_h)
15
+ raw_layout.to_unsafe_h
16
+ else
17
+ raw_layout
18
+ end
19
+ ViewLens::Layout.normalize(parsed || {})
20
+ rescue JSON::ParserError
21
+ ViewLens::Layout.empty.layout
22
+ end
23
+
24
+ def call
25
+ return if page_registered?
26
+
27
+ ActiveAdmin.register_page PAGE_NAME, as: "view_lens_preferences" do
28
+ menu false
29
+
30
+ controller do
31
+ def layout_from_request
32
+ ActiveAdmin::Favorites::RegisterViewLensPreferences.parse_layout_param(params[:layout])
33
+ end
34
+
35
+ def authorize_view_lens_page_action!(action_name)
36
+ return unless respond_to?(:authorized?, true)
37
+
38
+ return if authorized?(action_name, active_admin_config)
39
+
40
+ redirect_to admin_root_path,
41
+ alert: I18n.t("active_admin.access_denied.message", default: "You are not authorized to perform this action.")
42
+ end
43
+ end
44
+
45
+ page_action :update, method: :patch do
46
+ authorize_view_lens_page_action!(:update)
47
+ user = send(ActiveAdmin::Favorites.config.current_user_method)
48
+ layout = ActiveAdmin::Favorites::RegisterViewLensPreferences.parse_layout_param(params[:layout])
49
+ ViewLensDefault.upsert_for!(
50
+ user: user,
51
+ resource_key: params.fetch(:resource_key),
52
+ action: params.fetch(:view_lens_action),
53
+ layout: layout
54
+ )
55
+ redirect_back fallback_location: admin_root_path, notice: I18n.t("active_admin.favorites.layout_saved")
56
+ end
57
+
58
+ page_action :reset, method: :delete do
59
+ authorize_view_lens_page_action!(:reset)
60
+ user = send(ActiveAdmin::Favorites.config.current_user_method)
61
+ record = ViewLensDefault.for_user(
62
+ user,
63
+ resource_key: params.fetch(:resource_key),
64
+ action: params.fetch(:view_lens_action)
65
+ )
66
+ record&.destroy
67
+ redirect_back fallback_location: admin_root_path, notice: I18n.t("active_admin.favorites.layout_reset")
68
+ end
69
+
70
+ page_action :save_lens_favorite, method: :post do
71
+ authorize_view_lens_page_action!(:save_lens_favorite)
72
+ user = send(ActiveAdmin::Favorites.config.current_user_method)
73
+ bookmark = LensFavorite.new(
74
+ name: params.fetch(:name),
75
+ resource_key: params.fetch(:resource_key),
76
+ path: params.fetch(:path),
77
+ action: params.fetch(:view_lens_action),
78
+ layout: ActiveAdmin::Favorites::RegisterViewLensPreferences.parse_layout_param(params[:layout]),
79
+ published: ActiveModel::Type::Boolean.new.cast(params[:published]) || false
80
+ )
81
+ bookmark.public_send(:"#{ActiveAdmin::Favorites.config.user_foreign_key}=", user.id)
82
+
83
+ if bookmark.save
84
+ redirect_to admin_favorites_path(kind: "lens"), notice: I18n.t("active_admin.favorites.lens_created")
85
+ else
86
+ redirect_back fallback_location: admin_root_path, alert: bookmark.errors.full_messages.to_sentence
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def page_registered?
93
+ ActiveAdmin.application.namespace(ActiveAdmin::Favorites.config.namespace_name)
94
+ .resources.any? { |resource| resource.is_a?(ActiveAdmin::Page) && resource.name == PAGE_NAME }
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module Favorites
5
+ module UserRecord
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def define_favorites_user_association!
10
+ return if favorites_user_association_defined?
11
+
12
+ config = ActiveAdmin::Favorites.config
13
+ belongs_to config.user_association_name,
14
+ class_name: config.user_class,
15
+ foreign_key: config.user_foreign_key
16
+
17
+ self.favorites_user_association_defined = true
18
+ end
19
+ end
20
+
21
+ included do
22
+ class_attribute :favorites_user_association_defined, instance_accessor: false, default: false
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module Favorites
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module Favorites
5
+ module ViewLens
6
+ class Applicator
7
+ class << self
8
+ def current_layout
9
+ value = Current.layout
10
+ return Layout.empty if value.nil?
11
+ return value if value.is_a?(Layout)
12
+
13
+ Layout.new(value)
14
+ end
15
+
16
+ def current_layout=(value)
17
+ Current.layout = value
18
+ end
19
+
20
+ def current_catalog
21
+ Current.catalog
22
+ end
23
+
24
+ def current_catalog=(value)
25
+ Current.catalog = value
26
+ end
27
+
28
+ def registered_columns
29
+ Current.registered_columns
30
+ end
31
+
32
+ def registered_panels
33
+ Current.registered_panels
34
+ end
35
+
36
+ def registered_rows
37
+ Current.registered_rows
38
+ end
39
+
40
+ def reset!
41
+ Current.reset
42
+ end
43
+
44
+ def resolve_layout(controller:)
45
+ user = controller.send(ActiveAdmin::Favorites.config.current_user_method)
46
+ config = controller.send(:active_admin_config)
47
+ action = controller.action_name
48
+ resource_key = config.resource_name.route_key
49
+
50
+ layout = ViewLens::Layout.empty
51
+ default = ViewLensDefault.for_user(user, resource_key: resource_key, action: action)
52
+ layout = layout.merge(default.layout_object) if default
53
+
54
+ favorite_id = controller.params[:favorite_id]
55
+ if favorite_id.present?
56
+ bookmark = Favorite.visible_to(user).find_by(id: favorite_id)
57
+ if bookmark&.layout.present? && bookmark.resource_key == resource_key && bookmark.action == action
58
+ layout = layout.merge(bookmark.layout_object)
59
+ end
60
+ end
61
+
62
+ Current.layout = layout
63
+ Current.catalog_context = {
64
+ active_admin_config: config,
65
+ action: action,
66
+ render_context: controller.helpers
67
+ }
68
+ layout
69
+ end
70
+
71
+ def build_catalog(active_admin_config:, action:, render_context: nil)
72
+ Catalog.for(
73
+ active_admin_config: active_admin_config,
74
+ action: action,
75
+ render_context: render_context
76
+ )
77
+ end
78
+
79
+ def catalog_payload(active_admin_config:, action:, render_context: nil)
80
+ build_catalog(
81
+ active_admin_config: active_admin_config,
82
+ action: action,
83
+ render_context: render_context
84
+ ).map do |entry|
85
+ {id: entry.id, label: entry.label, group: entry.group}
86
+ end
87
+ end
88
+
89
+ def register_column(resource_key:, id:, label:)
90
+ return if id.blank?
91
+
92
+ registered_columns[resource_key] << {id: id.to_s, label: label.to_s}
93
+ end
94
+
95
+ def register_panel(resource_key:, id:, label:)
96
+ return if id.blank?
97
+
98
+ registered_panels[resource_key] << {id: id.to_s, label: label.to_s}
99
+ end
100
+
101
+ def register_row(resource_key:, id:, label:)
102
+ return if id.blank?
103
+
104
+ registered_rows[resource_key] << {id: id.to_s, label: label.to_s}
105
+ end
106
+
107
+ def extract_column_id(args, resource_key: nil)
108
+ title = args[0]
109
+ return title.to_s if title.is_a?(Symbol)
110
+ return title.to_s.parameterize(separator: "_") if title.is_a?(String) && title.present?
111
+
112
+ options = args.last.is_a?(Hash) ? args.last : {}
113
+ return options[:lens_id].to_s if options[:lens_id].present?
114
+
115
+ next_block_column_id(resource_key) if resource_key.present?
116
+ end
117
+
118
+ def column_label_from_args(args, column_id:)
119
+ title = args[0]
120
+ options = args.last.is_a?(Hash) ? args.last : {}
121
+ return options[:label].to_s if options[:label].present?
122
+ return title.to_s.humanize if title.is_a?(Symbol)
123
+ return title if title.is_a?(String) && title.present?
124
+
125
+ if column_id.to_s.start_with?("block_column_")
126
+ position = column_id.to_s.delete_prefix("block_column_")
127
+ return I18n.t("active_admin.favorites.block_column", position: position, default: "Custom column %{position}")
128
+ end
129
+
130
+ column_id.to_s.humanize
131
+ end
132
+
133
+ def next_block_column_id(resource_key)
134
+ sequence = Current.block_column_sequence
135
+ sequence[resource_key] += 1
136
+ "block_column_#{sequence[resource_key]}"
137
+ end
138
+
139
+ def extract_panel_id(title, options)
140
+ options[:lens_id]&.to_s.presence || title.to_s.parameterize(separator: "_")
141
+ end
142
+
143
+ def extract_row_id(title, options)
144
+ options[:lens_id]&.to_s.presence || title.to_s.parameterize(separator: "_")
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module Favorites
5
+ module ViewLens
6
+ class Catalog
7
+ Entry = Data.define(:id, :label, :group)
8
+
9
+ def self.for(active_admin_config:, action:, render_context: nil)
10
+ new(active_admin_config:, action:, render_context:).call
11
+ end
12
+
13
+ def initialize(active_admin_config:, action:, render_context: nil)
14
+ @active_admin_config = active_admin_config
15
+ @action = action.to_s
16
+ @render_context = render_context
17
+ end
18
+
19
+ def call
20
+ case @action
21
+ when "index"
22
+ index_entries
23
+ when "show"
24
+ show_entries
25
+ else
26
+ []
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def index_entries
33
+ entries = []
34
+ entries.concat(column_entries)
35
+ entries.concat(filter_entries)
36
+ entries.concat(action_item_entries(:index))
37
+ entries
38
+ end
39
+
40
+ def show_entries
41
+ entries = []
42
+ entries.concat(panel_entries)
43
+ entries.concat(row_entries)
44
+ entries.concat(action_item_entries(:show))
45
+ entries
46
+ end
47
+
48
+ def column_entries
49
+ registered = ViewLens::Applicator.registered_columns[@active_admin_config.resource_name.route_key] || []
50
+ registered.map do |column|
51
+ Entry.new(
52
+ id: column[:id],
53
+ label: column[:label],
54
+ group: "columns"
55
+ )
56
+ end
57
+ end
58
+
59
+ def filter_entries
60
+ return [] unless @active_admin_config.filters_enabled?
61
+
62
+ filters = @active_admin_config.filters
63
+ return [] if filters.blank?
64
+
65
+ filters.keys.map do |filter_key|
66
+ filter = @active_admin_config.filters[filter_key]
67
+ label = filter[:label] || filter_key.to_s.titleize
68
+ Entry.new(id: filter_key.to_s, label: label.to_s, group: "filters")
69
+ end
70
+ end
71
+
72
+ def action_item_entries(action)
73
+ action_items = @active_admin_config.action_items
74
+ return [] if action_items.blank?
75
+
76
+ action_items
77
+ .select { |item| item.display_on?(action, @render_context) }
78
+ .reject { |item| item.name.to_s.start_with?("personalize_page") }
79
+ .map do |item|
80
+ Entry.new(
81
+ id: item.name.to_s,
82
+ label: I18n.t("active_admin.favorites.action_items.#{item.name}", default: item.name.to_s.humanize),
83
+ group: "action_items"
84
+ )
85
+ end
86
+ end
87
+
88
+ def panel_entries
89
+ registered = ViewLens::Applicator.registered_panels[@active_admin_config.resource_name.route_key] || []
90
+ registered.map do |panel|
91
+ Entry.new(id: panel[:id], label: panel[:label], group: "panels")
92
+ end
93
+ end
94
+
95
+ def row_entries
96
+ registered = ViewLens::Applicator.registered_rows[@active_admin_config.resource_name.route_key] || []
97
+ registered.map do |row|
98
+ Entry.new(id: row[:id], label: row[:label], group: "show_rows")
99
+ end
100
+ end
101
+
102
+ def self.grouped(entries)
103
+ entries.group_by(&:group)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module Favorites
5
+ module ViewLens
6
+ class Current < ActiveSupport::CurrentAttributes
7
+ attribute :layout, :catalog, :catalog_context
8
+ attribute :registered_columns, :registered_panels, :registered_rows, :block_column_sequence
9
+
10
+ def registered_columns
11
+ super || self.registered_columns = Hash.new { |hash, key| hash[key] = [] }
12
+ end
13
+
14
+ def registered_panels
15
+ super || self.registered_panels = Hash.new { |hash, key| hash[key] = [] }
16
+ end
17
+
18
+ def registered_rows
19
+ super || self.registered_rows = Hash.new { |hash, key| hash[key] = [] }
20
+ end
21
+
22
+ def block_column_sequence
23
+ super || self.block_column_sequence = Hash.new(0)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end