ruby_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.
- checksums.yaml +7 -0
- data/.cursor/dhh.mdc +698 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/README.md +235 -0
- data/Rakefile +30 -0
- data/app/components/ruby_cms/admin/admin_page/admin_table_content.rb +32 -0
- data/app/components/ruby_cms/admin/admin_page.rb +345 -0
- data/app/components/ruby_cms/admin/base_component.rb +78 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +149 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +127 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +15 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +41 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +33 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +174 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +59 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +159 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +192 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +97 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +137 -0
- data/app/controllers/concerns/ruby_cms/admin_pagination.rb +120 -0
- data/app/controllers/concerns/ruby_cms/admin_turbo_table.rb +68 -0
- data/app/controllers/concerns/ruby_cms/page_tracking.rb +52 -0
- data/app/controllers/concerns/ruby_cms/visitor_error_capture.rb +39 -0
- data/app/controllers/ruby_cms/admin/analytics_controller.rb +191 -0
- data/app/controllers/ruby_cms/admin/base_controller.rb +105 -0
- data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +390 -0
- data/app/controllers/ruby_cms/admin/dashboard_controller.rb +50 -0
- data/app/controllers/ruby_cms/admin/locale_controller.rb +20 -0
- data/app/controllers/ruby_cms/admin/permissions_controller.rb +66 -0
- data/app/controllers/ruby_cms/admin/settings_controller.rb +223 -0
- data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +59 -0
- data/app/controllers/ruby_cms/admin/users_controller.rb +107 -0
- data/app/controllers/ruby_cms/admin/visitor_errors_controller.rb +89 -0
- data/app/controllers/ruby_cms/admin/visual_editor_controller.rb +322 -0
- data/app/controllers/ruby_cms/errors_controller.rb +35 -0
- data/app/helpers/ruby_cms/admin/admin_page_helper.rb +21 -0
- data/app/helpers/ruby_cms/admin/bulk_action_table_helper.rb +159 -0
- data/app/helpers/ruby_cms/application_helper.rb +41 -0
- data/app/helpers/ruby_cms/bulk_action_table_helper.rb +151 -0
- data/app/helpers/ruby_cms/content_blocks_helper.rb +375 -0
- data/app/helpers/ruby_cms/settings_helper.rb +160 -0
- data/app/javascript/controllers/ruby_cms/auto_save_preference_controller.js +73 -0
- data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +553 -0
- data/app/javascript/controllers/ruby_cms/clickable_row_controller.js +28 -0
- data/app/javascript/controllers/ruby_cms/flash_messages_controller.js +29 -0
- data/app/javascript/controllers/ruby_cms/index.js +104 -0
- data/app/javascript/controllers/ruby_cms/locale_tabs_controller.js +34 -0
- data/app/javascript/controllers/ruby_cms/mobile_menu_controller.js +55 -0
- data/app/javascript/controllers/ruby_cms/nav_order_sortable_controller.js +192 -0
- data/app/javascript/controllers/ruby_cms/page_preview_controller.js +135 -0
- data/app/javascript/controllers/ruby_cms/toggle_controller.js +39 -0
- data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +321 -0
- data/app/models/concerns/content_block/publishable.rb +54 -0
- data/app/models/concerns/content_block/searchable.rb +22 -0
- data/app/models/content_block.rb +155 -0
- data/app/models/ruby_cms/content_block.rb +8 -0
- data/app/models/ruby_cms/permission.rb +28 -0
- data/app/models/ruby_cms/permittable.rb +39 -0
- data/app/models/ruby_cms/preference.rb +111 -0
- data/app/models/ruby_cms/user_permission.rb +12 -0
- data/app/models/ruby_cms/visitor_error.rb +109 -0
- data/app/services/ruby_cms/analytics/report.rb +362 -0
- data/app/services/ruby_cms/security_tracker.rb +92 -0
- data/app/views/layouts/ruby_cms/_admin_flash_messages.html.erb +37 -0
- data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +121 -0
- data/app/views/layouts/ruby_cms/admin.html.erb +81 -0
- data/app/views/layouts/ruby_cms/minimal.html.erb +181 -0
- data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -0
- data/app/views/ruby_cms/admin/analytics/page_details.html.erb +84 -0
- data/app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb +3 -0
- data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +40 -0
- data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +58 -0
- data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +51 -0
- data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +31 -0
- data/app/views/ruby_cms/admin/analytics/partials/_security_alert.html.erb +4 -0
- data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +21 -0
- data/app/views/ruby_cms/admin/analytics/visitor_details.html.erb +125 -0
- data/app/views/ruby_cms/admin/content_blocks/_form.html.erb +161 -0
- data/app/views/ruby_cms/admin/content_blocks/_row.html.erb +25 -0
- data/app/views/ruby_cms/admin/content_blocks/edit.html.erb +17 -0
- data/app/views/ruby_cms/admin/content_blocks/index.html.erb +66 -0
- data/app/views/ruby_cms/admin/content_blocks/new.html.erb +5 -0
- data/app/views/ruby_cms/admin/content_blocks/show.html.erb +110 -0
- data/app/views/ruby_cms/admin/dashboard/index.html.erb +198 -0
- data/app/views/ruby_cms/admin/permissions/_row.html.erb +11 -0
- data/app/views/ruby_cms/admin/permissions/index.html.erb +62 -0
- data/app/views/ruby_cms/admin/settings/index.html.erb +220 -0
- data/app/views/ruby_cms/admin/shared/_bulk_action_table_index.html.erb +56 -0
- data/app/views/ruby_cms/admin/user_permissions/index.html.erb +55 -0
- data/app/views/ruby_cms/admin/users/_row.html.erb +14 -0
- data/app/views/ruby_cms/admin/users/index.html.erb +70 -0
- data/app/views/ruby_cms/admin/visitor_errors/_row.html.erb +35 -0
- data/app/views/ruby_cms/admin/visitor_errors/index.html.erb +57 -0
- data/app/views/ruby_cms/admin/visitor_errors/show.html.erb +147 -0
- data/app/views/ruby_cms/admin/visual_editor/index.html.erb +144 -0
- data/app/views/ruby_cms/errors/not_found.html.erb +92 -0
- data/config/database.yml +6 -0
- data/config/importmap.rb +36 -0
- data/config/locales/en.yml +101 -0
- data/config/routes.rb +65 -0
- data/db/migrate/20260125000001_create_ruby_cms_permissions.rb +14 -0
- data/db/migrate/20260125000002_create_ruby_cms_user_permissions.rb +14 -0
- data/db/migrate/20260125000003_create_ruby_cms_content_blocks.rb +19 -0
- data/db/migrate/20260125000010_add_indexes_to_ruby_cms_tables.rb +9 -0
- data/db/migrate/20260127000001_add_locale_to_ruby_cms_content_blocks.rb +34 -0
- data/db/migrate/20260129000001_create_ruby_cms_visitor_errors.rb +24 -0
- data/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +8 -0
- data/db/migrate/20260130000002_create_ruby_cms_preferences.rb +16 -0
- data/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +8 -0
- data/db/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb +19 -0
- data/db/migrate/20260212000001_use_unprefixed_cms_tables.rb +146 -0
- data/exe/ruby_cms +25 -0
- data/lib/generators/ruby_cms/install_generator.rb +1062 -0
- data/lib/generators/ruby_cms/templates/admin.html.erb +82 -0
- data/lib/generators/ruby_cms/templates/ruby_cms.rb +86 -0
- data/lib/ruby_cms/app_integration.rb +82 -0
- data/lib/ruby_cms/cli.rb +169 -0
- data/lib/ruby_cms/content_blocks_grouping.rb +41 -0
- data/lib/ruby_cms/content_blocks_sync.rb +329 -0
- data/lib/ruby_cms/css_compiler.rb +35 -0
- data/lib/ruby_cms/engine.rb +498 -0
- data/lib/ruby_cms/settings.rb +145 -0
- data/lib/ruby_cms/settings_registry.rb +289 -0
- data/lib/ruby_cms/version.rb +5 -0
- data/lib/ruby_cms.rb +195 -0
- data/lib/tasks/ruby_cms.rake +27 -0
- data/log/test.log +17875 -0
- data/sig/ruby_cms.rbs +4 -0
- metadata +223 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module RubyCms
|
|
6
|
+
# Service class for syncing content blocks between database and YAML locale files
|
|
7
|
+
class ContentBlocksSync
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
|
|
10
|
+
def initialize(namespace: nil, locales_dir: nil)
|
|
11
|
+
@namespace = namespace || default_namespace_from_config
|
|
12
|
+
@locales_dir = locales_dir || Rails.root.join("config/locales")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Export all published content blocks to YAML files
|
|
16
|
+
# Creates/updates locale files for each locale configured in I18n.available_locales
|
|
17
|
+
# @param only_published [Boolean] If true, only export published blocks
|
|
18
|
+
# @param flatten_keys [Boolean] If true, flatten dot-separated keys into nested structure
|
|
19
|
+
# @return [Hash] Summary of exported blocks per locale
|
|
20
|
+
def export_to_yaml(only_published: true, flatten_keys: false)
|
|
21
|
+
scope = only_published ? RubyCms::ContentBlock.published : RubyCms::ContentBlock.all
|
|
22
|
+
blocks_by_locale = scope.order(:key).group_by(&:locale)
|
|
23
|
+
|
|
24
|
+
summary = {}
|
|
25
|
+
I18n.available_locales.each do |locale|
|
|
26
|
+
summary[locale] = export_locale_to_yaml(
|
|
27
|
+
locale, blocks_by_locale, flatten_keys:
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
summary
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Import content blocks from YAML locale files to database
|
|
35
|
+
# @param locale [Symbol, String] Specific locale to import, or nil for all locales
|
|
36
|
+
# @param create_missing [Boolean] Create content blocks that don't exist in DB
|
|
37
|
+
# @param update_existing [Boolean] Update existing content blocks
|
|
38
|
+
# @param published [Boolean] Set published status for imported blocks
|
|
39
|
+
# @return [Hash] Summary of imported/updated blocks
|
|
40
|
+
def import_from_yaml(locale: nil, create_missing: true, update_existing: true, published: false)
|
|
41
|
+
locales_to_process = locale ? [locale.to_sym] : I18n.available_locales
|
|
42
|
+
summary = { created: 0, updated: 0, skipped: 0, errors: [] }
|
|
43
|
+
|
|
44
|
+
locales_to_process.each do |loc|
|
|
45
|
+
import_locale_from_yaml(
|
|
46
|
+
loc,
|
|
47
|
+
summary,
|
|
48
|
+
create_missing:,
|
|
49
|
+
update_existing:,
|
|
50
|
+
published:
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
summary
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Sync: export database to YAML, then optionally import from YAML
|
|
58
|
+
# Useful for keeping both in sync
|
|
59
|
+
# @param import_after_export [Boolean] Import from YAML after exporting
|
|
60
|
+
# @return [Hash] Summary of operations
|
|
61
|
+
def sync(import_after_export: false)
|
|
62
|
+
result = { export: {}, import: {} }
|
|
63
|
+
|
|
64
|
+
# Export database to YAML
|
|
65
|
+
result[:export] = export_to_yaml
|
|
66
|
+
|
|
67
|
+
# Optionally import from YAML (useful for seeding from YAML)
|
|
68
|
+
result[:import] = import_from_yaml if import_after_export
|
|
69
|
+
|
|
70
|
+
result
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def default_namespace_from_config
|
|
76
|
+
Rails.application.config.ruby_cms.content_blocks_translation_namespace
|
|
77
|
+
rescue StandardError
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def export_locale_to_yaml(locale, blocks_by_locale, flatten_keys:)
|
|
82
|
+
locale_str = locale.to_s
|
|
83
|
+
locale_file = @locales_dir.join("#{locale}.yml")
|
|
84
|
+
blocks = blocks_by_locale[locale_str] || []
|
|
85
|
+
blocks_hash = blocks.index_by(&:key)
|
|
86
|
+
update_locale_file(locale_file, locale, blocks_hash, flatten_keys:)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def update_locale_file(locale_file, locale, blocks, flatten_keys: false)
|
|
90
|
+
existing_data = load_existing_locale_data(locale_file)
|
|
91
|
+
locale_root = ensure_locale_root(existing_data, locale)
|
|
92
|
+
target_hash = target_hash_for_locale(locale_root)
|
|
93
|
+
|
|
94
|
+
updated_count = update_target_hash(
|
|
95
|
+
target_hash,
|
|
96
|
+
blocks,
|
|
97
|
+
flatten_keys:
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
write_locale_file(locale_file, existing_data)
|
|
101
|
+
updated_count
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def load_existing_locale_data(locale_file)
|
|
105
|
+
return {} unless locale_file.exist?
|
|
106
|
+
|
|
107
|
+
safe_yaml_load_file(locale_file) || {}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def ensure_locale_root(existing_data, locale)
|
|
111
|
+
existing_data[locale.to_s] ||= {}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def target_hash_for_locale(locale_root)
|
|
115
|
+
return locale_root if @namespace.blank?
|
|
116
|
+
|
|
117
|
+
locale_root[@namespace] ||= {}
|
|
118
|
+
locale_root[@namespace]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def update_target_hash(target_hash, blocks, flatten_keys:)
|
|
122
|
+
updated_count = 0
|
|
123
|
+
|
|
124
|
+
blocks.each do |key, block|
|
|
125
|
+
updated_count += apply_block_to_target_hash(
|
|
126
|
+
target_hash,
|
|
127
|
+
key,
|
|
128
|
+
block,
|
|
129
|
+
flatten_keys:
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
updated_count
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def apply_block_to_target_hash(target_hash, key, block, flatten_keys:)
|
|
137
|
+
content = extract_content_from_block(block)
|
|
138
|
+
key_str = key.to_s
|
|
139
|
+
|
|
140
|
+
if flatten_keys && key_str.include?(".")
|
|
141
|
+
merge_nested_hash(target_hash, unflatten_key(key_str, content))
|
|
142
|
+
return 0
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
return 0 if target_hash[key_str] == content
|
|
146
|
+
|
|
147
|
+
target_hash[key_str] = content
|
|
148
|
+
1
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def write_locale_file(locale_file, existing_data)
|
|
152
|
+
File.write(locale_file, existing_data.to_yaml)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Convert dot-separated key to nested hash structure
|
|
156
|
+
# Example: "hero.title" => { "hero" => { "title" => content } }
|
|
157
|
+
def unflatten_key(key, content)
|
|
158
|
+
parts = key.split(".")
|
|
159
|
+
result = {}
|
|
160
|
+
current = result
|
|
161
|
+
|
|
162
|
+
parts[0..-2].each do |part|
|
|
163
|
+
current[part] = {}
|
|
164
|
+
current = current[part]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
current[parts.last] = content
|
|
168
|
+
result
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Merge nested hash into target hash
|
|
172
|
+
def merge_nested_hash(target, source)
|
|
173
|
+
source.each do |key, value|
|
|
174
|
+
if value.kind_of?(Hash) && target[key].kind_of?(Hash)
|
|
175
|
+
merge_nested_hash(target[key], value)
|
|
176
|
+
else
|
|
177
|
+
target[key] = value
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def extract_content_from_block(block)
|
|
183
|
+
return rich_text_as_plain_text(block) if block.content_type == "rich_text"
|
|
184
|
+
|
|
185
|
+
block.content.to_s
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def rich_text_as_plain_text(block)
|
|
189
|
+
return block.content.to_s unless block.respond_to?(:rich_content)
|
|
190
|
+
return block.content.to_s if block.rich_content.blank?
|
|
191
|
+
|
|
192
|
+
block.rich_content.to_plain_text.presence || block.content.to_s
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def extract_blocks_from_locale(locale_data, locale)
|
|
196
|
+
locale_key = locale.to_s
|
|
197
|
+
return {} unless locale_data[locale_key]
|
|
198
|
+
|
|
199
|
+
if @namespace.present?
|
|
200
|
+
namespace_data = locale_data[locale_key][@namespace]
|
|
201
|
+
return {} unless namespace_data
|
|
202
|
+
|
|
203
|
+
flatten_hash(namespace_data)
|
|
204
|
+
else
|
|
205
|
+
# Filter out non-content-block keys (like activemodel, etc.)
|
|
206
|
+
filtered = locale_data[locale_key].reject {|k, _v| reserved_keys.include?(k.to_s) }
|
|
207
|
+
flatten_hash(filtered)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Flatten nested hash structure to dot-separated keys
|
|
212
|
+
# Example: { hero: { title: "..." } } => { "hero.title" => "..." }
|
|
213
|
+
def flatten_hash(hash, prefix: nil)
|
|
214
|
+
result = {}
|
|
215
|
+
hash.each do |key, value|
|
|
216
|
+
merge_flattened_value(result, key, value, prefix)
|
|
217
|
+
end
|
|
218
|
+
result
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def merge_flattened_value(result, key, value, prefix)
|
|
222
|
+
new_key = prefix ? "#{prefix}.#{key}" : key.to_s
|
|
223
|
+
|
|
224
|
+
if value.kind_of?(Hash)
|
|
225
|
+
result.merge!(flatten_hash(value, prefix: new_key))
|
|
226
|
+
return
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
return if value.kind_of?(Array)
|
|
230
|
+
|
|
231
|
+
result[new_key] = value.to_s
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def reserved_keys
|
|
235
|
+
%w[activemodel activerecord date time number currency support]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def import_locale_from_yaml(loc, summary, create_missing:, update_existing:, published:)
|
|
239
|
+
locale_file = @locales_dir.join("#{loc}.yml")
|
|
240
|
+
return unless locale_file.exist?
|
|
241
|
+
|
|
242
|
+
locale_data = safe_yaml_load_file(locale_file)
|
|
243
|
+
blocks_data = extract_blocks_from_locale(locale_data, loc)
|
|
244
|
+
|
|
245
|
+
assign_blocks_data_to_summary(summary, blocks_data, loc, create_missing:, update_existing:,
|
|
246
|
+
published:)
|
|
247
|
+
rescue StandardError => e
|
|
248
|
+
summary[:errors] << "Error processing #{loc}: #{e.message}"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def safe_yaml_load_file(path)
|
|
252
|
+
# Locale YAML should deserialize to Hash/Array/String/etc. Avoid YAML.load_file (object deserialization).
|
|
253
|
+
opts = {
|
|
254
|
+
permitted_classes: [],
|
|
255
|
+
permitted_symbols: [],
|
|
256
|
+
aliases: true
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return YAML.safe_load_file(path, **opts) if YAML.respond_to?(:safe_load_file)
|
|
260
|
+
|
|
261
|
+
YAML.safe_load_file(path, **opts)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def assign_blocks_data_to_summary(summary, blocks_data, locale, create_missing:, update_existing:, published:)
|
|
265
|
+
blocks_data.each do |key, content|
|
|
266
|
+
result = import_block(key, content, locale, create_missing:, update_existing:, published:)
|
|
267
|
+
summary[result[:action]] += 1
|
|
268
|
+
summary[:errors] << result[:error] if result[:error]
|
|
269
|
+
end
|
|
270
|
+
summary
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def import_block(key, content, locale, create_missing:, update_existing:, published:)
|
|
274
|
+
block = RubyCms::ContentBlock.find_by(key: key, locale: locale.to_s)
|
|
275
|
+
return import_new_block(key, content, locale, published) if block.nil? && create_missing
|
|
276
|
+
return { action: :skipped, error: nil } if block.nil?
|
|
277
|
+
return { action: :skipped, error: nil } unless update_existing
|
|
278
|
+
|
|
279
|
+
update_existing_block(block, key, content, locale, published)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def import_new_block(key, content, locale, published)
|
|
283
|
+
block = RubyCms::ContentBlock.new(
|
|
284
|
+
key: key,
|
|
285
|
+
locale: locale.to_s,
|
|
286
|
+
content: content.to_s,
|
|
287
|
+
content_type: infer_content_type(content),
|
|
288
|
+
published: published
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
save_block(block, key, locale, action: :created, failure_verb: "create")
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def update_existing_block(block, key, content, locale, published)
|
|
295
|
+
block.content = content.to_s
|
|
296
|
+
block.published = published if block.published != published
|
|
297
|
+
set_inferred_type_if_text(block, content)
|
|
298
|
+
|
|
299
|
+
save_block(block, key, locale, action: :updated, failure_verb: "update")
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def set_inferred_type_if_text(block, content)
|
|
303
|
+
return unless block.content_type == "text"
|
|
304
|
+
|
|
305
|
+
block.content_type = infer_content_type(content)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def save_block(block, key, locale, action:, failure_verb:)
|
|
309
|
+
return { action: action, error: nil } if block.save
|
|
310
|
+
|
|
311
|
+
{ action: :skipped, error: block_failure_message(block, key, locale, failure_verb) }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def block_failure_message(block, key, locale, failure_verb)
|
|
315
|
+
errors = block.errors.full_messages.join(", ")
|
|
316
|
+
"Failed to #{failure_verb} #{key} (#{locale}): #{errors}"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def infer_content_type(content)
|
|
320
|
+
# Simple heuristic: if content looks like HTML, use rich_text
|
|
321
|
+
content_str = content.to_s
|
|
322
|
+
if content_str.match?(/<[a-z][\s\S]*>/i)
|
|
323
|
+
"rich_text"
|
|
324
|
+
else
|
|
325
|
+
"text"
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module RubyCms
|
|
6
|
+
# Compiles admin.css from component files (no Rails required).
|
|
7
|
+
# Used by Engine.compile_admin_css and rake tasks.
|
|
8
|
+
module CssCompiler
|
|
9
|
+
# Core shared styles MUST be loaded first, then specific components
|
|
10
|
+
COMPONENTS = %w[
|
|
11
|
+
shared
|
|
12
|
+
layout sidebar header cards dashboard buttons forms alerts
|
|
13
|
+
flash_toast modals content_blocks visitor_errors settings
|
|
14
|
+
bulk_action_table bulk_action_table_bar bulk_action_table_delete
|
|
15
|
+
visual_editor visual_editor_header visual_editor_preview visual_editor_modal
|
|
16
|
+
visual_editor_edit_mode analytics
|
|
17
|
+
mobile scrollbar utilities
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def self.compile(gem_root, dest_path)
|
|
21
|
+
src_dir = Pathname(gem_root).join("app/assets/stylesheets/ruby_cms")
|
|
22
|
+
components_dir = src_dir.join("components")
|
|
23
|
+
header = <<~CSS
|
|
24
|
+
|
|
25
|
+
CSS
|
|
26
|
+
content = COMPONENTS.filter_map do |name|
|
|
27
|
+
file = components_dir.join("#{name}.css")
|
|
28
|
+
next nil unless file.exist?
|
|
29
|
+
|
|
30
|
+
"/* ===== Component: #{name} ===== */\n#{File.read(file)}\n"
|
|
31
|
+
end.compact.join("\n")
|
|
32
|
+
File.write(dest_path, header + content)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|