forest_admin_rails 1.26.3 → 1.27.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39f11eb40b1841e1953642ef5ca6dcb9802e44057ddc6b4c75263d300ed09e01
4
- data.tar.gz: c848ad601825470528a781f8caf5bc9edf7d3ca0b3563f589128ccbbbfb3e494
3
+ metadata.gz: 3104918c786deec910c0eb28a201002eb3181ae7e7d322891b1319870caf7283
4
+ data.tar.gz: 33923c4d8bf895af848bfa63dc24c0d425623e85736f8f0176251b2142c4d9d0
5
5
  SHA512:
6
- metadata.gz: 8b8260e3351c961081aee921074cb63eeed14afda0b9459c6c9984a995ecb9e963b05f672e6b67a4f86181d010e0eda36ea5692be1dc49e908e20f7a906a98cb
7
- data.tar.gz: 2a7eb3ae31b4361e24dc1fc280f81e3a441707f2b27ed17c5d2381523bc9a0a51e3e8b6431232a73733b7cc839b5ade0d88af25cd76eb018e8838082528d4979
6
+ metadata.gz: 0a00bc444b99799b50dc54d432a080c4949e665230fa48e1164c0f756e089880b013af9cba63feb0be0e32493b869068d7bab6a42e00ab2fdf1a283802632cd0
7
+ data.tar.gz: 0ae0572a332551eb9fe2340884cdd6ee26de9964c9dd9440a8eb9da216f9229c9d0a3a84e110204eb57b2426b3a46eedb3feb278d30d535048a42d8cefba9e50
@@ -128,6 +128,17 @@ module ForestAdmin
128
128
  # Plugin - base class for creating custom plugins
129
129
  Plugin = ForestAdminDatasourceCustomizer::Plugins::Plugin
130
130
 
131
+ # ActiveStoragePlugin - plugin to expose Active Storage attachments as File fields
132
+ # Lazy-loaded because Zeitwerk may not have autoloaded it yet when types.rb is required
133
+ def self.const_missing(name)
134
+ if name == :ActiveStoragePlugin
135
+ require 'forest_admin_rails/plugins/active_storage'
136
+ const_set(:ActiveStoragePlugin, ForestAdminRails::Plugins::ActiveStorage)
137
+ else
138
+ super
139
+ end
140
+ end
141
+
131
142
  # ============================================
132
143
  # Additional Context Classes
133
144
  # ============================================
@@ -0,0 +1,130 @@
1
+ require 'base64'
2
+ require 'cgi'
3
+ require 'rack/mime'
4
+
5
+ module ForestAdminRails
6
+ module Plugins
7
+ class ActiveStorage < ForestAdminDatasourceCustomizer::Plugins::Plugin
8
+ ACTIVE_STORAGE_COLLECTIONS = %w[
9
+ ActiveStorage__Attachment
10
+ ActiveStorage__Blob
11
+ ActiveStorage__VariantRecord
12
+ ].freeze
13
+
14
+ def run(datasource_customizer, _collection_customizer = nil, options = {})
15
+ datasource_customizer.collections.each do |name, collection|
16
+ next if options[:only] && !options[:only].include?(name)
17
+ next if options[:except]&.include?(name)
18
+
19
+ add_attachment_fields(collection, options)
20
+ end
21
+
22
+ remove_active_storage_collections(datasource_customizer) if options.fetch(:hide_internal_collections, true)
23
+ end
24
+
25
+ private
26
+
27
+ def remove_active_storage_collections(datasource_customizer)
28
+ ACTIVE_STORAGE_COLLECTIONS.each do |collection_name|
29
+ next unless datasource_customizer.collections.key?(collection_name)
30
+
31
+ datasource_customizer.remove_collection(collection_name)
32
+ end
33
+ end
34
+
35
+ def add_attachment_fields(collection, options)
36
+ model_class = find_model_class(collection)
37
+ return unless model_class
38
+ return unless defined?(::ActiveStorage) && model_class.respond_to?(:reflect_on_all_attachments)
39
+
40
+ model_class.reflect_on_all_attachments
41
+ .select { |a| a.macro == :has_one_attached }
42
+ .each do |attachment|
43
+ name = attachment.name.to_s
44
+ hide_attachment_relations(collection, name)
45
+ add_file_field(collection, model_class, name, options.fetch(:download_images_on_list, false))
46
+ end
47
+ end
48
+
49
+ def hide_attachment_relations(collection, attachment_name)
50
+ %W[#{attachment_name}_attachment #{attachment_name}_blob].each do |relation_name|
51
+ next unless collection.schema[:fields].key?(relation_name)
52
+
53
+ collection.remove_field(relation_name)
54
+ end
55
+ end
56
+
57
+ def add_file_field(collection, model_class, attachment_name, download_images_on_list)
58
+ return if collection.schema[:fields][attachment_name]
59
+
60
+ collection.add_field(attachment_name,
61
+ ForestAdminDatasourceCustomizer::Decorators::Computed::ComputedDefinition.new(
62
+ column_type: 'File',
63
+ dependencies: ['id'],
64
+ values: lambda { |records, _ctx|
65
+ compute_values(records, model_class, attachment_name, download_images_on_list)
66
+ }
67
+ ))
68
+
69
+ collection.replace_field_writing(attachment_name) do |value, context|
70
+ handle_write(value, context, model_class, attachment_name)
71
+ end
72
+ end
73
+
74
+ def compute_values(records, model_class, attachment_name, download_images_on_list)
75
+ ids = records.map { |r| r['id'] }
76
+ models = model_class
77
+ .where(id: ids)
78
+ .includes(:"#{attachment_name}_attachment", :"#{attachment_name}_blob")
79
+ .index_by(&:id)
80
+
81
+ records.map do |record|
82
+ model = models[record['id']]
83
+ next nil unless model&.public_send(attachment_name)&.attached?
84
+
85
+ blob = model.public_send(attachment_name).blob
86
+
87
+ if records.length == 1 || (download_images_on_list && blob.content_type&.start_with?('image/'))
88
+ content = blob.download
89
+ "data:#{blob.content_type};name=#{CGI.escape(blob.filename.to_s)};base64,#{Base64.strict_encode64(content)}"
90
+ else
91
+ "data:#{blob.content_type};name=#{CGI.escape(blob.filename.to_s)};base64,"
92
+ end
93
+ end
94
+ end
95
+
96
+ def handle_write(value, context, model_class, attachment_name)
97
+ record_id = context.filter&.condition_tree&.value
98
+ return {} unless record_id
99
+
100
+ record = model_class.find(record_id)
101
+
102
+ if value.nil? || value.to_s.strip.empty?
103
+ record.public_send(attachment_name).purge if record.public_send(attachment_name).attached?
104
+ else
105
+ parsed = ForestAdminAgent::Utils::Schema::ForestValueConverter.parse_data_uri(value)
106
+ if parsed
107
+ fallback_extension = Rack::Mime::MIME_TYPES.invert[parsed['mime_type']] || '.bin'
108
+ record.public_send(attachment_name).attach(
109
+ io: StringIO.new(parsed['buffer']),
110
+ filename: parsed['name'] || "#{attachment_name}#{fallback_extension}",
111
+ content_type: parsed['mime_type']
112
+ )
113
+ end
114
+ end
115
+ {}
116
+ end
117
+
118
+ def find_model_class(collection)
119
+ current = collection.respond_to?(:collection) ? collection.collection : collection
120
+ loop do
121
+ return current.model if current.respond_to?(:model) && current.model.is_a?(Class)
122
+ break unless current.respond_to?(:child_collection)
123
+
124
+ current = current.child_collection
125
+ end
126
+ nil
127
+ end
128
+ end
129
+ end
130
+ end
@@ -1,3 +1,3 @@
1
1
  module ForestAdminRails
2
- VERSION = "1.26.3"
2
+ VERSION = "1.27.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_admin_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.26.3
4
+ version: 1.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthieu
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-03-24 00:00:00.000000000 Z
12
+ date: 2026-04-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: base64
@@ -185,6 +185,7 @@ files:
185
185
  - lib/forest_admin/types.rb
186
186
  - lib/forest_admin_rails.rb
187
187
  - lib/forest_admin_rails/engine.rb
188
+ - lib/forest_admin_rails/plugins/active_storage.rb
188
189
  - lib/forest_admin_rails/version.rb
189
190
  - lib/generators/forest_admin_rails/install_generator.rb
190
191
  - lib/generators/forest_admin_rails/templates/create_agent.rb