iron-cms 0.6.0 → 0.7.1

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/iron.css +668 -388
  3. data/app/assets/tailwind/iron/application.css +1 -0
  4. data/app/assets/tailwind/iron/components/button.css +0 -7
  5. data/app/assets/tailwind/iron/components/checkbox.css +21 -0
  6. data/app/assets/tailwind/iron/components/form.css +1 -1
  7. data/app/assets/tailwind/iron/lexxy.css +165 -51
  8. data/app/controllers/iron/account/exports_controller.rb +26 -0
  9. data/app/controllers/iron/account/imports_controller.rb +27 -0
  10. data/app/helpers/iron/form_builder.rb +7 -0
  11. data/app/jobs/iron/export_job.rb +9 -0
  12. data/app/jobs/iron/import_job.rb +9 -0
  13. data/app/models/concerns/iron/broadcastable.rb +9 -0
  14. data/app/models/concerns/iron/processable.rb +34 -0
  15. data/app/models/iron/account/export.rb +86 -0
  16. data/app/models/iron/account/import.rb +208 -0
  17. data/app/models/iron/block_definition/exportable.rb +14 -0
  18. data/app/models/iron/block_definition/importable.rb +27 -0
  19. data/app/models/iron/block_definition.rb +1 -1
  20. data/app/models/iron/content_type/exportable.rb +20 -0
  21. data/app/models/iron/content_type/importable.rb +32 -0
  22. data/app/models/iron/content_type.rb +1 -1
  23. data/app/models/iron/current.rb +6 -3
  24. data/app/models/iron/entry/exportable.rb +49 -0
  25. data/app/models/iron/entry/importable.rb +181 -0
  26. data/app/models/iron/entry.rb +1 -1
  27. data/app/models/iron/field.rb +9 -1
  28. data/app/models/iron/field_definition/exportable.rb +23 -0
  29. data/app/models/iron/field_definition/importable.rb +39 -0
  30. data/app/models/iron/field_definition.rb +1 -1
  31. data/app/models/iron/fields/block.rb +18 -0
  32. data/app/models/iron/fields/block_list.rb +8 -0
  33. data/app/models/iron/fields/boolean.rb +4 -0
  34. data/app/models/iron/fields/date.rb +4 -0
  35. data/app/models/iron/fields/file.rb +16 -0
  36. data/app/models/iron/fields/number.rb +4 -0
  37. data/app/models/iron/fields/reference.rb +4 -0
  38. data/app/models/iron/fields/reference_list.rb +4 -0
  39. data/app/models/iron/fields/rich_text_area.rb +32 -0
  40. data/app/models/iron/fields/text_area.rb +4 -0
  41. data/app/models/iron/fields/text_field.rb +4 -0
  42. data/app/models/iron/user.rb +2 -0
  43. data/app/views/iron/account/exports/index.html.erb +43 -0
  44. data/app/views/iron/account/exports/new.html.erb +39 -0
  45. data/app/views/iron/account/exports/show.html.erb +40 -0
  46. data/app/views/iron/account/imports/index.html.erb +43 -0
  47. data/app/views/iron/account/imports/new.html.erb +52 -0
  48. data/app/views/iron/account/imports/show.html.erb +37 -0
  49. data/app/views/iron/content_types/index.html.erb +1 -8
  50. data/app/views/iron/entries/fields/_file.html.erb +3 -3
  51. data/app/views/iron/settings/show.html.erb +4 -11
  52. data/config/routes.rb +3 -9
  53. data/db/migrate/20251209103109_create_iron_account_exports.rb +13 -0
  54. data/db/migrate/20251209103110_create_iron_account_imports.rb +13 -0
  55. data/lib/iron/version.rb +1 -1
  56. data/lib/iron.rb +1 -1
  57. metadata +40 -28
  58. data/app/controllers/iron/contents_controller.rb +0 -33
  59. data/app/controllers/iron/schemas_controller.rb +0 -32
  60. data/app/models/concerns/iron/csv_serializable.rb +0 -28
  61. data/app/models/iron/archive.rb +0 -69
  62. data/app/models/iron/block_definition/portable.rb +0 -20
  63. data/app/models/iron/content_export.rb +0 -73
  64. data/app/models/iron/content_import/entry_builder.rb +0 -80
  65. data/app/models/iron/content_import/entry_snapshot.rb +0 -23
  66. data/app/models/iron/content_import/field_reconstructor.rb +0 -276
  67. data/app/models/iron/content_import/field_snapshot.rb +0 -33
  68. data/app/models/iron/content_import/registry.rb +0 -32
  69. data/app/models/iron/content_import/session.rb +0 -89
  70. data/app/models/iron/content_import.rb +0 -15
  71. data/app/models/iron/content_type/portable.rb +0 -30
  72. data/app/models/iron/entry/portable.rb +0 -35
  73. data/app/models/iron/field/portable.rb +0 -33
  74. data/app/models/iron/field_definition/portable.rb +0 -42
  75. data/app/models/iron/schema_archive.rb +0 -71
  76. data/app/models/iron/schema_exporter.rb +0 -15
  77. data/app/models/iron/schema_importer/import_strategy.rb +0 -59
  78. data/app/models/iron/schema_importer/merge_strategy.rb +0 -52
  79. data/app/models/iron/schema_importer/replace_strategy.rb +0 -51
  80. data/app/models/iron/schema_importer/safe_strategy.rb +0 -55
  81. data/app/models/iron/schema_importer.rb +0 -108
  82. data/app/views/iron/contents/new.html.erb +0 -34
  83. data/app/views/iron/schemas/new.html.erb +0 -57
  84. data/lib/iron/test_fixtures.rb +0 -50
@@ -0,0 +1,208 @@
1
+ module Iron
2
+ class Account::Import < ApplicationRecord
3
+ include Processable, Broadcastable
4
+
5
+ belongs_to :user, class_name: "Iron::User", default: -> { Current.user }
6
+ has_one_attached :file
7
+
8
+ def process_later
9
+ ImportJob.perform_later(self)
10
+ end
11
+
12
+ def title
13
+ created_at.strftime("%b %-d, %Y at %-l:%M %p")
14
+ end
15
+
16
+ private
17
+
18
+ def perform
19
+ raise ArgumentError, "Nothing selected to import" unless include_schema? || include_content?
20
+
21
+ import_from_zip
22
+ end
23
+
24
+ def import_from_zip
25
+ Tempfile.create([ "import", ".zip" ]) do |tempfile|
26
+ tempfile.binmode
27
+ tempfile.write(file.download)
28
+ tempfile.rewind
29
+
30
+ Zip::File.open(tempfile.path) do |zip|
31
+ @zip = zip
32
+ @files_dir = extract_files_to_temp_dir(zip)
33
+
34
+ import_schema_from_zip if include_schema?
35
+ import_content_from_zip if include_content?
36
+ ensure
37
+ FileUtils.rm_rf(@files_dir) if @files_dir
38
+ end
39
+ end
40
+ end
41
+
42
+ def extract_files_to_temp_dir(zip)
43
+ dir = Dir.mktmpdir("iron-import")
44
+ zip.glob("files/**/*").each do |entry|
45
+ next if entry.directory?
46
+
47
+ path = File.join(dir, entry.name.sub("files/", ""))
48
+ FileUtils.mkdir_p(File.dirname(path))
49
+ entry.extract(path)
50
+ end
51
+ dir
52
+ end
53
+
54
+ def import_schema_from_zip
55
+ return unless @zip.find_entry("schema.json")
56
+
57
+ schema_json = @zip.read("schema.json")
58
+ schema = parse_json(schema_json)
59
+ raise ArgumentError, "Invalid schema JSON format" unless schema
60
+
61
+ ActiveRecord::Base.transaction do
62
+ import_block_definitions(schema[:block_definitions] || [])
63
+ import_content_types(schema[:content_types] || [])
64
+ resolve_content_type_references(schema[:content_types] || [])
65
+ end
66
+ end
67
+
68
+ def import_content_from_zip
69
+ ActiveRecord::Base.transaction do
70
+ id_mapping = {}
71
+ entries_data = []
72
+
73
+ # First pass: read all entry files and create entries
74
+ @zip.glob("entries/**/*.json").each do |zip_entry|
75
+ attrs = parse_json(zip_entry.get_input_stream.read)
76
+ next unless attrs
77
+
78
+ entries_data << attrs
79
+ entry = Entry.import_from_attributes(attrs, @files_dir)
80
+ id_mapping[attrs[:id]] = entry if entry
81
+ end
82
+
83
+ # Second pass: resolve references
84
+ resolve_references(entries_data, id_mapping)
85
+ end
86
+ end
87
+
88
+ def resolve_references(entries_data, id_mapping)
89
+ entries_data.each do |attrs|
90
+ entry = id_mapping[attrs[:id]]
91
+ next unless entry
92
+
93
+ (attrs[:fields] || {}).each do |locale_code, field_data|
94
+ locale = Locale.find_by(code: locale_code.to_s)
95
+ next unless locale
96
+
97
+ resolve_references_in_fields(entry, locale, field_data, id_mapping)
98
+ end
99
+ end
100
+ end
101
+
102
+ def resolve_references_in_fields(entry, locale, fields_data, id_mapping, parent: nil)
103
+ fields_data.each do |handle, value_data|
104
+ next unless value_data.is_a?(Hash)
105
+
106
+ case value_data[:type]
107
+ when "reference"
108
+ resolve_reference_field(entry, locale, handle.to_s, value_data[:value], id_mapping, parent: parent)
109
+ when "reference_list"
110
+ resolve_reference_list_field(entry, locale, handle.to_s, value_data[:value], id_mapping, parent: parent)
111
+ when "block"
112
+ resolve_references_in_block(entry, locale, handle.to_s, value_data, id_mapping, parent: parent)
113
+ when "block_list"
114
+ resolve_references_in_block_list(entry, locale, handle.to_s, value_data, id_mapping, parent: parent)
115
+ end
116
+ end
117
+ end
118
+
119
+ def resolve_references_in_block(entry, locale, handle, value_data, id_mapping, parent: nil)
120
+ block_field = find_field(entry, locale, handle, parent)
121
+ return unless block_field
122
+
123
+ resolve_references_in_fields(entry, locale, value_data[:fields] || {}, id_mapping, parent: block_field)
124
+ end
125
+
126
+ def resolve_references_in_block_list(entry, locale, handle, value_data, id_mapping, parent: nil)
127
+ block_list_field = find_field(entry, locale, handle, parent)
128
+ return unless block_list_field
129
+
130
+ # Reload blocks and get them in rank order (matching import order)
131
+ ordered_blocks = block_list_field.blocks.reload.sort_by(&:rank)
132
+
133
+ (value_data[:value] || []).each_with_index do |block_data, index|
134
+ block = ordered_blocks[index]
135
+ next unless block
136
+
137
+ resolve_references_in_fields(entry, locale, block_data[:fields] || {}, id_mapping, parent: block)
138
+ end
139
+ end
140
+
141
+ def find_field(entry, locale, handle, parent)
142
+ # Reload associations to get freshly persisted fields from the first pass
143
+ fields = parent&.fields&.reload || entry.fields.reload
144
+ definition_scope = parent.is_a?(Fields::Block) ? parent.block_definition.field_definitions : entry.content_type.field_definitions
145
+ definition = definition_scope.find_by(handle: handle)
146
+ return unless definition
147
+
148
+ fields.find { |f| f.definition_id == definition.id && f.locale_id == locale.id }
149
+ end
150
+
151
+ def resolve_reference_field(entry, locale, handle, old_entry_id, id_mapping, parent: nil)
152
+ return unless old_entry_id
153
+
154
+ referenced_entry = id_mapping[old_entry_id]
155
+ return unless referenced_entry
156
+
157
+ field = find_field(entry, locale, handle, parent)
158
+ return unless field
159
+
160
+ field.update!(referenced_entry: referenced_entry)
161
+ end
162
+
163
+ def resolve_reference_list_field(entry, locale, handle, old_entry_ids, id_mapping, parent: nil)
164
+ return unless old_entry_ids.is_a?(Array)
165
+
166
+ field = find_field(entry, locale, handle, parent)
167
+ return unless field
168
+
169
+ old_entry_ids.each_with_index do |old_id, index|
170
+ referenced_entry = id_mapping[old_id]
171
+ next unless referenced_entry
172
+
173
+ field.references.create!(entry: referenced_entry, rank: index)
174
+ end
175
+ end
176
+
177
+ def parse_json(content)
178
+ JSON.parse(content, symbolize_names: true)
179
+ rescue JSON::ParserError
180
+ nil
181
+ end
182
+
183
+ def import_block_definitions(definitions)
184
+ definitions.each { |attrs| BlockDefinition.import_from_attributes(attrs) }
185
+ end
186
+
187
+ def import_content_types(definitions)
188
+ definitions.each { |attrs| ContentType.import_from_attributes(attrs) }
189
+ end
190
+
191
+ def resolve_content_type_references(definitions)
192
+ definitions.each do |attrs|
193
+ content_type = ContentType.find_by(handle: attrs[:handle])
194
+ next unless content_type
195
+
196
+ if attrs[:title_field_handle].present?
197
+ field = content_type.field_definitions.find_by(handle: attrs[:title_field_handle])
198
+ content_type.update!(title_field_definition: field) if field
199
+ end
200
+
201
+ if attrs[:web_page_title_field_handle].present?
202
+ field = content_type.field_definitions.find_by(handle: attrs[:web_page_title_field_handle])
203
+ content_type.update!(web_page_title_field_definition: field) if field
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,14 @@
1
+ module Iron
2
+ module BlockDefinition::Exportable
3
+ extend ActiveSupport::Concern
4
+
5
+ def export_attributes
6
+ {
7
+ handle:,
8
+ name:,
9
+ description:,
10
+ field_definitions: field_definitions.ranked.map(&:export_attributes)
11
+ }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ module Iron
2
+ module BlockDefinition::Importable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def import_from_attributes(attrs)
7
+ block_def = find_or_initialize_by(handle: attrs[:handle])
8
+ block_def.update!(
9
+ name: attrs[:name],
10
+ description: attrs[:description]
11
+ )
12
+
13
+ import_field_definitions(block_def, attrs[:field_definitions] || [])
14
+
15
+ block_def
16
+ end
17
+
18
+ private
19
+
20
+ def import_field_definitions(block_def, field_attrs_list)
21
+ field_attrs_list.each do |field_attrs|
22
+ FieldDefinition.import_from_attributes(field_attrs, schemable: block_def)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,6 +1,6 @@
1
1
  module Iron
2
2
  class BlockDefinition < ApplicationRecord
3
- include Portable
3
+ include Exportable, Importable
4
4
 
5
5
  has_many :field_definitions, -> { ranked }, as: :schemable, dependent: :destroy
6
6
 
@@ -0,0 +1,20 @@
1
+ module Iron
2
+ module ContentType::Exportable
3
+ extend ActiveSupport::Concern
4
+
5
+ def export_attributes
6
+ {
7
+ handle:,
8
+ name:,
9
+ type: type.demodulize.underscore,
10
+ description:,
11
+ icon:,
12
+ web_publishing_enabled:,
13
+ base_path:,
14
+ title_field_handle: title_field_definition&.handle,
15
+ web_page_title_field_handle: web_page_title_field_definition&.handle,
16
+ field_definitions: field_definitions.ranked.map(&:export_attributes)
17
+ }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ module Iron
2
+ module ContentType::Importable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def import_from_attributes(attrs)
7
+ content_type = find_or_initialize_by(handle: attrs[:handle])
8
+ content_type = content_type.becomes!(classify_type(attrs[:type].camelize).constantize)
9
+
10
+ content_type.update!(
11
+ name: attrs[:name],
12
+ description: attrs[:description],
13
+ icon: attrs[:icon],
14
+ web_publishing_enabled: attrs[:web_publishing_enabled],
15
+ base_path: attrs[:base_path]
16
+ )
17
+
18
+ import_field_definitions(content_type, attrs[:field_definitions] || [])
19
+
20
+ content_type
21
+ end
22
+
23
+ private
24
+
25
+ def import_field_definitions(content_type, field_attrs_list)
26
+ field_attrs_list.each do |field_attrs|
27
+ FieldDefinition.import_from_attributes(field_attrs, schemable: content_type)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,6 +1,6 @@
1
1
  module Iron
2
2
  class ContentType < ApplicationRecord
3
- include Titlable, WebPublishable, Portable
3
+ include Titlable, WebPublishable, Exportable, Importable
4
4
 
5
5
  TYPES = %w[Single Collection].freeze
6
6
 
@@ -1,11 +1,14 @@
1
1
  module Iron
2
2
  class Current < ActiveSupport::CurrentAttributes
3
- attribute :session
4
- delegate :user, to: :session, allow_nil: true
5
-
3
+ attribute :session, :user
6
4
  attribute :locale
7
5
  attribute :account
8
6
 
7
+ def session=(value)
8
+ super(value)
9
+ self.user = value&.user
10
+ end
11
+
9
12
  def account
10
13
  super || (self.account = Account.first)
11
14
  end
@@ -0,0 +1,49 @@
1
+ module Iron
2
+ module Entry::Exportable
3
+ extend ActiveSupport::Concern
4
+ include ActionView::Helpers::TagHelper
5
+
6
+ def export_json
7
+ JSON.pretty_generate(export_attributes)
8
+ end
9
+
10
+ def export_attributes
11
+ {
12
+ gid: to_gid.to_s,
13
+ id: id,
14
+ content_type_handle: content_type.handle,
15
+ created_at: created_at.iso8601,
16
+ updated_at: updated_at.iso8601,
17
+ route: route,
18
+ creator_gid: creator&.to_gid&.to_s,
19
+ fields: export_fields_by_locale
20
+ }
21
+ end
22
+
23
+ def export_attachments
24
+ collect_attachments.map do |blob|
25
+ { path: export_attachment_path(blob), blob: blob }
26
+ end
27
+ end
28
+
29
+ def export_attachment_path(blob)
30
+ "#{id}/#{blob.key}_#{blob.filename}"
31
+ end
32
+
33
+ private
34
+
35
+ def export_fields_by_locale
36
+ grouped = fields.includes(:locale, :definition).group_by(&:locale)
37
+
38
+ grouped.transform_keys(&:code).transform_values do |locale_fields|
39
+ locale_fields
40
+ .reject { |f| f.parent.present? }
41
+ .to_h { |f| [ f.definition.handle, f.export_value ] }
42
+ end
43
+ end
44
+
45
+ def collect_attachments
46
+ fields.flat_map(&:export_attachments).compact
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,181 @@
1
+ module Iron
2
+ module Entry::Importable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def import_from_attributes(attrs, files_dir)
7
+ content_type = ContentType.find_by(handle: attrs[:content_type_handle])
8
+ return nil unless content_type
9
+
10
+ entry = find_existing_entry(attrs, content_type)
11
+ entry ||= new(content_type: content_type, creator: find_creator(attrs[:creator_gid]))
12
+ entry.route = attrs[:route]
13
+ import_fields(entry, attrs[:fields] || {}, files_dir)
14
+ entry.save!
15
+ entry
16
+ end
17
+
18
+ private
19
+
20
+ def find_existing_entry(attrs, content_type)
21
+ find_on_same_instance(attrs[:gid]) || locate_by_route(attrs[:route], content_type)
22
+ end
23
+
24
+ def find_on_same_instance(gid)
25
+ return unless gid.present?
26
+
27
+ ::GlobalID::Locator.locate(gid)
28
+ rescue ActiveRecord::RecordNotFound
29
+ nil
30
+ end
31
+
32
+ def locate_by_route(route, content_type)
33
+ return if route.nil?
34
+ Entry.find_by(content_type: content_type, route: route)
35
+ end
36
+
37
+ def find_creator(gid)
38
+ return unless gid.present?
39
+ ::GlobalID::Locator.locate(gid)
40
+ rescue ActiveRecord::RecordNotFound
41
+ nil
42
+ end
43
+
44
+ def import_fields(entry, fields_by_locale, files_dir)
45
+ fields_by_locale.each do |locale_code, field_data|
46
+ locale = Locale.find_by(code: locale_code.to_s)
47
+ next unless locale
48
+
49
+ field_data.each do |handle, value_data|
50
+ next if value_data.nil?
51
+
52
+ definition = entry.content_type.field_definitions.find_by(handle: handle.to_s)
53
+ next unless definition
54
+
55
+ import_field(entry, definition, locale, value_data, files_dir)
56
+ end
57
+ end
58
+ end
59
+
60
+ def import_field(entry, definition, locale, value_data, files_dir)
61
+ field = entry.find_or_build_field(definition, locale)
62
+ import_field_value(field, value_data, entry, files_dir)
63
+ end
64
+
65
+ def import_rich_text_field(field, value_data, files_dir)
66
+ html = value_data[:value]
67
+ return unless html.present?
68
+
69
+ # Replace local file paths with attached blobs
70
+ html = replace_file_paths_with_blobs(html, files_dir)
71
+ field.rich_text = html
72
+ end
73
+
74
+ def import_file_field(field, value_data, files_dir)
75
+ path = value_data[:value]
76
+ return unless path.present? && files_dir
77
+
78
+ file_path = File.join(files_dir, path)
79
+ return unless File.exist?(file_path)
80
+
81
+ field.file.attach(
82
+ io: File.open(file_path),
83
+ filename: File.basename(path).sub(/^[^_]+_/, "")
84
+ )
85
+ end
86
+
87
+ def import_block_field(entry, field, value_data, files_dir)
88
+ field.fields.destroy_all if field.persisted?
89
+ import_block_fields(entry, field, field.block_definition, value_data[:fields] || {}, files_dir)
90
+ end
91
+
92
+ def import_block_list_field(entry, field, value_data, files_dir)
93
+ return unless value_data[:value].is_a?(Array)
94
+
95
+ field.blocks.destroy_all if field.persisted?
96
+
97
+ value_data[:value].each_with_index do |block_data, index|
98
+ import_block(entry, field, block_data, index, files_dir)
99
+ end
100
+ end
101
+
102
+ def import_block(entry, parent_field, block_data, rank, files_dir)
103
+ block_definition = BlockDefinition.find_by(handle: block_data[:block_handle])
104
+ return unless block_definition
105
+
106
+ block = parent_field.blocks.build(
107
+ entry: entry,
108
+ definition: block_definition,
109
+ locale: parent_field.locale,
110
+ rank: rank
111
+ )
112
+
113
+ import_block_fields(entry, block, block_definition, block_data[:fields] || {}, files_dir)
114
+ end
115
+
116
+ def import_block_fields(entry, parent, block_definition, fields_data, files_dir)
117
+ fields_data.each do |handle, value_data|
118
+ next if value_data.nil?
119
+
120
+ definition = block_definition.field_definitions.find_by(handle: handle.to_s)
121
+ next unless definition
122
+
123
+ nested_field = parent.fields.build(
124
+ type: definition.field_type,
125
+ entry: entry,
126
+ definition: definition,
127
+ locale: parent.locale
128
+ )
129
+
130
+ import_field_value(nested_field, value_data, entry, files_dir)
131
+ end
132
+ end
133
+
134
+ def import_field_value(field, value_data, entry, files_dir)
135
+ case value_data[:type]
136
+ when "text_field"
137
+ field.value_string = value_data[:value]
138
+ when "text_area"
139
+ field.value_text = value_data[:value]
140
+ when "number"
141
+ field.value_float = value_data[:value]
142
+ when "boolean"
143
+ field.value_boolean = value_data[:value]
144
+ when "date"
145
+ field.value_datetime = value_data[:value].present? ? Time.iso8601(value_data[:value]) : nil
146
+ when "rich_text_area"
147
+ import_rich_text_field(field, value_data, files_dir)
148
+ when "file"
149
+ import_file_field(field, value_data, files_dir)
150
+ when "reference", "reference_list"
151
+ # Skip - resolved in second pass
152
+ when "block"
153
+ import_block_field(entry, field, value_data, files_dir)
154
+ when "block_list"
155
+ import_block_list_field(entry, field, value_data, files_dir)
156
+ end
157
+ end
158
+
159
+ def replace_file_paths_with_blobs(html, files_dir)
160
+ return html unless files_dir
161
+
162
+ # Simple regex replacement for src and href attributes pointing to local files
163
+ html.gsub(/(?:src|href)="(\d+\/[^"]+)"/) do |match|
164
+ path = $1
165
+ file_path = File.join(files_dir, path)
166
+
167
+ if File.exist?(file_path)
168
+ filename = File.basename(path).sub(/^[^_]+_/, "")
169
+ blob = ActiveStorage::Blob.create_and_upload!(
170
+ io: File.open(file_path),
171
+ filename: filename
172
+ )
173
+ match.sub(path, Rails.application.routes.url_helpers.rails_blob_path(blob, only_path: true))
174
+ else
175
+ match
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -1,6 +1,6 @@
1
1
  module Iron
2
2
  class Entry < ApplicationRecord
3
- include Titlable, Schemable, WebPublishable, Presentable, DeepValidation, InstanceScoped, Portable
3
+ include Titlable, Schemable, WebPublishable, Presentable, DeepValidation, InstanceScoped, Exportable, Importable
4
4
 
5
5
  belongs_to :creator, class_name: "Iron::User", default: -> { Current.user }
6
6
  has_many :fields, inverse_of: :entry, dependent: :destroy
@@ -1,6 +1,6 @@
1
1
  module Iron
2
2
  class Field < ApplicationRecord
3
- include BelongsToEntry, Portable
3
+ include BelongsToEntry
4
4
 
5
5
  TYPES = %w[text_field text_area rich_text_area number file boolean date block block_list reference_list reference].freeze
6
6
 
@@ -16,5 +16,13 @@ module Iron
16
16
  def value
17
17
  raise "Field type '#{type}' value method not supported"
18
18
  end
19
+
20
+ def export_value
21
+ raise "Field type '#{type}' export_value method not supported"
22
+ end
23
+
24
+ def export_attachments
25
+ []
26
+ end
19
27
  end
20
28
  end
@@ -0,0 +1,23 @@
1
+ module Iron
2
+ module FieldDefinition::Exportable
3
+ extend ActiveSupport::Concern
4
+
5
+ def export_attributes
6
+ {
7
+ handle:,
8
+ name:,
9
+ type: type_handle,
10
+ rank:,
11
+ metadata:
12
+ }.tap do |attrs|
13
+ if respond_to?(:supported_block_definitions)
14
+ attrs[:supported_block_definitions] = supported_block_definitions.pluck(:handle)
15
+ end
16
+
17
+ if respond_to?(:supported_content_types)
18
+ attrs[:supported_content_types] = supported_content_types.pluck(:handle)
19
+ end
20
+ end.compact_blank
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ module Iron
2
+ module FieldDefinition::Importable
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def import_from_attributes(attrs, schemable:)
7
+ raise ArgumentError, "Field type is required" unless attrs[:type].present?
8
+
9
+ field_def = schemable.field_definitions.find_or_initialize_by(handle: attrs[:handle])
10
+ field_def = field_def.becomes!(classify_type(attrs[:type]).constantize)
11
+
12
+ field_def.assign_attributes(
13
+ name: attrs[:name],
14
+ rank: attrs[:rank],
15
+ metadata: attrs[:metadata] || {}
16
+ )
17
+
18
+ assign_supported_definitions(field_def, attrs)
19
+
20
+ field_def.save!
21
+ field_def
22
+ end
23
+
24
+ private
25
+
26
+ def assign_supported_definitions(field_def, attrs)
27
+ if field_def.respond_to?(:supported_block_definition_ids=) && attrs[:supported_block_definitions].present?
28
+ block_defs = BlockDefinition.where(handle: attrs[:supported_block_definitions])
29
+ field_def.supported_block_definition_ids = block_defs.pluck(:id)
30
+ end
31
+
32
+ if field_def.respond_to?(:supported_content_type_ids=) && attrs[:supported_content_types].present?
33
+ content_types = ContentType.where(handle: attrs[:supported_content_types])
34
+ field_def.supported_content_type_ids = content_types.pluck(:id)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,6 +1,6 @@
1
1
  module Iron
2
2
  class FieldDefinition < ApplicationRecord
3
- include Portable
3
+ include Exportable, Importable
4
4
 
5
5
  TYPES = %w[text_field text_area rich_text_area number file boolean date block block_list reference_list reference].freeze
6
6