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,289 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCms
|
|
4
|
+
module SettingsRegistry
|
|
5
|
+
Entry = Struct.new(
|
|
6
|
+
:key,
|
|
7
|
+
:type,
|
|
8
|
+
:default,
|
|
9
|
+
:category,
|
|
10
|
+
:description
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
mattr_accessor :entries, default: {}
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def register(key:, type:, default:, category: "general", description: nil)
|
|
17
|
+
k = key.to_s
|
|
18
|
+
entries[k] = Entry.new(
|
|
19
|
+
key: k,
|
|
20
|
+
type: normalize_type(type),
|
|
21
|
+
default: default,
|
|
22
|
+
category: category.to_s,
|
|
23
|
+
description: description.to_s
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def fetch(key)
|
|
28
|
+
entries[key.to_s]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def each(&)
|
|
32
|
+
entries.each_value(&)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def defaults_hash
|
|
36
|
+
seed_defaults! if entries.empty?
|
|
37
|
+
|
|
38
|
+
entries.values.to_h do |entry|
|
|
39
|
+
[
|
|
40
|
+
entry.key.to_sym,
|
|
41
|
+
{
|
|
42
|
+
value: entry.default,
|
|
43
|
+
type: value_type_for(entry.type),
|
|
44
|
+
description: entry.description,
|
|
45
|
+
category: entry.category
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def seed_defaults!
|
|
52
|
+
return unless entries.empty?
|
|
53
|
+
|
|
54
|
+
# Pagination
|
|
55
|
+
register(
|
|
56
|
+
key: :visitor_errors_per_page,
|
|
57
|
+
type: :integer,
|
|
58
|
+
default: 25,
|
|
59
|
+
category: :pagination,
|
|
60
|
+
description: "Number of visitor errors to show per page"
|
|
61
|
+
)
|
|
62
|
+
register(
|
|
63
|
+
key: :content_blocks_per_page,
|
|
64
|
+
type: :integer,
|
|
65
|
+
default: 50,
|
|
66
|
+
category: :pagination,
|
|
67
|
+
description: "Number of content blocks to show per page"
|
|
68
|
+
)
|
|
69
|
+
register(
|
|
70
|
+
key: :users_per_page,
|
|
71
|
+
type: :integer,
|
|
72
|
+
default: 50,
|
|
73
|
+
category: :pagination,
|
|
74
|
+
description: "Number of users to show per page"
|
|
75
|
+
)
|
|
76
|
+
register(
|
|
77
|
+
key: :permissions_per_page,
|
|
78
|
+
type: :integer,
|
|
79
|
+
default: 50,
|
|
80
|
+
category: :pagination,
|
|
81
|
+
description: "Number of permissions to show per page"
|
|
82
|
+
)
|
|
83
|
+
register(
|
|
84
|
+
key: :pagination_min_per_page,
|
|
85
|
+
type: :integer,
|
|
86
|
+
default: 5,
|
|
87
|
+
category: :pagination,
|
|
88
|
+
description: "Minimum allowed per-page size"
|
|
89
|
+
)
|
|
90
|
+
register(
|
|
91
|
+
key: :pagination_max_per_page,
|
|
92
|
+
type: :integer,
|
|
93
|
+
default: 200,
|
|
94
|
+
category: :pagination,
|
|
95
|
+
description: "Maximum allowed per-page size"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Analytics
|
|
99
|
+
register(
|
|
100
|
+
key: :analytics_high_volume_threshold,
|
|
101
|
+
type: :integer,
|
|
102
|
+
default: 1000,
|
|
103
|
+
category: :analytics,
|
|
104
|
+
description: "Threshold of visits per IP in selected period to flag high volume traffic"
|
|
105
|
+
)
|
|
106
|
+
register(
|
|
107
|
+
key: :analytics_rapid_request_threshold,
|
|
108
|
+
type: :integer,
|
|
109
|
+
default: 50,
|
|
110
|
+
category: :analytics,
|
|
111
|
+
description: "Threshold of visits from an IP per minute to flag rapid requests"
|
|
112
|
+
)
|
|
113
|
+
register(
|
|
114
|
+
key: :analytics_default_period,
|
|
115
|
+
type: :string,
|
|
116
|
+
default: "week",
|
|
117
|
+
category: :analytics,
|
|
118
|
+
description: "Default analytics period (day/week/month/year)"
|
|
119
|
+
)
|
|
120
|
+
register(
|
|
121
|
+
key: :analytics_max_date_range_days,
|
|
122
|
+
type: :integer,
|
|
123
|
+
default: 365,
|
|
124
|
+
category: :analytics,
|
|
125
|
+
description: "Maximum date range for analytics filters"
|
|
126
|
+
)
|
|
127
|
+
register(
|
|
128
|
+
key: :analytics_max_popular_pages,
|
|
129
|
+
type: :integer,
|
|
130
|
+
default: 10,
|
|
131
|
+
category: :analytics,
|
|
132
|
+
description: "Maximum number of popular pages to show"
|
|
133
|
+
)
|
|
134
|
+
register(
|
|
135
|
+
key: :analytics_max_top_visitors,
|
|
136
|
+
type: :integer,
|
|
137
|
+
default: 10,
|
|
138
|
+
category: :analytics,
|
|
139
|
+
description: "Maximum number of top visitors to show"
|
|
140
|
+
)
|
|
141
|
+
register(
|
|
142
|
+
key: :analytics_cache_duration_seconds,
|
|
143
|
+
type: :integer,
|
|
144
|
+
default: 600,
|
|
145
|
+
category: :analytics,
|
|
146
|
+
description: "Analytics cache duration in seconds"
|
|
147
|
+
)
|
|
148
|
+
register(
|
|
149
|
+
key: :analytics_recent_page_views_limit,
|
|
150
|
+
type: :integer,
|
|
151
|
+
default: 25,
|
|
152
|
+
category: :analytics,
|
|
153
|
+
description: "Recent page views shown on analytics dashboard"
|
|
154
|
+
)
|
|
155
|
+
register(
|
|
156
|
+
key: :analytics_page_details_limit,
|
|
157
|
+
type: :integer,
|
|
158
|
+
default: 100,
|
|
159
|
+
category: :analytics,
|
|
160
|
+
description: "Events shown on page details screen"
|
|
161
|
+
)
|
|
162
|
+
register(
|
|
163
|
+
key: :analytics_visitor_details_limit,
|
|
164
|
+
type: :integer,
|
|
165
|
+
default: 100,
|
|
166
|
+
category: :analytics,
|
|
167
|
+
description: "Events shown on visitor details screen"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Dashboard
|
|
171
|
+
register(
|
|
172
|
+
key: :dashboard_recent_errors_limit,
|
|
173
|
+
type: :integer,
|
|
174
|
+
default: 5,
|
|
175
|
+
category: :dashboard,
|
|
176
|
+
description: "Recent visitor errors shown on dashboard"
|
|
177
|
+
)
|
|
178
|
+
register(
|
|
179
|
+
key: :dashboard_recent_content_blocks_limit,
|
|
180
|
+
type: :integer,
|
|
181
|
+
default: 5,
|
|
182
|
+
category: :dashboard,
|
|
183
|
+
description: "Recent content blocks shown on dashboard"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Navigation visibility
|
|
187
|
+
register(
|
|
188
|
+
key: :nav_show_dashboard,
|
|
189
|
+
type: :boolean,
|
|
190
|
+
default: true,
|
|
191
|
+
category: :navigation,
|
|
192
|
+
description: "Show Dashboard in navigation"
|
|
193
|
+
)
|
|
194
|
+
register(
|
|
195
|
+
key: :nav_show_visual_editor,
|
|
196
|
+
type: :boolean,
|
|
197
|
+
default: true,
|
|
198
|
+
category: :navigation,
|
|
199
|
+
description: "Show Visual Editor in navigation"
|
|
200
|
+
)
|
|
201
|
+
register(
|
|
202
|
+
key: :nav_show_content_blocks,
|
|
203
|
+
type: :boolean,
|
|
204
|
+
default: true,
|
|
205
|
+
category: :navigation,
|
|
206
|
+
description: "Show Content Blocks in navigation"
|
|
207
|
+
)
|
|
208
|
+
register(
|
|
209
|
+
key: :nav_show_settings,
|
|
210
|
+
type: :boolean,
|
|
211
|
+
default: true,
|
|
212
|
+
category: :navigation,
|
|
213
|
+
description: "Show Settings in navigation"
|
|
214
|
+
)
|
|
215
|
+
register(
|
|
216
|
+
key: :nav_show_visitor_errors,
|
|
217
|
+
type: :boolean,
|
|
218
|
+
default: true,
|
|
219
|
+
category: :navigation,
|
|
220
|
+
description: "Show Visitor Errors in navigation"
|
|
221
|
+
)
|
|
222
|
+
register(
|
|
223
|
+
key: :nav_show_permissions,
|
|
224
|
+
type: :boolean,
|
|
225
|
+
default: true,
|
|
226
|
+
category: :navigation,
|
|
227
|
+
description: "Show Permissions in navigation"
|
|
228
|
+
)
|
|
229
|
+
register(
|
|
230
|
+
key: :nav_show_users,
|
|
231
|
+
type: :boolean,
|
|
232
|
+
default: true,
|
|
233
|
+
category: :navigation,
|
|
234
|
+
description: "Show Users in navigation"
|
|
235
|
+
)
|
|
236
|
+
register(
|
|
237
|
+
key: :nav_show_analytics,
|
|
238
|
+
type: :boolean,
|
|
239
|
+
default: true,
|
|
240
|
+
category: :navigation,
|
|
241
|
+
description: "Show Analytics in navigation"
|
|
242
|
+
)
|
|
243
|
+
register(
|
|
244
|
+
key: :nav_order,
|
|
245
|
+
type: :json,
|
|
246
|
+
default: [],
|
|
247
|
+
category: :navigation,
|
|
248
|
+
description: "Order of nav items (set via Settings → Navigation drag-and-drop)"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Content/image constraints
|
|
252
|
+
register(
|
|
253
|
+
key: :reserved_key_prefixes,
|
|
254
|
+
type: :json,
|
|
255
|
+
default: ["admin_"],
|
|
256
|
+
category: :content,
|
|
257
|
+
description: "Reserved prefixes blocked for content block keys"
|
|
258
|
+
)
|
|
259
|
+
register(
|
|
260
|
+
key: :image_content_types,
|
|
261
|
+
type: :json,
|
|
262
|
+
default: ["image/png", "image/jpeg", "image/gif", "image/webp"],
|
|
263
|
+
category: :content,
|
|
264
|
+
description: "Allowed MIME types for content block images"
|
|
265
|
+
)
|
|
266
|
+
register(
|
|
267
|
+
key: :image_max_size,
|
|
268
|
+
type: :integer,
|
|
269
|
+
default: 5 * 1024 * 1024,
|
|
270
|
+
category: :content,
|
|
271
|
+
description: "Max image attachment size in bytes"
|
|
272
|
+
)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
private
|
|
276
|
+
|
|
277
|
+
def normalize_type(type)
|
|
278
|
+
t = type.to_sym
|
|
279
|
+
return t if %i[string integer boolean json].include?(t)
|
|
280
|
+
|
|
281
|
+
:string
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def value_type_for(type)
|
|
285
|
+
normalize_type(type).to_s
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
data/lib/ruby_cms.rb
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ruby_cms/version"
|
|
4
|
+
require_relative "ruby_cms/css_compiler"
|
|
5
|
+
require_relative "ruby_cms/settings_registry"
|
|
6
|
+
require_relative "ruby_cms/settings"
|
|
7
|
+
require_relative "ruby_cms/engine"
|
|
8
|
+
require_relative "ruby_cms/app_integration"
|
|
9
|
+
require_relative "ruby_cms/content_blocks_sync"
|
|
10
|
+
require_relative "ruby_cms/content_blocks_grouping"
|
|
11
|
+
|
|
12
|
+
module RubyCms
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
|
|
15
|
+
mattr_accessor :nav_registry
|
|
16
|
+
self.nav_registry = []
|
|
17
|
+
|
|
18
|
+
# Navigation section keys. Order in sidebar: main, then Settings (bottom).
|
|
19
|
+
# User can add items to either via nav_register with section: NAV_SECTION_MAIN (order 10+)
|
|
20
|
+
# or section: NAV_SECTION_BOTTOM (order 10+).
|
|
21
|
+
NAV_SECTION_MAIN = "main"
|
|
22
|
+
NAV_SECTION_BOTTOM = "Settings"
|
|
23
|
+
|
|
24
|
+
ANALYTICS_ICON_PATH = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
|
|
25
|
+
'd="M3 3v18h18"></path>' \
|
|
26
|
+
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" ' \
|
|
27
|
+
'd="M7 13l3-3 3 2 4-5"></path>'
|
|
28
|
+
|
|
29
|
+
def self.configure
|
|
30
|
+
yield(Rails.application.config.ruby_cms)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.setting(key, default: nil)
|
|
34
|
+
RubyCms::Settings.get(key, default:)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Register a link for admin navigation.
|
|
38
|
+
# Sections: main (top), Settings (bottom). Add to main with section: "main", order: 10+;
|
|
39
|
+
# add to bottom with section: "Settings", order: 10+. Order can be changed via Settings → Navigation drag-and-drop.
|
|
40
|
+
# Options:
|
|
41
|
+
# - key: required Symbol/String
|
|
42
|
+
# - label: required String
|
|
43
|
+
# - path: required String or callable(view_context) -> path
|
|
44
|
+
# - icon: optional SVG path fragment
|
|
45
|
+
# - section: "main" | "Settings" (or nil => main)
|
|
46
|
+
# - order: optional Integer for sorting within section
|
|
47
|
+
# - permission: optional permission key (e.g., :manage_analytics)
|
|
48
|
+
# - default_visible: optional Boolean (default true)
|
|
49
|
+
# - if: optional callable for custom visibility gate
|
|
50
|
+
def self.nav_register(key:, label:, path:, icon: nil, section: nil, order: nil, permission: nil, default_visible: true, **options)
|
|
51
|
+
normalized_key = key.to_sym
|
|
52
|
+
normalized_section = section.presence || NAV_SECTION_MAIN
|
|
53
|
+
entry = {
|
|
54
|
+
key: normalized_key,
|
|
55
|
+
label: label.to_s,
|
|
56
|
+
path: path,
|
|
57
|
+
icon: icon,
|
|
58
|
+
section: normalized_section,
|
|
59
|
+
order: order,
|
|
60
|
+
permission: permission&.to_s,
|
|
61
|
+
default_visible: default_visible ? true : false,
|
|
62
|
+
if: options[:if]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
self.nav_registry = nav_registry.reject {|e| e[:key] == normalized_key }
|
|
66
|
+
self.nav_registry += [entry]
|
|
67
|
+
|
|
68
|
+
register_navigation_setting!(entry)
|
|
69
|
+
|
|
70
|
+
entry
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns only entries that pass settings + permissions + conditional checks,
|
|
74
|
+
# sorted by saved nav_order (Settings → Navigation drag-and-drop) when set, else by section + order.
|
|
75
|
+
def self.visible_nav_registry(view_context: nil, user: nil)
|
|
76
|
+
list = nav_registry
|
|
77
|
+
.select {|item| nav_entry_visible?(item, view_context:, user:) }
|
|
78
|
+
.sort_by {|item| nav_sort_tuple(item) }
|
|
79
|
+
apply_nav_order(list)
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
Rails.logger.error("[RubyCMS] Error filtering navigation: #{e.message}") if defined?(Rails.logger)
|
|
82
|
+
nav_registry
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
module Nav
|
|
86
|
+
def self.register(key:, label:, path:, icon: nil, section: nil, order: nil, permission: nil, default_visible: true, **)
|
|
87
|
+
RubyCms.nav_register(
|
|
88
|
+
key:,
|
|
89
|
+
label:,
|
|
90
|
+
path:,
|
|
91
|
+
icon:,
|
|
92
|
+
section:,
|
|
93
|
+
order:,
|
|
94
|
+
permission:,
|
|
95
|
+
default_visible:,
|
|
96
|
+
**
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class << self
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def nav_sort_tuple(item)
|
|
105
|
+
section = item[:section].to_s.presence || NAV_SECTION_MAIN
|
|
106
|
+
priority = nav_section_priority(section)
|
|
107
|
+
[priority, item[:order] || 1000, item[:label].to_s]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def nav_section_priority(section)
|
|
111
|
+
section.to_s == NAV_SECTION_MAIN ? 0 : 2
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def apply_nav_order(list)
|
|
115
|
+
# Read directly from DB so order is always from Settings, not Settings.get (which can return registry default).
|
|
116
|
+
pref = RubyCms::Preference.find_by(key: "nav_order")
|
|
117
|
+
return list unless pref&.value_type == "json" && pref.value.present?
|
|
118
|
+
|
|
119
|
+
saved = JSON.parse(pref.value)
|
|
120
|
+
return list unless saved.kind_of?(Array) && saved.any?
|
|
121
|
+
|
|
122
|
+
order_map = saved.each_with_index.to_h {|k, i| [k.to_s, i] }
|
|
123
|
+
list.sort_by {|item| order_map.fetch(item[:key].to_s, 9999) }
|
|
124
|
+
rescue StandardError
|
|
125
|
+
list
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def nav_entry_visible?(item, view_context:, user:)
|
|
129
|
+
return false unless setting_enabled_for_nav_item?(item)
|
|
130
|
+
return false unless permission_allows_nav_item?(item, view_context:, user:)
|
|
131
|
+
return false unless condition_allows_nav_item?(item, view_context:)
|
|
132
|
+
|
|
133
|
+
true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def setting_enabled_for_nav_item?(item)
|
|
137
|
+
pref_key = :"nav_show_#{item[:key]}"
|
|
138
|
+
RubyCms::Settings.get(pref_key, default: item.fetch(:default_visible, true))
|
|
139
|
+
rescue StandardError
|
|
140
|
+
item.fetch(:default_visible, true)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def permission_allows_nav_item?(item, view_context:, user:)
|
|
144
|
+
permission_key = item[:permission]
|
|
145
|
+
return true if permission_key.blank?
|
|
146
|
+
|
|
147
|
+
current_user = user || user_from_view_context(view_context)
|
|
148
|
+
return false if current_user.nil?
|
|
149
|
+
return true unless current_user.respond_to?(:can?)
|
|
150
|
+
|
|
151
|
+
current_user.can?(permission_key.to_sym)
|
|
152
|
+
rescue StandardError
|
|
153
|
+
false
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def condition_allows_nav_item?(item, view_context:)
|
|
157
|
+
condition = item[:if]
|
|
158
|
+
return true unless condition.respond_to?(:call)
|
|
159
|
+
|
|
160
|
+
case condition.arity
|
|
161
|
+
when 1
|
|
162
|
+
condition.call(view_context)
|
|
163
|
+
else
|
|
164
|
+
condition.call
|
|
165
|
+
end
|
|
166
|
+
rescue StandardError
|
|
167
|
+
false
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def user_from_view_context(view_context)
|
|
171
|
+
return nil unless view_context
|
|
172
|
+
|
|
173
|
+
return view_context.current_user_cms if view_context.respond_to?(:current_user_cms)
|
|
174
|
+
return view_context.current_user if view_context.respond_to?(:current_user)
|
|
175
|
+
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def register_navigation_setting!(entry)
|
|
180
|
+
key = :"nav_show_#{entry[:key]}"
|
|
181
|
+
|
|
182
|
+
RubyCms::SettingsRegistry.register(
|
|
183
|
+
key: key,
|
|
184
|
+
type: :boolean,
|
|
185
|
+
default: entry.fetch(:default_visible, true),
|
|
186
|
+
category: :navigation,
|
|
187
|
+
description: "Show #{entry[:label]} in navigation"
|
|
188
|
+
)
|
|
189
|
+
rescue StandardError => e
|
|
190
|
+
Rails.logger.warn("[RubyCMS] Failed to register nav setting for #{entry[:key]}: #{e.message}") if defined?(Rails.logger)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
RubyCms::SettingsRegistry.seed_defaults!
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :ruby_cms do
|
|
4
|
+
namespace :css do
|
|
5
|
+
desc "Compile RubyCMS admin.css from component files (for gem development)"
|
|
6
|
+
task compile_gem: :environment do
|
|
7
|
+
require "ruby_cms/engine" unless defined?(RubyCms::Engine)
|
|
8
|
+
|
|
9
|
+
dest = RubyCms::Engine.root.join("app/assets/stylesheets/ruby_cms/admin.css")
|
|
10
|
+
RubyCms::Engine.compile_admin_css(dest)
|
|
11
|
+
puts "✓ Compiled admin.css in gem"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc "Compile RubyCMS CSS to host app (combines component files)"
|
|
15
|
+
task compile: :environment do
|
|
16
|
+
require "fileutils"
|
|
17
|
+
require "ruby_cms/engine" unless defined?(RubyCms::Engine)
|
|
18
|
+
|
|
19
|
+
dest_dir = Rails.root.join("app/assets/stylesheets/ruby_cms")
|
|
20
|
+
FileUtils.mkdir_p(dest_dir)
|
|
21
|
+
dest = dest_dir.join("admin.css")
|
|
22
|
+
RubyCms::Engine.compile_admin_css(dest)
|
|
23
|
+
puts "✓ Compiled admin.css to #{dest}"
|
|
24
|
+
puts "✓ RubyCMS CSS compilation complete!"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|