iron-cms 0.5.2 → 0.7.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/iron.css +682 -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/javascript/iron/controllers/local_preference_controller.js +62 -0
  12. data/app/jobs/iron/export_job.rb +9 -0
  13. data/app/jobs/iron/import_job.rb +9 -0
  14. data/app/models/concerns/iron/broadcastable.rb +9 -0
  15. data/app/models/concerns/iron/processable.rb +34 -0
  16. data/app/models/iron/account/export.rb +86 -0
  17. data/app/models/iron/account/import.rb +208 -0
  18. data/app/models/iron/block_definition/exportable.rb +14 -0
  19. data/app/models/iron/block_definition/importable.rb +27 -0
  20. data/app/models/iron/block_definition.rb +1 -1
  21. data/app/models/iron/content_type/exportable.rb +20 -0
  22. data/app/models/iron/content_type/importable.rb +32 -0
  23. data/app/models/iron/content_type.rb +1 -1
  24. data/app/models/iron/current.rb +6 -3
  25. data/app/models/iron/entry/exportable.rb +49 -0
  26. data/app/models/iron/entry/importable.rb +181 -0
  27. data/app/models/iron/entry.rb +1 -1
  28. data/app/models/iron/field.rb +9 -1
  29. data/app/models/iron/field_definition/exportable.rb +23 -0
  30. data/app/models/iron/field_definition/importable.rb +39 -0
  31. data/app/models/iron/field_definition.rb +1 -1
  32. data/app/models/iron/fields/block.rb +34 -0
  33. data/app/models/iron/fields/block_list.rb +8 -0
  34. data/app/models/iron/fields/boolean.rb +4 -0
  35. data/app/models/iron/fields/date.rb +4 -0
  36. data/app/models/iron/fields/file.rb +16 -0
  37. data/app/models/iron/fields/number.rb +4 -0
  38. data/app/models/iron/fields/reference.rb +4 -0
  39. data/app/models/iron/fields/reference_list.rb +4 -0
  40. data/app/models/iron/fields/rich_text_area.rb +32 -0
  41. data/app/models/iron/fields/text_area.rb +4 -0
  42. data/app/models/iron/fields/text_field.rb +4 -0
  43. data/app/models/iron/user.rb +2 -0
  44. data/app/views/iron/account/exports/index.html.erb +43 -0
  45. data/app/views/iron/account/exports/new.html.erb +39 -0
  46. data/app/views/iron/account/exports/show.html.erb +40 -0
  47. data/app/views/iron/account/imports/index.html.erb +43 -0
  48. data/app/views/iron/account/imports/new.html.erb +52 -0
  49. data/app/views/iron/account/imports/show.html.erb +37 -0
  50. data/app/views/iron/content_types/index.html.erb +1 -8
  51. data/app/views/iron/entries/fields/_block.html.erb +23 -10
  52. data/app/views/iron/entries/fields/_file.html.erb +3 -3
  53. data/app/views/iron/settings/show.html.erb +4 -11
  54. data/app/views/layouts/iron/application.html.erb +14 -0
  55. data/config/routes.rb +3 -9
  56. data/db/migrate/20251209103109_create_iron_account_exports.rb +13 -0
  57. data/db/migrate/20251209103110_create_iron_account_imports.rb +13 -0
  58. data/lib/iron/version.rb +1 -1
  59. data/lib/iron.rb +1 -1
  60. metadata +41 -28
  61. data/app/controllers/iron/contents_controller.rb +0 -33
  62. data/app/controllers/iron/schemas_controller.rb +0 -32
  63. data/app/models/concerns/iron/csv_serializable.rb +0 -28
  64. data/app/models/iron/archive.rb +0 -69
  65. data/app/models/iron/block_definition/portable.rb +0 -20
  66. data/app/models/iron/content_export.rb +0 -73
  67. data/app/models/iron/content_import/entry_builder.rb +0 -80
  68. data/app/models/iron/content_import/entry_snapshot.rb +0 -23
  69. data/app/models/iron/content_import/field_reconstructor.rb +0 -276
  70. data/app/models/iron/content_import/field_snapshot.rb +0 -33
  71. data/app/models/iron/content_import/registry.rb +0 -32
  72. data/app/models/iron/content_import/session.rb +0 -89
  73. data/app/models/iron/content_import.rb +0 -15
  74. data/app/models/iron/content_type/portable.rb +0 -30
  75. data/app/models/iron/entry/portable.rb +0 -35
  76. data/app/models/iron/field/portable.rb +0 -33
  77. data/app/models/iron/field_definition/portable.rb +0 -42
  78. data/app/models/iron/schema_archive.rb +0 -71
  79. data/app/models/iron/schema_exporter.rb +0 -15
  80. data/app/models/iron/schema_importer/import_strategy.rb +0 -59
  81. data/app/models/iron/schema_importer/merge_strategy.rb +0 -52
  82. data/app/models/iron/schema_importer/replace_strategy.rb +0 -51
  83. data/app/models/iron/schema_importer/safe_strategy.rb +0 -55
  84. data/app/models/iron/schema_importer.rb +0 -108
  85. data/app/views/iron/contents/new.html.erb +0 -34
  86. data/app/views/iron/schemas/new.html.erb +0 -57
  87. data/lib/iron/test_fixtures.rb +0 -50
@@ -1,59 +0,0 @@
1
- module Iron
2
- class SchemaImporter::ImportStrategy
3
- def import_block_definition(row)
4
- raise NotImplementedError
5
- end
6
-
7
- def import_content_type(row)
8
- raise NotImplementedError
9
- end
10
-
11
- def import_field_definition(row)
12
- raise NotImplementedError
13
- end
14
-
15
- protected
16
-
17
- def find_parent(parent_type, parent_handle)
18
- case parent_type
19
- when "content_type"
20
- ContentType.find_by(handle: parent_handle)
21
- when "block"
22
- BlockDefinition.find_by(handle: parent_handle)
23
- end
24
- end
25
-
26
- def content_type_class(type)
27
- case type
28
- when "single" then ContentTypes::Single
29
- when "collection" then ContentTypes::Collection
30
- else
31
- raise "Invalid content type: #{type}. Must be 'single' or 'collection'"
32
- end
33
- end
34
-
35
- def field_definition_class(type)
36
- "Iron::FieldDefinitions::#{type.camelize}".constantize
37
- end
38
-
39
- def parse_metadata(metadata_string)
40
- return {} if metadata_string.blank?
41
-
42
- JSON.parse(metadata_string)
43
- rescue JSON::ParserError
44
- {}
45
- end
46
-
47
- def parse_supported_block_definitions(supported_handles_string)
48
- return BlockDefinition.none unless supported_handles_string.present?
49
-
50
- BlockDefinition.where(handle: supported_handles_string.split("|"))
51
- end
52
-
53
- def parse_supported_content_types(supported_handles_string)
54
- return ContentType.none unless supported_handles_string.present?
55
-
56
- ContentType.where(handle: supported_handles_string.split("|"))
57
- end
58
- end
59
- end
@@ -1,52 +0,0 @@
1
- module Iron
2
- class SchemaImporter::MergeStrategy < SchemaImporter::ImportStrategy
3
- def import_block_definition(row)
4
- definition = BlockDefinition.find_or_initialize_by(handle: row["handle"])
5
- definition.update!(
6
- name: row["name"],
7
- description: row["description"]
8
- )
9
- end
10
-
11
- def import_content_type(row)
12
- content_type = ContentType.find_or_initialize_by(handle: row["handle"])
13
-
14
- content_type = content_type.becomes!(content_type_class(row["type"]))
15
- content_type.update!(
16
- name: row["name"],
17
- description: row["description"],
18
- icon: row["icon"],
19
- web_publishing_enabled: row["web_publishing_enabled"] == "true",
20
- base_path: row["base_path"]
21
- )
22
- end
23
-
24
- def import_field_definition(row)
25
- raise "Field type is required" unless row["type"].present?
26
- return unless row["parent_type"].present? && row["parent_handle"].present? && row["handle"].present?
27
-
28
- parent = find_parent(row["parent_type"], row["parent_handle"])
29
- return unless parent
30
-
31
- field_def = parent.field_definitions.find_or_initialize_by(handle: row["handle"])
32
- field_def = field_def.becomes!(field_definition_class(row["type"]))
33
-
34
- attributes = {
35
- name: row["name"],
36
- rank: row["rank"],
37
- metadata: parse_metadata(row["metadata"])
38
- }
39
-
40
- if field_def.is_a?(FieldDefinitions::Block) || field_def.is_a?(FieldDefinitions::BlockList)
41
- attributes[:supported_block_definition_ids] = parse_supported_block_definitions(row["supported_block_definitions"]).pluck(:id)
42
- end
43
-
44
- if field_def.is_a?(FieldDefinitions::Reference) || field_def.is_a?(FieldDefinitions::ReferenceList)
45
- attributes[:supported_content_type_ids] = parse_supported_content_types(row["supported_content_types"]).pluck(:id)
46
- end
47
-
48
- field_def.assign_attributes(attributes)
49
- field_def.save!
50
- end
51
- end
52
- end
@@ -1,51 +0,0 @@
1
- module Iron
2
- class SchemaImporter::ReplaceStrategy < SchemaImporter::ImportStrategy
3
- def import_block_definition(row)
4
- BlockDefinition.create!(
5
- handle: row["handle"],
6
- name: row["name"],
7
- description: row["description"]
8
- )
9
- end
10
-
11
- def import_content_type(row)
12
- content_type_class(row["type"]).create!(
13
- handle: row["handle"],
14
- name: row["name"],
15
- description: row["description"],
16
- icon: row["icon"],
17
- web_publishing_enabled: row["web_publishing_enabled"] == "true",
18
- base_path: row["base_path"]
19
- )
20
- end
21
-
22
- def import_field_definition(row)
23
- raise "Field type is required" unless row["type"].present?
24
- return unless row["parent_type"].present? && row["parent_handle"].present? && row["handle"].present?
25
-
26
- parent = find_parent(row["parent_type"], row["parent_handle"])
27
- return unless parent
28
-
29
- parent.field_definitions.find_by(handle: row["handle"])&.destroy!
30
-
31
- klass = field_definition_class(row["type"])
32
- attributes = {
33
- schemable: parent,
34
- handle: row["handle"],
35
- name: row["name"],
36
- rank: row["rank"],
37
- metadata: parse_metadata(row["metadata"])
38
- }
39
-
40
- if klass <= FieldDefinitions::Block || klass <= FieldDefinitions::BlockList
41
- attributes[:supported_block_definition_ids] = parse_supported_block_definitions(row["supported_block_definitions"]).pluck(:id)
42
- end
43
-
44
- if klass <= FieldDefinitions::Reference || klass <= FieldDefinitions::ReferenceList
45
- attributes[:supported_content_type_ids] = parse_supported_content_types(row["supported_content_types"]).pluck(:id)
46
- end
47
-
48
- klass.create!(attributes)
49
- end
50
- end
51
- end
@@ -1,55 +0,0 @@
1
- module Iron
2
- class SchemaImporter::SafeStrategy < SchemaImporter::ImportStrategy
3
- def import_block_definition(row)
4
- return if BlockDefinition.exists?(handle: row["handle"])
5
-
6
- BlockDefinition.create!(
7
- handle: row["handle"],
8
- name: row["name"],
9
- description: row["description"]
10
- )
11
- end
12
-
13
- def import_content_type(row)
14
- return if ContentType.exists?(handle: row["handle"])
15
-
16
- content_type_class(row["type"]).create!(
17
- handle: row["handle"],
18
- name: row["name"],
19
- description: row["description"],
20
- icon: row["icon"],
21
- web_publishing_enabled: row["web_publishing_enabled"] == "true",
22
- base_path: row["base_path"]
23
- )
24
- end
25
-
26
- def import_field_definition(row)
27
- raise "Field type is required" unless row["type"].present?
28
- return unless row["parent_type"].present? && row["parent_handle"].present? && row["handle"].present?
29
-
30
- parent = find_parent(row["parent_type"], row["parent_handle"])
31
- return unless parent
32
-
33
- return if parent.field_definitions.exists?(handle: row["handle"])
34
-
35
- klass = field_definition_class(row["type"])
36
- attributes = {
37
- schemable: parent,
38
- handle: row["handle"],
39
- name: row["name"],
40
- rank: row["rank"],
41
- metadata: parse_metadata(row["metadata"])
42
- }
43
-
44
- if klass <= FieldDefinitions::Block || klass <= FieldDefinitions::BlockList
45
- attributes[:supported_block_definition_ids] = parse_supported_block_definitions(row["supported_block_definitions"]).pluck(:id)
46
- end
47
-
48
- if klass <= FieldDefinitions::Reference || klass <= FieldDefinitions::ReferenceList
49
- attributes[:supported_content_type_ids] = parse_supported_content_types(row["supported_content_types"]).pluck(:id)
50
- end
51
-
52
- klass.create!(attributes)
53
- end
54
- end
55
- end
@@ -1,108 +0,0 @@
1
- require "csv"
2
-
3
- module Iron
4
- class SchemaImporter
5
- IMPORT_MODES = %w[merge replace safe].freeze
6
-
7
- attr_reader :file, :mode, :errors
8
-
9
- def self.import(file, mode: "merge")
10
- new(file, mode:).import
11
- end
12
-
13
- def initialize(file, mode: "merge")
14
- raise ArgumentError, "Missing import file" unless file.present?
15
- raise ArgumentError, "Invalid import mode: #{mode}. Must be one of: #{IMPORT_MODES.join(', ')}" unless IMPORT_MODES.include?(mode)
16
-
17
- @file = file
18
- @mode = mode.to_s
19
- @errors = []
20
- @strategy = build_strategy(mode)
21
- end
22
-
23
- def import
24
- ActiveRecord::Base.transaction do
25
- Account.clear_schema! if mode == "replace"
26
-
27
- archive = SchemaArchive.from_file(file)
28
-
29
- unless archive.valid?
30
- errors.concat(archive.errors)
31
- return ImportResult.new(false, errors)
32
- end
33
-
34
- if archive["block_definitions.csv"]
35
- CSV.parse(archive["block_definitions.csv"], headers: true) do |row|
36
- next unless row["handle"].present?
37
- @strategy.import_block_definition(row)
38
- end
39
- end
40
-
41
- if archive["content_types.csv"]
42
- CSV.parse(archive["content_types.csv"], headers: true) do |row|
43
- next unless row["handle"].present?
44
- @strategy.import_content_type(row)
45
- end
46
- end
47
-
48
- if archive["field_definitions.csv"]
49
- CSV.parse(archive["field_definitions.csv"], headers: true) do |row|
50
- @strategy.import_field_definition(row)
51
- end
52
- end
53
-
54
- update_content_type_references(archive["content_types.csv"])
55
-
56
- ImportResult.new(true, [])
57
- end
58
- rescue StandardError => e
59
- errors << "Import failed: #{e.message}"
60
- ImportResult.new(false, errors)
61
- end
62
-
63
- private
64
-
65
- def build_strategy(mode)
66
- case mode.to_s
67
- when "merge" then SchemaImporter::MergeStrategy.new
68
- when "replace" then SchemaImporter::ReplaceStrategy.new
69
- when "safe" then SchemaImporter::SafeStrategy.new
70
- else
71
- raise ArgumentError, "Invalid import mode: #{mode}"
72
- end
73
- end
74
-
75
- def update_content_type_references(content_types_csv)
76
- return unless content_types_csv
77
-
78
- CSV.parse(content_types_csv, headers: true) do |row|
79
- handle = row["handle"]
80
- content_type = ContentType.find_by(handle: handle)
81
- next unless content_type
82
-
83
- if row["title_field_handle"].present?
84
- title_field = content_type.field_definitions.find_by(handle: row["title_field_handle"])
85
- content_type.update!(title_field_definition: title_field) if title_field
86
- end
87
-
88
- if row["web_page_title_field_handle"].present?
89
- page_title_field = content_type.field_definitions.find_by(handle: row["web_page_title_field_handle"])
90
- content_type.update!(web_page_title_field_definition: page_title_field) if page_title_field
91
- end
92
- end
93
- end
94
-
95
- class ImportResult
96
- attr_reader :errors
97
-
98
- def initialize(success, errors)
99
- @success = success
100
- @errors = errors
101
- end
102
-
103
- def success?
104
- @success
105
- end
106
- end
107
- end
108
- end
@@ -1,34 +0,0 @@
1
- <% content_for :title, "Import Content" %>
2
-
3
- <div class="">
4
- <%= back_button_to "Settings", settings_path %>
5
- <h1 class="page-title">Import Content</h1>
6
- <p class="mt-2 text-sm text-stone-500">Upload a content archive exported from another Iron instance.</p>
7
-
8
- <%= form_with url: import_content_path, multipart: true, class: "mt-8 max-w-2xl", data: { turbo: false } do |form| %>
9
- <div class="field-group">
10
- <div class="field">
11
- <%= form.label :content_file, "Content Archive" %>
12
- <div>
13
- <%= form.file_field :content_file,
14
- accept: ".tar",
15
- required: true,
16
- class: "input" %>
17
- <p class="mt-2 text-sm text-stone-500">Select a .tar file exported using Iron's content export.</p>
18
- </div>
19
- </div>
20
-
21
- <div class="field">
22
- <label class="text-sm font-medium text-stone-700">Import behaviour</label>
23
- <p class="text-sm text-stone-500 mt-1">
24
- Entries with matching IDs and instance tokens are updated. Other records are created.
25
- </p>
26
- </div>
27
- </div>
28
-
29
- <div class="mt-6 flex items-center gap-x-3">
30
- <%= form.submit "Import Content", class: "button-primary" %>
31
- <%= link_to "Cancel", settings_path, class: "button-secondary" %>
32
- </div>
33
- <% end %>
34
- </div>
@@ -1,57 +0,0 @@
1
- <% content_for :title, "Import Schema" %>
2
-
3
- <div class="">
4
- <%= back_button_to "Content Types", content_types_path %>
5
- <h1 class="page-title">Import Schema</h1>
6
- <p class="mt-2 text-sm text-stone-500">Upload a schema tar file to import content types, fields, and blocks.</p>
7
-
8
- <%= form_with url: import_schema_path, multipart: true, class: "mt-8 max-w-2xl", data: { turbo: false } do |form| %>
9
- <div class="field-group">
10
- <div class="field">
11
- <%= form.label :schema_file, "Schema File" %>
12
- <div>
13
- <%= form.file_field :schema_file,
14
- accept: ".tar",
15
- required: true,
16
- class: "input" %>
17
- <p class="mt-2 text-sm text-stone-500">Select a .tar file exported from Iron CMS</p>
18
- </div>
19
- </div>
20
-
21
- <div class="field">
22
- <%= form.label :import_mode, "Import Mode" %>
23
- <div class="space-y-4">
24
- <% @import_modes.each do |mode| %>
25
- <div class="flex items-start">
26
- <div class="flex h-6 items-center">
27
- <%= form.radio_button :import_mode, mode,
28
- checked: mode == "merge",
29
- class: "h-4 w-4 border-stone-300 text-blue-600 focus:ring-blue-600" %>
30
- </div>
31
- <div class="ml-3">
32
- <%= form.label "import_mode_#{mode}", class: "text-sm font-medium text-stone-700" do %>
33
- <%= mode.capitalize %>
34
- <% end %>
35
- <p class="text-sm text-stone-500">
36
- <% case mode %>
37
- <% when "merge" %>
38
- Add new items and update existing ones by handle
39
- <% when "replace" %>
40
- Clear existing schema and import fresh (destructive)
41
- <% when "safe" %>
42
- Only add new items, skip existing handles
43
- <% end %>
44
- </p>
45
- </div>
46
- </div>
47
- <% end %>
48
- </div>
49
- </div>
50
- </div>
51
-
52
- <div class="mt-6 flex items-center gap-x-3">
53
- <%= form.submit "Import Schema", class: "button-primary" %>
54
- <%= link_to "Cancel", content_types_path, class: "button-secondary" %>
55
- </div>
56
- <% end %>
57
- </div>
@@ -1,50 +0,0 @@
1
- require "stringio"
2
-
3
- module Iron
4
- module TestFixtures
5
- class << self
6
- def load
7
- return if @loaded
8
-
9
- fixture_path = Rails.root.join("test/fixtures/iron")
10
- schema_file = fixture_path.join("schema.tar.gz")
11
- content_file = fixture_path.join("content.tar.gz")
12
-
13
- if schema_file.exist?
14
- load_schema(schema_file)
15
- else
16
- Rails.logger.warn "Iron test fixture not found: #{schema_file}"
17
- end
18
-
19
- if content_file.exist?
20
- load_content(content_file)
21
- else
22
- Rails.logger.warn "Iron test fixture not found: #{content_file}"
23
- end
24
-
25
- @loaded = true
26
- end
27
-
28
- private
29
-
30
- def load_schema(file_path)
31
- File.open(file_path, "rb") do |file|
32
- result = SchemaImporter.import(file, mode: "merge")
33
- unless result.success?
34
- raise "Failed to load Iron schema fixtures: #{result.errors.join(', ')}"
35
- end
36
- end
37
- end
38
-
39
- def load_content(file_path)
40
- File.open(file_path, "rb") do |file|
41
- archive = Archive.from_file(file)
42
- result = ContentImport.import(archive)
43
- unless result.success?
44
- raise "Failed to load Iron content fixtures: #{result.errors.join(', ')}"
45
- end
46
- end
47
- end
48
- end
49
- end
50
- end