iron-cms 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 (240) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +174 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/builds/iron.css +2 -0
  6. data/app/assets/config/iron_manifest.js +1 -0
  7. data/app/assets/images/iron/icons/sprite.svg +9425 -0
  8. data/app/assets/tailwind/iron/actiontext.css +550 -0
  9. data/app/assets/tailwind/iron/application.css +66 -0
  10. data/app/assets/tailwind/iron/components/button.css +74 -0
  11. data/app/assets/tailwind/iron/components/dropdown.css +42 -0
  12. data/app/assets/tailwind/iron/components/fieldset.css +18 -0
  13. data/app/assets/tailwind/iron/components/form.css +14 -0
  14. data/app/assets/tailwind/iron/components/icon-picker.css +9 -0
  15. data/app/assets/tailwind/iron/components/input.css +44 -0
  16. data/app/assets/tailwind/iron/components/page.css +7 -0
  17. data/app/assets/tailwind/iron/components/select.css +36 -0
  18. data/app/assets/tailwind/iron/components/sidebar.css +21 -0
  19. data/app/assets/tailwind/iron/components/textarea.css +27 -0
  20. data/app/controllers/concerns/iron/authentication.rb +66 -0
  21. data/app/controllers/concerns/iron/authorization.rb +11 -0
  22. data/app/controllers/concerns/iron/locale_aware.rb +20 -0
  23. data/app/controllers/concerns/iron/web_page.rb +18 -0
  24. data/app/controllers/iron/application_controller.rb +7 -0
  25. data/app/controllers/iron/block_definitions/field_definitions_controller.rb +8 -0
  26. data/app/controllers/iron/block_definitions_controller.rb +52 -0
  27. data/app/controllers/iron/blocks_controller.rb +12 -0
  28. data/app/controllers/iron/content_types/field_definitions_controller.rb +8 -0
  29. data/app/controllers/iron/content_types_controller.rb +52 -0
  30. data/app/controllers/iron/entries_controller.rb +103 -0
  31. data/app/controllers/iron/field_definitions_controller.rb +55 -0
  32. data/app/controllers/iron/first_runs_controller.rb +29 -0
  33. data/app/controllers/iron/home_controller.rb +6 -0
  34. data/app/controllers/iron/icons_controller.rb +10 -0
  35. data/app/controllers/iron/locales_controller.rb +52 -0
  36. data/app/controllers/iron/passwords_controller.rb +36 -0
  37. data/app/controllers/iron/references_controller.rb +11 -0
  38. data/app/controllers/iron/schemas_controller.rb +32 -0
  39. data/app/controllers/iron/sessions_controller.rb +32 -0
  40. data/app/controllers/iron/settings_controller.rb +23 -0
  41. data/app/controllers/iron/users_controller.rb +66 -0
  42. data/app/helpers/iron/application_helper.rb +25 -0
  43. data/app/helpers/iron/avatar_helper.rb +35 -0
  44. data/app/helpers/iron/block_helper.rb +4 -0
  45. data/app/helpers/iron/buttons_helper.rb +14 -0
  46. data/app/helpers/iron/components/badge_helper.rb +70 -0
  47. data/app/helpers/iron/components/dropdown_helper.rb +161 -0
  48. data/app/helpers/iron/content_types_helper.rb +11 -0
  49. data/app/helpers/iron/entries_helper.rb +13 -0
  50. data/app/helpers/iron/field_definitions_helper.rb +5 -0
  51. data/app/helpers/iron/form_builder.rb +43 -0
  52. data/app/helpers/iron/icons_helper.rb +30 -0
  53. data/app/helpers/iron/image_helper.rb +50 -0
  54. data/app/javascript/iron/application.js +6 -0
  55. data/app/javascript/iron/controllers/application.js +9 -0
  56. data/app/javascript/iron/controllers/element_controller.js +11 -0
  57. data/app/javascript/iron/controllers/file_upload_controller.js +18 -0
  58. data/app/javascript/iron/controllers/form_controller.js +7 -0
  59. data/app/javascript/iron/controllers/handle_controller.js +27 -0
  60. data/app/javascript/iron/controllers/icon_picker_controller.js +48 -0
  61. data/app/javascript/iron/controllers/index.js +4 -0
  62. data/app/javascript/iron/controllers/sortable_list_controller.js +125 -0
  63. data/app/javascript/iron/controllers/toggle_controller.js +14 -0
  64. data/app/javascript/iron/controllers/trix_controller.js +19 -0
  65. data/app/javascript/iron/lib/lexorank.js +51 -0
  66. data/app/jobs/iron/application_job.rb +4 -0
  67. data/app/jobs/iron/generate_entry_routes_job.rb +15 -0
  68. data/app/mailers/iron/application_mailer.rb +6 -0
  69. data/app/mailers/passwords_mailer.rb +6 -0
  70. data/app/models/concerns/iron/csv_serializable.rb +28 -0
  71. data/app/models/iron/account/joinable.rb +16 -0
  72. data/app/models/iron/account.rb +13 -0
  73. data/app/models/iron/application_record.rb +5 -0
  74. data/app/models/iron/block_definition/portable.rb +20 -0
  75. data/app/models/iron/block_definition.rb +10 -0
  76. data/app/models/iron/content_type/field_queryable.rb +101 -0
  77. data/app/models/iron/content_type/portable.rb +30 -0
  78. data/app/models/iron/content_type/titlable.rb +13 -0
  79. data/app/models/iron/content_type/web_publishable.rb +47 -0
  80. data/app/models/iron/content_type.rb +39 -0
  81. data/app/models/iron/content_types/collection.rb +6 -0
  82. data/app/models/iron/content_types/single.rb +9 -0
  83. data/app/models/iron/current.rb +12 -0
  84. data/app/models/iron/entry/deep_validation.rb +32 -0
  85. data/app/models/iron/entry/presentable.rb +40 -0
  86. data/app/models/iron/entry/schemable.rb +16 -0
  87. data/app/models/iron/entry/titlable.rb +35 -0
  88. data/app/models/iron/entry/web_publishable.rb +85 -0
  89. data/app/models/iron/entry.rb +16 -0
  90. data/app/models/iron/field/belongs_to_entry.rb +29 -0
  91. data/app/models/iron/field.rb +20 -0
  92. data/app/models/iron/field_definition/portable.rb +36 -0
  93. data/app/models/iron/field_definition.rb +42 -0
  94. data/app/models/iron/field_definitions/block.rb +23 -0
  95. data/app/models/iron/field_definitions/block_list.rb +8 -0
  96. data/app/models/iron/field_definitions/boolean.rb +7 -0
  97. data/app/models/iron/field_definitions/date.rb +7 -0
  98. data/app/models/iron/field_definitions/file.rb +67 -0
  99. data/app/models/iron/field_definitions/number.rb +7 -0
  100. data/app/models/iron/field_definitions/reference.rb +8 -0
  101. data/app/models/iron/field_definitions/reference_list.rb +8 -0
  102. data/app/models/iron/field_definitions/rich_text_area.rb +4 -0
  103. data/app/models/iron/field_definitions/text_area.rb +7 -0
  104. data/app/models/iron/field_definitions/text_field.rb +25 -0
  105. data/app/models/iron/fields/block.rb +40 -0
  106. data/app/models/iron/fields/block_list.rb +16 -0
  107. data/app/models/iron/fields/boolean.rb +7 -0
  108. data/app/models/iron/fields/date.rb +7 -0
  109. data/app/models/iron/fields/file.rb +29 -0
  110. data/app/models/iron/fields/number.rb +7 -0
  111. data/app/models/iron/fields/reference.rb +14 -0
  112. data/app/models/iron/fields/reference_list.rb +17 -0
  113. data/app/models/iron/fields/rich_text_area.rb +9 -0
  114. data/app/models/iron/fields/text_area.rb +7 -0
  115. data/app/models/iron/fields/text_field.rb +19 -0
  116. data/app/models/iron/first_run.rb +13 -0
  117. data/app/models/iron/icon_catalog.rb +45 -0
  118. data/app/models/iron/locale.rb +30 -0
  119. data/app/models/iron/reference.rb +8 -0
  120. data/app/models/iron/schema_archive.rb +71 -0
  121. data/app/models/iron/schema_exporter.rb +15 -0
  122. data/app/models/iron/schema_importer.rb +278 -0
  123. data/app/models/iron/session.rb +5 -0
  124. data/app/models/iron/user/role.rb +21 -0
  125. data/app/models/iron/user.rb +37 -0
  126. data/app/views/active_storage/blobs/_blob.html.erb +14 -0
  127. data/app/views/iron/block_definitions/_block_definition.html.erb +21 -0
  128. data/app/views/iron/block_definitions/_form.html.erb +46 -0
  129. data/app/views/iron/block_definitions/edit.html.erb +8 -0
  130. data/app/views/iron/block_definitions/index.html.erb +18 -0
  131. data/app/views/iron/block_definitions/new.html.erb +8 -0
  132. data/app/views/iron/block_definitions/show.html.erb +34 -0
  133. data/app/views/iron/blocks/new.turbo_stream.erb +6 -0
  134. data/app/views/iron/content_types/_content_type.html.erb +21 -0
  135. data/app/views/iron/content_types/_form.html.erb +137 -0
  136. data/app/views/iron/content_types/edit.html.erb +8 -0
  137. data/app/views/iron/content_types/index.html.erb +28 -0
  138. data/app/views/iron/content_types/new.html.erb +8 -0
  139. data/app/views/iron/content_types/show.html.erb +49 -0
  140. data/app/views/iron/entries/_entry.html.erb +17 -0
  141. data/app/views/iron/entries/_entry_option.html.erb +19 -0
  142. data/app/views/iron/entries/_form.html.erb +63 -0
  143. data/app/views/iron/entries/edit.html.erb +52 -0
  144. data/app/views/iron/entries/entry.html.erb +21 -0
  145. data/app/views/iron/entries/fields/_block.html.erb +92 -0
  146. data/app/views/iron/entries/fields/_block_list.html.erb +45 -0
  147. data/app/views/iron/entries/fields/_boolean.html.erb +13 -0
  148. data/app/views/iron/entries/fields/_date.html.erb +11 -0
  149. data/app/views/iron/entries/fields/_file.html.erb +60 -0
  150. data/app/views/iron/entries/fields/_number.html.erb +11 -0
  151. data/app/views/iron/entries/fields/_reference.html.erb +12 -0
  152. data/app/views/iron/entries/fields/_reference_item.html.erb +28 -0
  153. data/app/views/iron/entries/fields/_reference_list.html.erb +44 -0
  154. data/app/views/iron/entries/fields/_rich_text_area.html.erb +13 -0
  155. data/app/views/iron/entries/fields/_text_area.html.erb +11 -0
  156. data/app/views/iron/entries/fields/_text_field.html.erb +28 -0
  157. data/app/views/iron/entries/index.html.erb +21 -0
  158. data/app/views/iron/entries/new.html.erb +8 -0
  159. data/app/views/iron/entries/search.html.erb +33 -0
  160. data/app/views/iron/field_definitions/_field_definition.html.erb +37 -0
  161. data/app/views/iron/field_definitions/block/_form.html.erb +16 -0
  162. data/app/views/iron/field_definitions/block_list/_form.html.erb +13 -0
  163. data/app/views/iron/field_definitions/boolean/_form.html.erb +2 -0
  164. data/app/views/iron/field_definitions/date/_form.html.erb +2 -0
  165. data/app/views/iron/field_definitions/edit.html.erb +16 -0
  166. data/app/views/iron/field_definitions/file/_form.html.erb +40 -0
  167. data/app/views/iron/field_definitions/index.html.erb +14 -0
  168. data/app/views/iron/field_definitions/layouts/_form.html.erb +47 -0
  169. data/app/views/iron/field_definitions/new.html.erb +22 -0
  170. data/app/views/iron/field_definitions/number/_form.html.erb +2 -0
  171. data/app/views/iron/field_definitions/reference/_form.html.erb +13 -0
  172. data/app/views/iron/field_definitions/reference_list/_form.html.erb +13 -0
  173. data/app/views/iron/field_definitions/rich_text_area/_form.html.erb +2 -0
  174. data/app/views/iron/field_definitions/text_area/_form.html.erb +2 -0
  175. data/app/views/iron/field_definitions/text_field/_form.html.erb +17 -0
  176. data/app/views/iron/first_runs/show.html.erb +44 -0
  177. data/app/views/iron/home/show.html.erb +2 -0
  178. data/app/views/iron/icons/index.html.erb +21 -0
  179. data/app/views/iron/locales/_form.html.erb +24 -0
  180. data/app/views/iron/locales/_locale.html.erb +15 -0
  181. data/app/views/iron/locales/edit.html.erb +8 -0
  182. data/app/views/iron/locales/index.html.erb +12 -0
  183. data/app/views/iron/locales/new.html.erb +8 -0
  184. data/app/views/iron/passwords/edit.html.erb +7 -0
  185. data/app/views/iron/passwords/new.html.erb +26 -0
  186. data/app/views/iron/passwords_mailer/reset.html.erb +4 -0
  187. data/app/views/iron/passwords_mailer/reset.text.erb +2 -0
  188. data/app/views/iron/published_pages/show.html.erb +10 -0
  189. data/app/views/iron/references/new.turbo_stream.erb +6 -0
  190. data/app/views/iron/schemas/new.html.erb +57 -0
  191. data/app/views/iron/sessions/new.html.erb +36 -0
  192. data/app/views/iron/settings/show.html.erb +38 -0
  193. data/app/views/iron/shared/_icon_picker.html.erb +60 -0
  194. data/app/views/iron/users/_form.html.erb +21 -0
  195. data/app/views/iron/users/_user.html.erb +27 -0
  196. data/app/views/iron/users/edit.html.erb +12 -0
  197. data/app/views/iron/users/index.html.erb +21 -0
  198. data/app/views/iron/users/new.html.erb +29 -0
  199. data/app/views/iron/users/show.html.erb +21 -0
  200. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  201. data/app/views/layouts/iron/_sidebar.html.erb +83 -0
  202. data/app/views/layouts/iron/_sidebar_item.html.erb +14 -0
  203. data/app/views/layouts/iron/_toast.html.erb +29 -0
  204. data/app/views/layouts/iron/application.html.erb +46 -0
  205. data/app/views/layouts/iron/authentication.html.erb +41 -0
  206. data/config/importmap.rb +9 -0
  207. data/config/routes.rb +40 -0
  208. data/db/migrate/20250422131656_initial_iron_schema.rb +105 -0
  209. data/db/migrate/20250427100754_create_iron_accounts.rb +10 -0
  210. data/db/migrate/20250427104020_add_role_to_iron_users.rb +5 -0
  211. data/db/migrate/20250428094923_add_default_iron_locale_to_iron_account.rb +5 -0
  212. data/db/migrate/20250504144731_create_join_table_content_types_field_definitions.rb +25 -0
  213. data/db/migrate/20250505090716_add_title_field_definition_to_iron_content_types.rb +5 -0
  214. data/db/migrate/20250512210352_create_iron_references.rb +11 -0
  215. data/db/migrate/20250524220355_add_referenced_entry_id_to_iron_fields.rb +5 -0
  216. data/db/migrate/20250529211703_add_metadata_to_iron_field_definitions.rb +5 -0
  217. data/db/migrate/20250529211704_add_value_boolean_to_iron_fields.rb +5 -0
  218. data/db/migrate/20250531172837_add_type_to_iron_content_types.rb +5 -0
  219. data/db/migrate/20250601080146_add_icon_to_iron_content_types.rb +5 -0
  220. data/db/migrate/20250609091605_add_web_publishing_to_iron_content_types.rb +7 -0
  221. data/db/migrate/20250609091813_add_route_to_iron_entries.rb +6 -0
  222. data/lib/generators/iron/pages/pages_generator.rb +34 -0
  223. data/lib/generators/iron/pages/templates/pages_controller.rb +14 -0
  224. data/lib/generators/iron/pages/templates/show.html.erb +9 -0
  225. data/lib/generators/iron/template/template_generator.rb +35 -0
  226. data/lib/generators/iron/template/templates/content_type.html.erb +5 -0
  227. data/lib/iron/cva.rb +69 -0
  228. data/lib/iron/engine.rb +42 -0
  229. data/lib/iron/image_analyzer.rb +26 -0
  230. data/lib/iron/lexorank/exceptions.rb +4 -0
  231. data/lib/iron/lexorank/rankable.rb +185 -0
  232. data/lib/iron/lexorank/rebalance_rank_job.rb +10 -0
  233. data/lib/iron/lexorank/utils.rb +77 -0
  234. data/lib/iron/lexorank.rb +7 -0
  235. data/lib/iron/routing.rb +66 -0
  236. data/lib/iron/sdk.rb +19 -0
  237. data/lib/iron/version.rb +3 -0
  238. data/lib/iron.rb +8 -0
  239. data/lib/puma/plugin/iron_tailwindcss.rb +78 -0
  240. metadata +458 -0
@@ -0,0 +1,278 @@
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
+ @file = file
15
+ @mode = mode.to_s
16
+ @errors = []
17
+
18
+ validate_mode!
19
+ end
20
+
21
+ def import
22
+ return ImportResult.new(false, errors) unless valid?
23
+
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
+ import_block_definitions(archive["block_definitions.csv"])
35
+ import_content_types(archive["content_types.csv"])
36
+ import_field_definitions(archive["field_definitions.csv"])
37
+
38
+ update_content_type_references(archive["content_types.csv"])
39
+
40
+ ImportResult.new(true, [])
41
+ end
42
+ rescue StandardError => e
43
+ errors << "Import failed: #{e.message}"
44
+ ImportResult.new(false, errors)
45
+ end
46
+
47
+ private
48
+
49
+ def valid?
50
+ validate_file_presence
51
+ errors.empty?
52
+ end
53
+
54
+ def validate_mode!
55
+ unless IMPORT_MODES.include?(mode)
56
+ raise ArgumentError, "Invalid import mode: #{mode}. Must be one of: #{IMPORT_MODES.join(', ')}"
57
+ end
58
+ end
59
+
60
+ def validate_file_presence
61
+ errors << "No file provided" unless file
62
+ end
63
+
64
+ def import_block_definitions(csv_content)
65
+ return unless csv_content
66
+
67
+ CSV.parse(csv_content, headers: true) do |row|
68
+ handle = row["handle"]
69
+ next unless handle.present?
70
+
71
+ case mode
72
+ when "merge"
73
+ block_def = BlockDefinition.find_or_initialize_by(handle: handle)
74
+ block_def.assign_attributes(
75
+ name: row["name"],
76
+ description: row["description"]
77
+ )
78
+ block_def.save!
79
+ when "replace"
80
+ BlockDefinition.create!(
81
+ handle: handle,
82
+ name: row["name"],
83
+ description: row["description"]
84
+ )
85
+ when "safe"
86
+ next if BlockDefinition.exists?(handle: handle)
87
+
88
+ BlockDefinition.create!(
89
+ handle: handle,
90
+ name: row["name"],
91
+ description: row["description"]
92
+ )
93
+ end
94
+ end
95
+ end
96
+
97
+ def import_content_types(csv_content)
98
+ return unless csv_content
99
+
100
+ CSV.parse(csv_content, headers: true) do |row|
101
+ handle = row["handle"]
102
+ next unless handle.present?
103
+
104
+ case mode
105
+ when "merge"
106
+ content_type = ContentType.find_or_initialize_by(handle: handle)
107
+ update_content_type(content_type, row)
108
+ when "replace"
109
+ content_type = build_content_type(row)
110
+ content_type.save!
111
+ when "safe"
112
+ next if ContentType.exists?(handle: handle)
113
+
114
+ content_type = build_content_type(row)
115
+ content_type.save!
116
+ end
117
+ end
118
+ end
119
+
120
+ def import_field_definitions(csv_content)
121
+ return unless csv_content
122
+
123
+ CSV.parse(csv_content, headers: true) do |row|
124
+ parent_type = row["parent_type"]
125
+ parent_handle = row["parent_handle"]
126
+ handle = row["handle"]
127
+
128
+ next unless parent_type.present? && parent_handle.present? && handle.present?
129
+
130
+ parent = find_parent(parent_type, parent_handle)
131
+ next unless parent
132
+
133
+ case mode
134
+ when "merge"
135
+ field_def = parent.field_definitions.find_or_initialize_by(handle: handle)
136
+ update_field_definition(field_def, row)
137
+ when "replace"
138
+ field_def = build_field_definition(parent, row)
139
+ field_def.save!
140
+ when "safe"
141
+ next if parent.field_definitions.exists?(handle: handle)
142
+
143
+ field_def = build_field_definition(parent, row)
144
+ field_def.save!
145
+ end
146
+ end
147
+ end
148
+
149
+ def find_parent(parent_type, parent_handle)
150
+ case parent_type
151
+ when "content_type"
152
+ ContentType.find_by(handle: parent_handle)
153
+ when "block"
154
+ BlockDefinition.find_by(handle: parent_handle)
155
+ end
156
+ end
157
+
158
+ def build_content_type(row)
159
+ klass = case row["type"]
160
+ when "single" then ContentTypes::Single
161
+ when "collection" then ContentTypes::Collection
162
+ else ContentType
163
+ end
164
+
165
+ klass.new(
166
+ handle: row["handle"],
167
+ name: row["name"],
168
+ description: row["description"],
169
+ icon: row["icon"],
170
+ web_publishing_enabled: row["web_publishing_enabled"] == "true",
171
+ base_path: row["base_path"]
172
+ )
173
+ end
174
+
175
+ def update_content_type(content_type, row)
176
+ content_type.assign_attributes(
177
+ name: row["name"],
178
+ description: row["description"],
179
+ icon: row["icon"],
180
+ web_publishing_enabled: row["web_publishing_enabled"] == "true",
181
+ base_path: row["base_path"]
182
+ )
183
+
184
+ # Update type if changed
185
+ if row["type"] && content_type.type != "Iron::ContentTypes::#{row['type'].camelize}"
186
+ content_type.type = "Iron::ContentTypes::#{row['type'].camelize}"
187
+ end
188
+
189
+ content_type.save!
190
+ end
191
+
192
+ def build_field_definition(parent, row)
193
+ klass = "Iron::FieldDefinitions::#{row['type'].camelize}".constantize
194
+
195
+ field_def = klass.new(
196
+ schemable: parent,
197
+ handle: row["handle"],
198
+ name: row["name"],
199
+ rank: row["rank"],
200
+ metadata: parse_metadata(row["metadata"])
201
+ )
202
+
203
+ # Set supported block definitions before saving
204
+ set_supported_block_definitions(field_def, row["supported_block_definitions"])
205
+
206
+ field_def
207
+ end
208
+
209
+ def update_field_definition(field_def, row)
210
+ field_def.assign_attributes(
211
+ name: row["name"],
212
+ rank: row["rank"],
213
+ metadata: parse_metadata(row["metadata"])
214
+ )
215
+
216
+ # Update type if changed
217
+ if row["type"] && field_def.type != "Iron::FieldDefinitions::#{row['type'].camelize}"
218
+ field_def.type = "Iron::FieldDefinitions::#{row['type'].camelize}"
219
+ end
220
+
221
+ # Set supported block definitions before saving
222
+ set_supported_block_definitions(field_def, row["supported_block_definitions"])
223
+
224
+ field_def.save!
225
+ end
226
+
227
+ def parse_metadata(metadata_string)
228
+ return {} if metadata_string.blank?
229
+
230
+ JSON.parse(metadata_string)
231
+ rescue JSON::ParserError
232
+ {}
233
+ end
234
+
235
+ def update_content_type_references(content_types_csv)
236
+ return unless content_types_csv
237
+
238
+ CSV.parse(content_types_csv, headers: true) do |row|
239
+ handle = row["handle"]
240
+ content_type = ContentType.find_by(handle: handle)
241
+ next unless content_type
242
+
243
+ if row["title_field_handle"].present?
244
+ title_field = content_type.field_definitions.find_by(handle: row["title_field_handle"])
245
+ content_type.update!(title_field_definition: title_field) if title_field
246
+ end
247
+
248
+ if row["web_page_title_field_handle"].present?
249
+ page_title_field = content_type.field_definitions.find_by(handle: row["web_page_title_field_handle"])
250
+ content_type.update!(web_page_title_field_definition: page_title_field) if page_title_field
251
+ end
252
+ end
253
+ end
254
+
255
+ def set_supported_block_definitions(field_def, supported_handles_string)
256
+ return unless supported_handles_string.present?
257
+ return unless field_def.respond_to?(:supported_block_definitions)
258
+
259
+ handles = supported_handles_string.split("|")
260
+ block_definitions = BlockDefinition.where(handle: handles)
261
+
262
+ field_def.supported_block_definitions = block_definitions
263
+ end
264
+
265
+ class ImportResult
266
+ attr_reader :errors
267
+
268
+ def initialize(success, errors)
269
+ @success = success
270
+ @errors = errors
271
+ end
272
+
273
+ def success?
274
+ @success
275
+ end
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,5 @@
1
+ module Iron
2
+ class Session < ApplicationRecord
3
+ belongs_to :user
4
+ end
5
+ end
@@ -0,0 +1,21 @@
1
+ module Iron::User::Role
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ enum :role, %w[member administrator].index_with(&:itself), default: :member
6
+ validate :cannot_change_own_role, if: -> { role_changed? && persisted? }
7
+ end
8
+
9
+ def can_administer?
10
+ administrator?
11
+ end
12
+
13
+ private
14
+
15
+ def cannot_change_own_role
16
+ if current?
17
+ errors.add(:role, "cannot be changed for your own account")
18
+ self.role = role_was
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ module Iron
2
+ class User < ApplicationRecord
3
+ include Role
4
+
5
+ before_destroy :ensure_not_current_user
6
+
7
+ has_secure_password
8
+ has_many :sessions, dependent: :destroy
9
+
10
+ normalizes :email_address, with: ->(e) { e.strip.downcase }
11
+
12
+ def current?
13
+ self == Current.user
14
+ end
15
+
16
+ def initials
17
+ email_local_part[0..1]
18
+ end
19
+
20
+ def name
21
+ email_local_part
22
+ end
23
+
24
+ private
25
+
26
+ def email_local_part
27
+ email_address.split("@").first
28
+ end
29
+
30
+ def ensure_not_current_user
31
+ if Current.user == self
32
+ errors.add(:base, "You cannot delete your own account while logged in")
33
+ throw :abort
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ <figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
2
+ <% if blob.representable? %>
3
+ <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
4
+ <% end %>
5
+
6
+ <figcaption class="attachment__caption">
7
+ <% if caption = blob.try(:caption) %>
8
+ <%= caption %>
9
+ <% else %>
10
+ <span class="attachment__name"><%= blob.filename %></span>
11
+ <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
12
+ <% end %>
13
+ </figcaption>
14
+ </figure>
@@ -0,0 +1,21 @@
1
+ <div id="<%= dom_id block_definition %>" class="relative">
2
+ <%= link_to block_definition_path(block_definition), class: "block px-6 py-4 hover:bg-stone-50 dark:hover:bg-stone-800/50 transition-colors" do %>
3
+ <div class="flex items-start justify-between gap-4 pr-6">
4
+ <div class="flex flex-col sm:flex-row sm:items-center sm:gap-8 min-w-0 flex-1">
5
+ <div class="flex items-center gap-3 shrink-0">
6
+ <h3 class="text-base font-semibold text-stone-900 dark:text-white truncate"><%= block_definition.name %></h3>
7
+ <%= badge block_definition.handle, color: :blue, class: "hidden sm:inline-flex" %>
8
+ </div>
9
+
10
+ <% if block_definition.description.present? %>
11
+ <p class="mt-1 sm:mt-0 text-sm text-stone-500 dark:text-stone-400 line-clamp-1 min-w-0 flex-1"><%= block_definition.description %></p>
12
+ <% end %>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="absolute right-6 top-1/2 -translate-y-1/2 flex items-center gap-4">
17
+ <%= badge block_definition.handle, color: :blue, class: "sm:hidden" %>
18
+ <%= icon "chevron-right", variant: :mini, class: "text-stone-400 dark:text-stone-500" %>
19
+ </div>
20
+ <% end %>
21
+ </div>
@@ -0,0 +1,46 @@
1
+ <%= form_with(model: block_definition, class: "max-w-96") do |form| %>
2
+ <div class="field-group">
3
+ <div
4
+ class="flex gap-4"
5
+ data-controller="handle"
6
+ data-handle-automatic-value="<%= block_definition.new_record? %>"
7
+ >
8
+ <div class="field flex-1">
9
+ <%= form.label :name %>
10
+ <div>
11
+ <%= form.text_field :name,
12
+ data: {
13
+ handle_target: "source",
14
+ action: "handle#handleize",
15
+ "1p_ignore": "",
16
+ } %>
17
+ <%= form.error_for :name %>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="field flex-1">
22
+ <%= form.label :handle %>
23
+ <div>
24
+ <%= form.text_field :handle,
25
+ data: {
26
+ handle_target: "destination",
27
+ action: "handle#edit",
28
+ } %>
29
+ <%= form.error_for :handle %>
30
+ </div>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="field">
35
+ <%= form.label :description %>
36
+ <div>
37
+ <%= form.textarea :description, rows: 4 %>
38
+ <%= form.error_for :description %>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <div class="mt-4">
44
+ <%= form.submit class: "btn" %>
45
+ </div>
46
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <% content_for :title, "Editing block definition" %>
2
+
3
+ <div class="">
4
+ <%= back_button_to "Block definitions", block_definitions_path %>
5
+ <h1 class="page-title">Editing block definition</h1>
6
+
7
+ <%= render "form", block_definition: @block_definition %>
8
+ </div>
@@ -0,0 +1,18 @@
1
+ <% content_for :title, "Block definitions" %>
2
+
3
+ <div class="space-y-8">
4
+ <div class="flex justify-between items-center">
5
+ <h1 class="page-title">Block definitions</h1>
6
+ <%= link_to "New block definition", new_block_definition_path, class: "btn" %>
7
+ </div>
8
+
9
+ <div
10
+ id="block-definitions"
11
+ class="
12
+ bg-white dark:bg-stone-800/50 rounded-lg shadow-sm divide-y divide-stone-200
13
+ dark:divide-stone-700/20 overflow-hidden
14
+ "
15
+ >
16
+ <%= render @block_definitions %>
17
+ </div>
18
+ </div>
@@ -0,0 +1,8 @@
1
+ <% content_for :title, "New block definition" %>
2
+
3
+ <div class="">
4
+ <%= back_button_to "Block definitions", block_definitions_path %>
5
+ <h1 class="page-title">New block definition</h1>
6
+
7
+ <%= render "form", block_definition: @block_definition %>
8
+ </div>
@@ -0,0 +1,34 @@
1
+ <% content_for :title, "#{@block_definition.name} block definition" %>
2
+
3
+ <div id="<%= dom_id @block_definition %>">
4
+ <%= back_button_to "Block definitions", block_definitions_path %>
5
+ <div class="flex justify-between">
6
+ <h1 class="page-title"><%= @block_definition.name %></h1>
7
+ <div>
8
+ <%= link_to "Edit", edit_block_definition_path(@block_definition), class: "btn", data: { variant: "outline" } %>
9
+ <div class="mt-4 inline-block ml-2">
10
+ <%= button_to "Destroy", @block_definition, method: :delete, class: "btn", data: { color: "destructive" } %>
11
+ </div>
12
+ </div>
13
+ </div>
14
+ <p class="dark:text-stone-400"><%= @block_definition.description %></p>
15
+
16
+ <ul
17
+ id="fields"
18
+ class="
19
+ bg-white dark:bg-stone-800/50 rounded-lg shadow-sm divide-y divide-stone-200
20
+ dark:divide-stone-700/20 overflow-hidden mt-8
21
+ "
22
+ data-controller="sortable-list"
23
+ >
24
+ <% @block_definition.field_definitions.each do |field_definition| %>
25
+ <%= render "iron/field_definitions/field_definition", field_definition: field_definition %>
26
+ <% end %>
27
+ </ul>
28
+
29
+ <div class="mt-4">
30
+ <%= link_to "+ Add new field",
31
+ new_block_definition_field_definition_path(@block_definition),
32
+ class: "btn" %>
33
+ </div>
34
+ </div>
@@ -0,0 +1,6 @@
1
+ <%= turbo_stream.append dom_id(@block_list.definition) do %>
2
+ <%= fields_for "#{@form_field_name}[#{unique_id}]", @block do |builder| %>
3
+ <%= render "iron/entries/fields/block", builder: builder, field: @block %>
4
+ <%= builder.hidden_field :id %>
5
+ <% end %>
6
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <div id="<%= dom_id content_type %>" class="relative">
2
+ <%= link_to content_type_path(content_type), class: "block px-6 py-4 hover:bg-stone-50 dark:hover:bg-stone-800/50 transition-colors" do %>
3
+ <div class="flex items-center justify-between gap-4 pr-6">
4
+ <div class="flex flex-col sm:flex-row sm:items-center sm:gap-8 min-w-0 flex-1">
5
+ <div class="flex items-center gap-3 shrink-0">
6
+ <h3 class="text-base font-semibold text-stone-900 dark:text-white truncate"><%= content_type.name %></h3>
7
+ <%= badge content_type.handle, color: :blue, class: "hidden sm:inline-flex" %>
8
+ </div>
9
+
10
+ <% if content_type.description.present? %>
11
+ <p class="mt-1 sm:mt-0 text-sm text-stone-500 dark:text-stone-400 line-clamp-1 min-w-0 flex-1"><%= content_type.description %></p>
12
+ <% end %>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="absolute right-6 top-1/2 -translate-y-1/2 flex items-center gap-4">
17
+ <%= badge content_type.handle, color: :blue, class: "sm:hidden" %>
18
+ <%= icon "chevron-right", class: "text-stone-400 dark:text-stone-500" %>
19
+ </div>
20
+ <% end %>
21
+ </div>
@@ -0,0 +1,137 @@
1
+ <%= form_with(model: content_type, class: "max-w-96") do |form| %>
2
+ <div class="field-group">
3
+ <div
4
+ class="flex gap-4"
5
+ data-controller="handle"
6
+ data-handle-automatic-value="<%= content_type.new_record? %>"
7
+ >
8
+ <div class="field flex-1">
9
+ <%= form.label :name %>
10
+ <div>
11
+ <%= form.text_field :name,
12
+ data: {
13
+ handle_target: "source",
14
+ action: "handle#handleize",
15
+ "1p_ignore": "",
16
+ } %>
17
+ <%= form.error_for :name %>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="field flex-1">
22
+ <%= form.label :handle %>
23
+ <div>
24
+ <%= form.text_field :handle,
25
+ data: {
26
+ handle_target: "destination",
27
+ action: "handle#edit",
28
+ } %>
29
+ <%= form.error_for :handle %>
30
+ </div>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="field">
35
+ <%= form.label :description %>
36
+ <div>
37
+ <%= form.textarea :description, rows: 4 %>
38
+ <%= form.error_for :description %>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="field">
43
+ <%= form.label :icon %>
44
+ <div>
45
+ <%= render "iron/shared/icon_picker", form: form, field: :icon %>
46
+ <p class="text-sm text-stone-500 mt-1">Icon to display in the sidebar</p>
47
+ <%= form.error_for :icon %>
48
+ </div>
49
+ </div>
50
+
51
+ <% if content_type.new_record? %>
52
+ <div class="field">
53
+ <%= form.label :type, "Type" %>
54
+ <div>
55
+ <%= form.select :type,
56
+ options_for_select(Iron::ContentType::TYPES.map { |t| [t, Iron::ContentType.classify_type(t)] }, content_type.type),
57
+ {},
58
+ { class: "select" } %>
59
+ <p class="text-sm text-stone-500 mt-1">Single entry types have only one entry, collections can have multiple entries</p>
60
+ <%= form.error_for :type %>
61
+ </div>
62
+ </div>
63
+ <% end %>
64
+
65
+ <% if !content_type.new_record? && content_type.field_definitions.any? %>
66
+ <div class="field">
67
+ <%= form.label :title_field_definition_id, "Title Field" %>
68
+ <div>
69
+ <%= form.collection_select :title_field_definition_id,
70
+ content_type.titlable_definitions,
71
+ :id,
72
+ :name,
73
+ { include_blank: "Select a field" },
74
+ { class: "select" } %>
75
+ <p class="text-sm text-stone-500 mt-1">Field used for entry titles in the admin interface</p>
76
+ <%= form.error_for :title_field_definition_id %>
77
+ </div>
78
+ </div>
79
+ <% end %>
80
+
81
+ </div>
82
+
83
+ <% if !content_type.new_record? && content_type.collection? %>
84
+ <fieldset class="mt-6">
85
+ <legend>Web Publishing</legend>
86
+
87
+ <div class="field">
88
+ <div class="flex items-center gap-2">
89
+ <%= form.check_box :web_publishing_enabled,
90
+ data: {
91
+ controller: "toggle",
92
+ toggle_selector_value: "#web-publishing-fields",
93
+ action: "toggle#toggle"
94
+ } %>
95
+ <%= form.label :web_publishing_enabled, "Enable web publishing", class: "!font-normal" %>
96
+ </div>
97
+ <p class="text-sm text-stone-500 mt-1">Publish entries as web pages with unique URLs</p>
98
+ </div>
99
+
100
+ <div id="web-publishing-fields" data-ui="<%= content_type.web_publishing_enabled? ? 'visible' : 'hidden' %>" class="data-[ui~=hidden]:hidden">
101
+ <div class="field mt-4">
102
+ <%= form.label :web_page_title_field_definition_id, "Web Page Title Field" %>
103
+ <div>
104
+ <% if content_type.titlable_definitions.any? %>
105
+ <%= form.collection_select :web_page_title_field_definition_id,
106
+ content_type.titlable_definitions,
107
+ :id,
108
+ :name,
109
+ { include_blank: "Select a field" },
110
+ { class: "select" } %>
111
+ <% else %>
112
+ <%= form.select :web_page_title_field_definition_id,
113
+ options_for_select([["No compatible fields", ""]]),
114
+ {},
115
+ { class: "select", disabled: true } %>
116
+ <% end %>
117
+ <p class="text-sm text-stone-500 mt-1">Field used to generate the URL handle for each entry</p>
118
+ <%= form.error_for :web_page_title_field_definition_id %>
119
+ </div>
120
+ </div>
121
+
122
+ <div class="field mt-4">
123
+ <%= form.label :base_path, "Base Path" %>
124
+ <div>
125
+ <%= form.text_field :base_path, placeholder: "e.g. news, blog, products" %>
126
+ <p class="text-sm text-stone-500 mt-1">Optional URL prefix for all entries (e.g. /news/my-article)</p>
127
+ <%= form.error_for :base_path %>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </fieldset>
132
+ <% end %>
133
+
134
+ <div class="mt-4">
135
+ <%= form.submit %>
136
+ </div>
137
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <% content_for :title, "Editing content type" %>
2
+
3
+ <div class="">
4
+ <%= back_button_to "Content Types", content_types_path %>
5
+ <h1 class="page-title">Editing content type</h1>
6
+
7
+ <%= render "form", content_type: @content_type %>
8
+ </div>