rails-settings-cached-rails-admin 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.
@@ -0,0 +1,435 @@
1
+ <%# Set page title with fallback %>
2
+ <% content_for :title do %>
3
+ <%= t('admin.actions.settings_ui.title', default: 'Settings') %>
4
+ <% end %>
5
+
6
+ <%# Set breadcrumb with fallback %>
7
+ <% content_for :breadcrumbs do %>
8
+ <li class="breadcrumb-item">
9
+ <%= link_to t('admin.home.name', default: 'Home'), rails_admin.dashboard_path %>
10
+ </li>
11
+ <% end %>
12
+
13
+ <!-- Search Bar -->
14
+ <div class="row mb-3">
15
+ <div class="col-12">
16
+ <div class="card">
17
+ <div class="card-body py-2">
18
+ <div class="row">
19
+ <div class="col-md-6">
20
+ <div class="input-group">
21
+ <div class="input-group-prepend">
22
+ <span class="input-group-text"><i class="fa fa-search"></i></span>
23
+ </div>
24
+ <input type="text" class="form-control" id="settings-search" placeholder="Search settings..." autocomplete="off">
25
+ <div class="input-group-append">
26
+ <button class="btn btn-outline-secondary" type="button" id="clear-search">
27
+ <i class="fa fa-times"></i>
28
+ </button>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ <div class="col-md-6">
33
+ <div class="float-right">
34
+ <span class="badge badge-info" id="settings-count">
35
+ <i class="fa fa-cog"></i> <span id="visible-count"><%= @settings_data.values.flatten.size %></span> settings
36
+ </span>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <!-- Settings Table -->
46
+ <div class="row">
47
+ <div class="col-md-12">
48
+ <% if @settings_data.any? %>
49
+ <% @settings_data.each_with_index do |(category, settings), category_index| %>
50
+ <div class="card settings-category" data-category="<%= category.parameterize %>">
51
+ <div class="card-header">
52
+ <h3 class="card-title">
53
+ <i class="fa fa-folder-o"></i>
54
+ <%= category %>
55
+ <span class="badge badge-primary ml-2"><%= settings.size %></span>
56
+ </h3>
57
+ <div class="card-tools">
58
+ <button type="button" class="btn btn-tool" data-card-widget="collapse">
59
+ <i class="fa fa-minus"></i>
60
+ </button>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="card-body table-responsive p-0">
65
+ <table class="table table-striped table-hover">
66
+ <thead>
67
+ <tr>
68
+ <th style="width: 250px;">Setting</th>
69
+ <th>Current Value</th>
70
+ <th style="width: 120px;">Type</th>
71
+ <th style="width: 100px;">Status</th>
72
+ <th style="width: 120px;">Actions</th>
73
+ </tr>
74
+ </thead>
75
+ <tbody>
76
+ <% settings.each do |setting| %>
77
+ <tr class="setting-row" data-key="<%= setting[:key] %>" data-searchable="<%= [setting[:label], setting[:key]].compact.join(' ').downcase %>">
78
+ <td>
79
+ <div class="setting-info">
80
+ <strong><%= setting[:label] %></strong>
81
+ <br>
82
+ <small class="text-muted"><code><%= setting[:key] %></code></small>
83
+ </div>
84
+ </td>
85
+
86
+ <td>
87
+ <div class="setting-value-container">
88
+ <%= form_tag request.path, method: :post, class: 'setting-form d-inline', local: false, data: { key: setting[:key] } do %>
89
+ <% field_name = "setting_value" %>
90
+ <% field_id = "setting_#{setting[:key]}" %>
91
+ <% current_value = setting[:current_value] %>
92
+ <%= hidden_field_tag "setting_key", setting[:key] %>
93
+
94
+ <div class="setting-input-group">
95
+ <% case setting[:field_type] %>
96
+ <% when :boolean %>
97
+ <div class="custom-control custom-switch">
98
+ <%= check_box_tag(field_name, '1', current_value, id: field_id, class: 'custom-control-input') %>
99
+ <%= hidden_field_tag(field_name, '0') %>
100
+ <%= label_tag(field_id, current_value ? 'Enabled' : 'Disabled', class: 'custom-control-label') %>
101
+ </div>
102
+ <% when :integer %>
103
+ <%= number_field_tag(field_name, current_value, id: field_id, class: 'form-control form-control-sm', step: 1, style: 'max-width: 200px;') %>
104
+ <% when :float %>
105
+ <%= number_field_tag(field_name, current_value, id: field_id, class: 'form-control form-control-sm', step: 0.01, style: 'max-width: 200px;') %>
106
+ <% when :text %>
107
+ <%= text_area_tag(field_name, current_value, id: field_id, class: 'form-control form-control-sm', rows: 2, style: 'min-width: 300px;') %>
108
+ <% when :email %>
109
+ <%= email_field_tag(field_name, current_value, id: field_id, class: 'form-control form-control-sm', style: 'min-width: 250px;') %>
110
+ <% when :url %>
111
+ <%= url_field_tag(field_name, current_value, id: field_id, class: 'form-control form-control-sm', style: 'min-width: 300px;') %>
112
+ <% when :array %>
113
+ <% array_value = current_value.is_a?(Array) ? current_value.join(', ') : current_value.to_s %>
114
+ <%= text_field_tag(field_name, array_value, id: field_id, class: 'form-control form-control-sm', placeholder: 'comma, separated, values', style: 'min-width: 300px;') %>
115
+ <% when :json %>
116
+ <% json_value = current_value.is_a?(String) ? current_value : current_value.to_json %>
117
+ <%= text_area_tag(field_name, json_value, id: field_id, class: 'form-control form-control-sm font-monospace', rows: 3, placeholder: '{"key": "value"}', style: 'min-width: 350px;') %>
118
+ <% else # :string %>
119
+ <%= text_field_tag(field_name, current_value, id: field_id, class: 'form-control form-control-sm', style: 'min-width: 250px;') %>
120
+ <% end %>
121
+
122
+ <% if [:array, :json].include?(setting[:field_type]) %>
123
+ <small class="form-text text-muted">
124
+ <% if setting[:field_type] == :array %>
125
+ Separate values with commas
126
+ <% else %>
127
+ Valid JSON format required
128
+ <% end %>
129
+ </small>
130
+ <% end %>
131
+ </div>
132
+ <% end %>
133
+
134
+ <% if setting[:default_value] != setting[:current_value] %>
135
+ <small class="text-muted mt-1 d-block">
136
+ <i class="fa fa-info-circle"></i>
137
+ Default: <code><%= setting[:default_value].inspect %></code>
138
+ </small>
139
+ <% end %>
140
+ </div>
141
+ </td>
142
+
143
+ <td>
144
+ <span>
145
+ <%= setting[:field_type].to_s.humanize %>
146
+ </span>
147
+ </td>
148
+
149
+ <td>
150
+ <div class="setting-status">
151
+ <% if setting[:current_value] != setting[:default_value] %>
152
+ <span>
153
+ <i class="fa fa-edit"></i> Modified
154
+ </span>
155
+ <% else %>
156
+ <span>
157
+ <i class="fa fa-check"></i> Default
158
+ </span>
159
+ <% end %>
160
+ </div>
161
+ <div class="setting-feedback mt-1">
162
+ <!-- AJAX feedback messages -->
163
+ </div>
164
+ </td>
165
+
166
+ <td>
167
+ <button type="button" class="btn btn-primary btn-sm btn-save" data-key="<%= setting[:key] %>" title="Save changes">
168
+ <i class="fa fa-save"></i>
169
+ </button>
170
+ </td>
171
+ </tr>
172
+ <% end %>
173
+ </tbody>
174
+ </table>
175
+ </div>
176
+ </div>
177
+ <% end %>
178
+
179
+ <!-- Empty state when no settings match search -->
180
+ <div class="card no-results" style="display: none;">
181
+ <div class="card-body text-center py-5">
182
+ <i class="fa fa-search fa-3x text-muted mb-3"></i>
183
+ <h4 class="text-muted">No settings found</h4>
184
+ <p class="text-muted">Try adjusting your search criteria.</p>
185
+ <button type="button" class="btn btn-primary" id="clear-search-results">
186
+ <i class="fa fa-refresh"></i> Clear Search
187
+ </button>
188
+ </div>
189
+ </div>
190
+
191
+ <% else %>
192
+ <div class="card">
193
+ <div class="card-body text-center py-5">
194
+ <i class="fa fa-cog fa-3x text-muted mb-3"></i>
195
+ <h4>No settings configured</h4>
196
+ <p class="text-muted">Make sure your Setting class has default values defined.</p>
197
+ <a href="<%= rails_admin.dashboard_path %>" class="btn btn-primary">
198
+ <i class="fa fa-arrow-left"></i> Back to Dashboard
199
+ </a>
200
+ </div>
201
+ </div>
202
+ <% end %>
203
+ </div>
204
+ </div>
205
+
206
+ <!-- Back to Dashboard Footer -->
207
+ <div class="row mt-4">
208
+ <div class="col-12">
209
+ <div class="text-center">
210
+ <a href="<%= rails_admin.dashboard_path %>" class="btn btn-secondary">
211
+ <i class="fa fa-arrow-left"></i> Back to Dashboard
212
+ </a>
213
+ </div>
214
+ </div>
215
+ </div>
216
+
217
+ <% content_for :before_body_end do %>
218
+ <style>
219
+ .settings-category .card-header {
220
+ background-color: #f8f9fa;
221
+ border-bottom: 1px solid #dee2e6;
222
+ }
223
+
224
+ .setting-input-group {
225
+ display: inline-block;
226
+ vertical-align: top;
227
+ }
228
+
229
+ .setting-value-container {
230
+ min-height: 60px;
231
+ }
232
+
233
+ .setting-info strong {
234
+ font-size: 0.95em;
235
+ }
236
+
237
+ .badge {
238
+ font-size: 0.75em;
239
+ font-weight: 500;
240
+ }
241
+
242
+ /* Ensure good contrast for all badges */
243
+ .badge-dark {
244
+ background-color: #343a40 !important;
245
+ color: #fff !important;
246
+ }
247
+
248
+ .badge-success {
249
+ background-color: #28a745 !important;
250
+ color: #fff !important;
251
+ }
252
+
253
+ .badge-primary {
254
+ background-color: #007bff !important;
255
+ color: #fff !important;
256
+ }
257
+
258
+ /* Fix table header text visibility */
259
+ .table thead th {
260
+ color: #495057 !important;
261
+ background-color: #f8f9fa !important;
262
+ border-bottom: 2px solid #dee2e6 !important;
263
+ font-weight: 600 !important;
264
+ }
265
+
266
+ /* Fix table cell text visibility */
267
+ .table td, .table tbody th {
268
+ color: #495057 !important;
269
+ }
270
+
271
+ .setting-feedback {
272
+ min-height: 20px;
273
+ }
274
+
275
+ .table td {
276
+ vertical-align: middle;
277
+ }
278
+
279
+ .custom-switch {
280
+ padding-left: 2.25rem;
281
+ }
282
+
283
+ .content-header h1 {
284
+ font-size: 1.8rem;
285
+ font-weight: 500;
286
+ }
287
+
288
+ @media (max-width: 768px) {
289
+ .table-responsive table {
290
+ font-size: 0.85em;
291
+ }
292
+
293
+ .setting-input-group input,
294
+ .setting-input-group textarea {
295
+ min-width: 200px !important;
296
+ }
297
+ }
298
+ </style>
299
+ <% end %>
300
+
301
+ <script>
302
+ document.addEventListener('DOMContentLoaded', function() {
303
+ // Search functionality
304
+ const searchInput = document.getElementById('settings-search');
305
+ const clearSearchButton = document.getElementById('clear-search');
306
+ const settingRows = document.querySelectorAll('.setting-row');
307
+ const visibleCountSpan = document.getElementById('visible-count');
308
+ const noResultsCard = document.querySelector('.no-results');
309
+ const settingsCategories = document.querySelectorAll('.settings-category');
310
+
311
+ // Search functionality
312
+ if (searchInput) {
313
+ searchInput.addEventListener('input', function() {
314
+ const searchTerm = this.value.toLowerCase();
315
+ let visibleCount = 0;
316
+ let hasVisibleCategories = false;
317
+
318
+ settingsCategories.forEach(function(category) {
319
+ let categoryHasVisible = false;
320
+ const rows = category.querySelectorAll('.setting-row');
321
+
322
+ rows.forEach(function(row) {
323
+ const searchable = row.dataset.searchable || '';
324
+ const isVisible = searchTerm === '' || searchable.includes(searchTerm);
325
+
326
+ row.style.display = isVisible ? '' : 'none';
327
+ if (isVisible) {
328
+ visibleCount++;
329
+ categoryHasVisible = true;
330
+ }
331
+ });
332
+
333
+ category.style.display = categoryHasVisible ? '' : 'none';
334
+ if (categoryHasVisible) hasVisibleCategories = true;
335
+ });
336
+
337
+ visibleCountSpan.textContent = visibleCount;
338
+ noResultsCard.style.display = hasVisibleCategories ? 'none' : 'block';
339
+
340
+ // Update clear button
341
+ clearSearchButton.style.display = searchTerm ? 'block' : 'none';
342
+ });
343
+ }
344
+
345
+ // Clear search
346
+ if (clearSearchButton) {
347
+ clearSearchButton.addEventListener('click', function() {
348
+ searchInput.value = '';
349
+ searchInput.dispatchEvent(new Event('input'));
350
+ });
351
+ }
352
+
353
+ // Clear search from no results page
354
+ document.getElementById('clear-search-results')?.addEventListener('click', function() {
355
+ searchInput.value = '';
356
+ searchInput.dispatchEvent(new Event('input'));
357
+ });
358
+
359
+ // Handle save buttons
360
+ document.querySelectorAll('.btn-save').forEach(function(button) {
361
+ button.addEventListener('click', function() {
362
+ const key = this.dataset.key;
363
+ const settingRow = document.querySelector(`.setting-row[data-key="${key}"]`);
364
+ const form = settingRow.querySelector('.setting-form');
365
+
366
+ if (form) {
367
+ // Create and dispatch form submit event
368
+ const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
369
+ form.dispatchEvent(submitEvent);
370
+ }
371
+ });
372
+ });
373
+
374
+ // Handle individual setting form submissions
375
+ document.querySelectorAll('.setting-form').forEach(function(form) {
376
+ form.addEventListener('submit', function(e) {
377
+ e.preventDefault();
378
+
379
+ var formData = new FormData(form);
380
+ var key = form.dataset.key;
381
+ var settingRow = form.closest('.setting-row');
382
+ var saveButton = settingRow.querySelector('.btn-save');
383
+ var feedbackDiv = settingRow.querySelector('.setting-feedback');
384
+
385
+ // Show loading state
386
+ saveButton.disabled = true;
387
+ saveButton.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
388
+ feedbackDiv.innerHTML = '';
389
+
390
+ fetch(form.action, {
391
+ method: 'POST',
392
+ body: formData,
393
+ headers: {
394
+ 'X-Requested-With': 'XMLHttpRequest',
395
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
396
+ }
397
+ })
398
+ .then(response => response.json())
399
+ .then(data => {
400
+ if (data.success) {
401
+ // Show success message
402
+ feedbackDiv.innerHTML = '<small class="text-success"><i class="fa fa-check"></i> Saved successfully!</small>';
403
+
404
+ // Clear success message after 3 seconds
405
+ setTimeout(function() {
406
+ feedbackDiv.innerHTML = '';
407
+ }, 3000);
408
+ } else {
409
+ // Show error message
410
+ feedbackDiv.innerHTML = '<small class="text-danger"><i class="fa fa-exclamation-triangle"></i> ' + (data.error || 'Failed to save') + '</small>';
411
+ }
412
+ })
413
+ .catch(error => {
414
+ console.error('Error:', error);
415
+ feedbackDiv.innerHTML = '<small class="text-danger"><i class="fa fa-exclamation-triangle"></i> Network error</small>';
416
+ })
417
+ .finally(function() {
418
+ // Restore button state
419
+ saveButton.disabled = false;
420
+ saveButton.innerHTML = '<i class="fa fa-save"></i>';
421
+ });
422
+ });
423
+ });
424
+
425
+ // Handle switch changes for boolean fields
426
+ document.querySelectorAll('input[type="checkbox"]').forEach(function(checkbox) {
427
+ checkbox.addEventListener('change', function() {
428
+ const label = document.querySelector(`label[for="${this.id}"]`);
429
+ if (label && label.classList.contains('custom-control-label')) {
430
+ label.textContent = this.checked ? 'Enabled' : 'Disabled';
431
+ }
432
+ });
433
+ });
434
+ });
435
+ </script>
@@ -0,0 +1,156 @@
1
+ # Testing the gem locally
2
+
3
+ ## 1. Create a test Rails app
4
+
5
+ ```bash
6
+ rails new test_app
7
+ cd test_app
8
+ ```
9
+
10
+ ## 2. Add gems to Gemfile
11
+
12
+ Add these gems to your `Gemfile`:
13
+
14
+ ```ruby
15
+ # Add these to your Gemfile
16
+ gem 'rails_admin'
17
+ gem 'rails-settings-cached'
18
+ gem 'rails_admin_settings_ui', path: '/Users/r3cha/rails_admin_settings_ui'
19
+
20
+ # For authentication (required by rails_admin)
21
+ gem 'devise'
22
+ ```
23
+
24
+ ## 3. Bundle install
25
+
26
+ ```bash
27
+ bundle install
28
+ ```
29
+
30
+ ## 4. Set up Devise (required for Rails Admin)
31
+
32
+ ```bash
33
+ rails generate devise:install
34
+ rails generate devise User
35
+ ```
36
+
37
+ ## 5. Set up Rails Admin
38
+
39
+ ```bash
40
+ rails generate rails_admin:install
41
+ ```
42
+
43
+ ## 6. Create Setting model
44
+
45
+ Create `app/models/setting.rb`:
46
+
47
+ ```ruby
48
+ class Setting < RailsSettings::Base
49
+ cache_prefix { "v1" }
50
+
51
+ # General settings
52
+ field :app_name, default: "My Test Application"
53
+ field :maintenance_mode, default: false
54
+ field :max_users, default: 1000
55
+ field :welcome_message, default: "Welcome to our application!"
56
+
57
+ # Email settings
58
+ field :mail_from, default: "noreply@example.com"
59
+ field :mail_host, default: "smtp.example.com"
60
+ field :mail_port, default: 587
61
+ field :mail_ssl, default: true
62
+
63
+ # API settings
64
+ field :api_key, default: ""
65
+ field :api_timeout, default: 30.0
66
+ field :api_endpoints, default: ["https://api.example.com", "https://backup-api.example.com"]
67
+
68
+ # Advanced settings
69
+ field :feature_flags, default: { "new_ui" => false, "beta_features" => false, "analytics" => true }
70
+ field :cache_ttl, default: 3600
71
+ field :debug_mode, default: false
72
+ end
73
+ ```
74
+
75
+ ## 7. Configure Rails Admin
76
+
77
+ Edit `config/initializers/rails_admin.rb`:
78
+
79
+ ```ruby
80
+ RailsAdmin.config do |config|
81
+ config.authenticate_with do
82
+ # Add your authentication logic here
83
+ # For testing, you can comment this out or use a simple check
84
+ end
85
+
86
+ config.current_user_method(&:current_user)
87
+
88
+ config.actions do
89
+ dashboard # mandatory
90
+ index # mandatory
91
+ new
92
+ export
93
+ bulk_delete
94
+ show
95
+ edit
96
+ delete
97
+ show_in_app
98
+
99
+ # Add the settings UI action
100
+ settings_ui
101
+ end
102
+
103
+ # Hide the default Setting model from navigation
104
+ config.model 'Setting' do
105
+ visible false
106
+ end
107
+ end
108
+ ```
109
+
110
+ ## 8. Run migrations
111
+
112
+ ```bash
113
+ rails db:create
114
+ rails db:migrate
115
+ ```
116
+
117
+ ## 9. Create a user (if using Devise)
118
+
119
+ ```bash
120
+ rails console
121
+ ```
122
+
123
+ In the console:
124
+ ```ruby
125
+ User.create!(email: 'admin@example.com', password: 'password', password_confirmation: 'password')
126
+ ```
127
+
128
+ ## 10. Start the server
129
+
130
+ ```bash
131
+ rails server
132
+ ```
133
+
134
+ ## 11. Test the Settings UI
135
+
136
+ 1. Go to `http://localhost:3000/admin`
137
+ 2. Sign in with your test user
138
+ 3. Click on "Settings" in the navigation
139
+ 4. You should see your settings organized in tabs with a user-friendly interface!
140
+
141
+ ## Testing different setting types
142
+
143
+ Try changing various settings to see the different field types in action:
144
+
145
+ - **Boolean**: `maintenance_mode`, `mail_ssl`, `debug_mode`
146
+ - **Integer**: `max_users`, `mail_port`, `cache_ttl`
147
+ - **Float**: `api_timeout`
148
+ - **String**: `app_name`, `api_key`, `mail_from`, `mail_host`
149
+ - **Text**: `welcome_message` (if you make it longer)
150
+ - **Array**: `api_endpoints`
151
+ - **JSON**: `feature_flags`
152
+
153
+ The interface will automatically categorize them into:
154
+ - **General**: `app_name`, `maintenance_mode`, `max_users`, etc.
155
+ - **Mail**: `mail_from`, `mail_host`, `mail_port`, `mail_ssl`
156
+ - **Api**: `api_key`, `api_timeout`, `api_endpoints`
@@ -0,0 +1,8 @@
1
+ en:
2
+ admin:
3
+ actions:
4
+ settings_ui:
5
+ title: "Application Settings"
6
+ menu: "Settings"
7
+ breadcrumb: "Settings"
8
+ description: "Manage application settings with an intuitive interface"
@@ -0,0 +1,27 @@
1
+ require "rails"
2
+ require_relative "settings_ui"
3
+ require_relative "helper"
4
+
5
+ module RailsAdminSettingsUi
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace RailsAdminSettingsUi
8
+
9
+ # Action registration is handled by the Railtie
10
+
11
+ # Load locale files
12
+ config.before_configuration do
13
+ I18n.load_path += Dir[File.join(File.dirname(__FILE__), '..', 'locales', '*.yml')]
14
+ end
15
+
16
+ # Include helpers in Rails Admin
17
+ initializer "rails_admin_settings_ui.helpers" do
18
+ ActiveSupport.on_load(:action_view) do
19
+ ActionView::Base.include RailsAdminSettingsUi::Helper
20
+ end
21
+ end
22
+
23
+ config.generators do |g|
24
+ g.test_framework :rspec
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ module RailsAdminSettingsUi
2
+ module Helper
3
+ def badge_color_for_type(field_type)
4
+ case field_type
5
+ when :boolean
6
+ 'success'
7
+ when :integer, :float
8
+ 'info'
9
+ when :text, :json
10
+ 'warning'
11
+ when :email, :url
12
+ 'primary'
13
+ when :array
14
+ 'dark'
15
+ else # :string
16
+ 'dark'
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ module RailsAdminSettingsUi
2
+ class Railtie < Rails::Railtie
3
+ railtie_name :rails_admin_settings_ui
4
+
5
+ initializer "rails_admin_settings_ui.register_action", before: :load_config_initializers do
6
+ # This will run before the Rails Admin config initializer
7
+ Rails.application.config.to_prepare do
8
+ if defined?(RailsAdmin) && defined?(RailsAdmin::Config::Actions)
9
+ RailsAdmin::Config::Actions.register(:settings_ui, RailsAdminSettingsUi::SettingsUi)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end