super_settings 2.5.0 → 2.6.1
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 +4 -4
- data/CHANGELOG.md +37 -0
- data/README.md +8 -7
- data/VERSION +1 -1
- data/app/helpers/super_settings/settings_helper.rb +1 -0
- data/lib/super_settings/application/helper.rb +9 -1
- data/lib/super_settings/application/layout.html.erb +56 -0
- data/lib/super_settings/application/layout_styles.css +37 -0
- data/lib/super_settings/application/layout_vars.css.erb +5 -1
- data/lib/super_settings/application/scripts.js +9 -4
- data/lib/super_settings/application/style_vars.css.erb +4 -2
- data/lib/super_settings/application.rb +10 -15
- data/lib/super_settings/coerce.rb +4 -7
- data/lib/super_settings/configuration.rb +36 -7
- data/lib/super_settings/controller_actions.rb +13 -3
- data/lib/super_settings/engine.rb +2 -2
- data/lib/super_settings/http_client.rb +23 -22
- data/lib/super_settings/local_cache.rb +8 -21
- data/lib/super_settings/mini_i18n.rb +9 -8
- data/lib/super_settings/rack_application.rb +52 -12
- data/lib/super_settings/rest_api.rb +9 -3
- data/lib/super_settings/setting.rb +35 -9
- data/lib/super_settings/storage/active_record_storage.rb +29 -14
- data/lib/super_settings/storage/json_storage.rb +49 -36
- data/lib/super_settings/storage/mongodb_storage.rb +11 -4
- data/lib/super_settings/storage/redis_storage.rb +1 -1
- data/lib/super_settings/storage/s3_storage.rb +25 -10
- data/lib/super_settings/storage/test_storage.rb +1 -0
- data/lib/super_settings/storage/transaction.rb +6 -4
- data/lib/super_settings.rb +1 -0
- data/super_settings.gemspec +0 -2
- metadata +2 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 290e32e0c4e0a4512fa7e4192a91fd9e654d1500a28bae270c8f21ec9c1a8478
|
|
4
|
+
data.tar.gz: 11ff70806b5849faae35e1d05f226af696f40a0f60334880fe0944d7770d48c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 912971873912df17a2ff9eb3c2462356161fb19ee7bb2c127264f12b59cfff2282ad1084eb62c3e62ec91afed8a49de22ad3fc0cb30a40d2982e230a951765dc
|
|
7
|
+
data.tar.gz: b843b3b01bbd4ab7e8a91328c20134379bb6f5a218d7bef0a34f0b3868aed9b7f41746b1cbb3949e76f3b22290fd44be7ec0e4a1af360057e7235bb2ac0f4fc7
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 2.6.1
|
|
8
|
+
|
|
9
|
+
### Security
|
|
10
|
+
|
|
11
|
+
- Fixed a stored cross-site scripting (XSS) vulnerability where a setting key was interpolated into the history pagination links in the web UI without HTML escaping. A user with write access could craft a key that executed JavaScript in another user's browser.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed `Setting.save!` raising a `NoMethodError` in non-Rails applications that relied on the implicit ActiveRecord storage default. The transaction now resolves the storage class through the public accessor instead of the uninitialized instance variable.
|
|
16
|
+
- Fixed `LocalCache#to_h` returning only the first element of `array` type settings. It now returns the full array value.
|
|
17
|
+
- Fixed `Setting#save!` always updating the `updated_at` timestamp (and triggering a write) even when nothing had changed, which caused unnecessary cache invalidation across processes.
|
|
18
|
+
- Fixed a race condition in `LocalCache` where a value read on a cache miss could overwrite a fresher value written by a concurrent refresh.
|
|
19
|
+
- Fixed `LocalCache#refresh` never picking up newly added settings if the cache had been loaded while the data store was empty.
|
|
20
|
+
- Fixed `LocalCache` returning mutable array values for settings added by a cache refresh or a cache miss. All cached values are now frozen so callers cannot mutate the shared cache.
|
|
21
|
+
- Fixed the web UI history view failing to render when a history record has no timestamp.
|
|
22
|
+
- Fixed a duplicate-key race in `ActiveRecordStorage#save!` that raised an unhandled `ActiveRecord::RecordNotUnique` when the same key was created concurrently. The conflict is now retried and merged.
|
|
23
|
+
- Fixed a bulk update against `HttpStorage` silently reporting success when the remote API rejected the changes. `bulk_update` now returns `false` and `save!` raises a `SuperSettings::Setting::PersistenceError` in this case so storage failures can be distinguished from validation errors. The storage failure message is reported in the `errors` payload so the web UI and API clients are told why the update failed.
|
|
24
|
+
- Fixed thread-safety issues in the cached S3 and MongoDB clients that could expose a stale client or permanently cache a `nil` client after a transient connection failure.
|
|
25
|
+
- Fixed `MongoDBStorage.find_by_key` returning records that reported `persisted?` as `false`.
|
|
26
|
+
- Fixed the escaping of `SuperSettings.authentication_url` when injected into the inline web UI JavaScript. URLs containing single quotes previously produced corrupted or invalid JavaScript.
|
|
27
|
+
- Fixed the Rack application returning 404 for all routes when mounted under a path (e.g. via `map` or Rails `mount`) without repeating the mount path in the constructor.
|
|
28
|
+
- Fixed `HttpClient` corrupting base URLs that include a query string when appending the trailing path separator.
|
|
29
|
+
- Fixed `HttpClient` retrying non-idempotent POST requests after a connection error, which could apply an update twice. The retry of a GET request now discards the failed connection instead of reusing it.
|
|
30
|
+
- Fixed the `/settings/updated_since` endpoint returning a 500 (or a misleading empty success) when the `time` parameter was missing or unparseable. It now returns a 400 Bad Request.
|
|
31
|
+
- Fixed the web UI POST endpoint returning a 500 for malformed JSON request bodies instead of a 400 Bad Request. The endpoint now also returns a 400 Bad Request when the `settings` parameter is missing or is not an array of hashes.
|
|
32
|
+
- Fixed an authenticated but unauthorized user being redirected to the login page (a potential redirect loop) instead of receiving a 403 Forbidden.
|
|
33
|
+
- Fixed the Rails layout helper using the raw dark mode selector instead of the resolved value, which could render a page with mismatched light/dark styling.
|
|
34
|
+
- Fixed `Coerce.boolean` returning `true` for a whitespace-only string.
|
|
35
|
+
- Fixed the Rails engine eagerly loading `ActiveJob::Base` during initialization.
|
|
36
|
+
- Added a missing `require "time"` so `Time.parse` based coercion works in non-Rails applications.
|
|
37
|
+
|
|
38
|
+
## 2.6.0
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Added `SuperSettings::Application#dark_mode_selector` configuration option that allows you to specify a CSS selector that sets dark mode when it matches an element in the page. This is an alternative to using the `color_scheme` option and allows for more flexible control of when dark mode is enabled.
|
|
43
|
+
|
|
7
44
|
## 2.5.0
|
|
8
45
|
|
|
9
46
|
### Added
|
data/README.md
CHANGED
|
@@ -308,6 +308,13 @@ SuperSettings.configure do |config|
|
|
|
308
308
|
# Set the color scheme to use for the Web UI. Options are :light (default), :dark, or :system.
|
|
309
309
|
config.controller.color_scheme = :dark
|
|
310
310
|
|
|
311
|
+
# Set a CSS selector for enabling dark mode. When the selector matches an element on the page,
|
|
312
|
+
# dark mode styles will be applied. A theme toggle button will also be rendered in the header
|
|
313
|
+
# allowing users to switch between light and dark modes (persisted via localStorage).
|
|
314
|
+
# This is an alternative to the color_scheme option. If neither color_scheme nor dark_mode_selector
|
|
315
|
+
# is set, it defaults to "[data-theme=dark]" which enables the toggle automatically.
|
|
316
|
+
config.controller.dark_mode_selector = "[data-theme=dark]"
|
|
317
|
+
|
|
311
318
|
# Add additional code to the controller. In this case we are adding code to ensure only
|
|
312
319
|
# admins can access the functionality and changing the layout to use one defined by the application.
|
|
313
320
|
config.controller.enhance do
|
|
@@ -461,15 +468,9 @@ This will work out of the box with the defaults for the storage engines when run
|
|
|
461
468
|
|
|
462
469
|
- `REDIS_URL` - `redis://localhost:6379/0`
|
|
463
470
|
- `REST_API_URL` - `http://localhost:3000/settings` (this is the default URL for the Rails application)
|
|
464
|
-
- `S3_URL` - `s3://accesskey:secretkey@region-1/settings/settings.json` (the S3 endpoint will be set to `http://localhost:
|
|
471
|
+
- `S3_URL` - `s3://accesskey:secretkey@region-1/settings/settings.json` (the S3 endpoint will be set to `http://localhost:24456`)
|
|
465
472
|
- `MONGODB_URL` - `mongodb://localhost:27017/super_settings`
|
|
466
473
|
|
|
467
|
-
Finally, you can run the application in dark mode by setting the `COLOR_SCHEME` environment variable.
|
|
468
|
-
|
|
469
|
-
```bash
|
|
470
|
-
COLOR_SCHEME=dark bundle exec rackup
|
|
471
|
-
```
|
|
472
|
-
|
|
473
474
|
## License
|
|
474
475
|
|
|
475
476
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.6.1
|
|
@@ -29,6 +29,7 @@ module SuperSettings
|
|
|
29
29
|
application_dir = File.expand_path(File.join("..", "..", "..", "lib", "super_settings", "application"), __dir__)
|
|
30
30
|
erb = ERB.new(File.read(File.join(application_dir, "layout_vars.css.erb")))
|
|
31
31
|
color_scheme = SuperSettings.configuration.controller.color_scheme
|
|
32
|
+
dark_mode_selector = SuperSettings.configuration.controller.resolved_dark_mode_selector
|
|
32
33
|
erb.result(binding).html_safe
|
|
33
34
|
end
|
|
34
35
|
end
|
|
@@ -56,7 +56,7 @@ module SuperSettings
|
|
|
56
56
|
window.__superSettingsI18n = #{translations_json};
|
|
57
57
|
#{File.read(File.join(__dir__, "scripts.js"))}
|
|
58
58
|
#{File.read(File.join(__dir__, "api.js"))}
|
|
59
|
-
#{"SuperSettingsAPI.authenticationUrl =
|
|
59
|
+
#{"SuperSettingsAPI.authenticationUrl = #{SuperSettings.authentication_url.to_s.to_json.gsub("</", "<\\/")};" if SuperSettings.authentication_url}
|
|
60
60
|
#{SuperSettings.web_ui_javascript}
|
|
61
61
|
</script>
|
|
62
62
|
HTML
|
|
@@ -222,6 +222,14 @@ module SuperSettings
|
|
|
222
222
|
@color_scheme if defined?(@color_scheme)
|
|
223
223
|
end
|
|
224
224
|
|
|
225
|
+
# A CSS selector that sets dark mode when it matches an element in the page.
|
|
226
|
+
# This is an alternative to using the color_scheme option.
|
|
227
|
+
#
|
|
228
|
+
# @return [String, nil]
|
|
229
|
+
def dark_mode_selector
|
|
230
|
+
@dark_mode_selector if defined?(@dark_mode_selector)
|
|
231
|
+
end
|
|
232
|
+
|
|
225
233
|
# Whether the application is in read-only mode.
|
|
226
234
|
#
|
|
227
235
|
# @return [Boolean]
|
|
@@ -8,6 +8,18 @@
|
|
|
8
8
|
<meta name="format-detection" content="telephone=no email=no date=no address=no">
|
|
9
9
|
<meta name="robots" content="noindex, nofollow">
|
|
10
10
|
<meta name="referrer" content="no-referrer-when-downgrade">
|
|
11
|
+
<% if dark_mode_selector %>
|
|
12
|
+
<script>
|
|
13
|
+
(function() {
|
|
14
|
+
try {
|
|
15
|
+
if (localStorage.getItem("super_settings_theme") === "dark") {
|
|
16
|
+
document.documentElement.setAttribute("data-theme", "dark");
|
|
17
|
+
}
|
|
18
|
+
} catch (e) {
|
|
19
|
+
}
|
|
20
|
+
})();
|
|
21
|
+
</script>
|
|
22
|
+
<% end %>
|
|
11
23
|
<%= layout_style_tag %>
|
|
12
24
|
<%= add_to_head %>
|
|
13
25
|
</head>
|
|
@@ -16,6 +28,24 @@
|
|
|
16
28
|
<header class="super-settings-page-header">
|
|
17
29
|
<div class="super-settings-page-header-inner">
|
|
18
30
|
<%= application_header %>
|
|
31
|
+
<% if dark_mode_selector %>
|
|
32
|
+
<button type="button" class="super-settings-theme-toggle" id="super-settings-theme-toggle" aria-label="Toggle dark mode">
|
|
33
|
+
<svg class="super-settings-theme-icon super-settings-theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
34
|
+
<circle cx="12" cy="12" r="4"></circle>
|
|
35
|
+
<path d="M12 2v2"></path>
|
|
36
|
+
<path d="M12 20v2"></path>
|
|
37
|
+
<path d="m4.93 4.93 1.41 1.41"></path>
|
|
38
|
+
<path d="m17.66 17.66 1.41 1.41"></path>
|
|
39
|
+
<path d="M2 12h2"></path>
|
|
40
|
+
<path d="M20 12h2"></path>
|
|
41
|
+
<path d="m6.34 17.66-1.41 1.41"></path>
|
|
42
|
+
<path d="m19.07 4.93-1.41 1.41"></path>
|
|
43
|
+
</svg>
|
|
44
|
+
<svg class="super-settings-theme-icon super-settings-theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
45
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
|
|
46
|
+
</svg>
|
|
47
|
+
</button>
|
|
48
|
+
<% end %>
|
|
19
49
|
</div>
|
|
20
50
|
</header>
|
|
21
51
|
|
|
@@ -72,5 +102,31 @@
|
|
|
72
102
|
</details>
|
|
73
103
|
</footer>
|
|
74
104
|
<% end %>
|
|
105
|
+
<% if dark_mode_selector %>
|
|
106
|
+
<script>
|
|
107
|
+
(function() {
|
|
108
|
+
var toggle = document.getElementById("super-settings-theme-toggle");
|
|
109
|
+
if (!toggle) return;
|
|
110
|
+
|
|
111
|
+
var html = document.documentElement;
|
|
112
|
+
toggle.addEventListener("click", function() {
|
|
113
|
+
var isDark = html.getAttribute("data-theme") === "dark";
|
|
114
|
+
if (isDark) {
|
|
115
|
+
html.removeAttribute("data-theme");
|
|
116
|
+
try {
|
|
117
|
+
localStorage.removeItem("super_settings_theme");
|
|
118
|
+
} catch (e) {
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
html.setAttribute("data-theme", "dark");
|
|
122
|
+
try {
|
|
123
|
+
localStorage.setItem("super_settings_theme", "dark");
|
|
124
|
+
} catch (e) {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
})();
|
|
129
|
+
</script>
|
|
130
|
+
<% end %>
|
|
75
131
|
</body>
|
|
76
132
|
</html>
|
|
@@ -112,6 +112,43 @@ a:visited {
|
|
|
112
112
|
justify-content: flex-end;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
.super-settings-theme-toggle {
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
justify-content: center;
|
|
119
|
+
width: 36px;
|
|
120
|
+
height: 36px;
|
|
121
|
+
border: 1px solid transparent;
|
|
122
|
+
border-radius: 8px;
|
|
123
|
+
background: transparent;
|
|
124
|
+
color: #9ca3af;
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
flex-shrink: 0;
|
|
127
|
+
transition: color 0.2s, border-color 0.2s;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.super-settings-theme-toggle:hover {
|
|
131
|
+
color: var(--link-color);
|
|
132
|
+
border-color: var(--link-color);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.super-settings-theme-icon {
|
|
136
|
+
width: 18px;
|
|
137
|
+
height: 18px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.super-settings-theme-icon-moon {
|
|
141
|
+
display: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
[data-theme="dark"] .super-settings-theme-icon-sun {
|
|
145
|
+
display: none;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
[data-theme="dark"] .super-settings-theme-icon-moon {
|
|
149
|
+
display: block;
|
|
150
|
+
}
|
|
151
|
+
|
|
115
152
|
/* ══════════════════════════════════════════
|
|
116
153
|
LANGUAGE MENU (in footer)
|
|
117
154
|
══════════════════════════════════════════ */
|
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
<% end %>
|
|
14
14
|
<% if color_scheme == :system || color_scheme == :dark %>
|
|
15
15
|
:root {
|
|
16
|
-
|
|
16
|
+
<% elsif dark_mode_selector %>
|
|
17
|
+
:root<%= dark_mode_selector %> {
|
|
18
|
+
<% end %>
|
|
19
|
+
<% if color_scheme == :system || color_scheme == :dark || dark_mode_selector %>
|
|
20
|
+
--text-color: #eef0f8;
|
|
17
21
|
--background-color: #1a1d23;
|
|
18
22
|
--header-background-color: #21252b;
|
|
19
23
|
--link-color: #2dd4bf;
|
|
@@ -413,8 +413,13 @@
|
|
|
413
413
|
const historyItems = parent.querySelector(".super-settings-history-items");
|
|
414
414
|
let itemsHTML = "";
|
|
415
415
|
payload.histories.forEach(function(history) {
|
|
416
|
-
|
|
417
|
-
|
|
416
|
+
let dateString = "";
|
|
417
|
+
if (history.created_at) {
|
|
418
|
+
const timestamp = Date.parse(history.created_at);
|
|
419
|
+
if (!isNaN(timestamp)) {
|
|
420
|
+
dateString = dateFormatter().format(new Date(timestamp));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
418
423
|
const value = (history.deleted ? '<em class="super-settings-text-danger">' + escapeHTML(t("history.deleted")) + '</em>' : escapeHTML(history.value));
|
|
419
424
|
itemsHTML += `<div class="super-settings-history-item">
|
|
420
425
|
<div class="super-settings-history-time">${escapeHTML(dateString)}</div>
|
|
@@ -427,10 +432,10 @@
|
|
|
427
432
|
if (payload.previous_page_params || payload.next_page_params) {
|
|
428
433
|
let paginationHTML = `<div class="super-settings-align-center">`;
|
|
429
434
|
if (payload.previous_page_params) {
|
|
430
|
-
paginationHTML += `<div style="float:left;"><a href="#" class="js-show-history" title="${escapeHTML(t("history.newer"))}" data-offset="${payload.previous_page_params.offset}" data-limit="${payload.previous_page_params.limit}" data-key="${payload.previous_page_params.key}"
|
|
435
|
+
paginationHTML += `<div style="float:left;"><a href="#" class="js-show-history" title="${escapeHTML(t("history.newer"))}" data-offset="${payload.previous_page_params.offset}" data-limit="${payload.previous_page_params.limit}" data-key="${escapeHTML(payload.previous_page_params.key)}">← ${escapeHTML(t("history.newer"))}</a></div>`;
|
|
431
436
|
}
|
|
432
437
|
if (payload.next_page_params) {
|
|
433
|
-
paginationHTML += `<div style="float:right;"><a href="#" class="js-show-history" title="${escapeHTML(t("history.older"))}" data-offset="${payload.next_page_params.offset}" data-limit="${payload.next_page_params.limit}" data-key="${payload.next_page_params.key}"
|
|
438
|
+
paginationHTML += `<div style="float:right;"><a href="#" class="js-show-history" title="${escapeHTML(t("history.older"))}" data-offset="${payload.next_page_params.offset}" data-limit="${payload.next_page_params.limit}" data-key="${escapeHTML(payload.next_page_params.key)}">${escapeHTML(t("history.older"))} →</a></div>`;
|
|
434
439
|
}
|
|
435
440
|
paginationHTML += '<div style="clear:both;"></div>';
|
|
436
441
|
parent.querySelector(".super-settings-history-container").insertAdjacentHTML("afterend", paginationHTML);
|
|
@@ -71,8 +71,10 @@
|
|
|
71
71
|
|
|
72
72
|
<% if color_scheme == :system %>
|
|
73
73
|
@media (prefers-color-scheme: dark) {
|
|
74
|
+
<% elsif dark_mode_selector %>
|
|
75
|
+
<%= dark_mode_selector %> {
|
|
74
76
|
<% end %>
|
|
75
|
-
<% if color_scheme == :system || color_scheme == :dark %>
|
|
77
|
+
<% if color_scheme == :system || color_scheme == :dark || dark_mode_selector %>
|
|
76
78
|
.super-settings {
|
|
77
79
|
/* Canvas */
|
|
78
80
|
--bg-page: #1a1d23;
|
|
@@ -128,6 +130,6 @@
|
|
|
128
130
|
--icon-disabled-color: #4b5563;
|
|
129
131
|
}
|
|
130
132
|
<% end %>
|
|
131
|
-
<% if color_scheme == :system %>
|
|
133
|
+
<% if color_scheme == :system || dark_mode_selector %>
|
|
132
134
|
}
|
|
133
135
|
<% end %>
|
|
@@ -8,14 +8,16 @@ module SuperSettings
|
|
|
8
8
|
include Helper
|
|
9
9
|
|
|
10
10
|
# @param layout [String, Symbol] path to an ERB template to use as the layout around the application UI. You can
|
|
11
|
-
#
|
|
11
|
+
# pass the symbol +:default+ to use the default layout that ships with the gem.
|
|
12
12
|
# @param add_to_head [String] HTML code to add to the <head> element on the page.
|
|
13
13
|
# @param api_base_url [String] the base URL for the REST API.
|
|
14
14
|
# @param color_scheme [Symbol] whether to use dark mode for the application UI. If +nil+, the user's system
|
|
15
|
-
#
|
|
15
|
+
# preference will be used.
|
|
16
|
+
# @param dark_mode_selector [String] a CSS selector that sets dark mode when it matches an element in the page.
|
|
17
|
+
# This is an alternative to using the color_scheme option.
|
|
16
18
|
# @param read_only [Boolean] whether to render the application in read-only mode (edit controls hidden).
|
|
17
19
|
# @param locale [String] the locale code for translations (default: "en").
|
|
18
|
-
def initialize(layout: nil, add_to_head: nil, api_base_url: nil, color_scheme: nil, read_only: false, locale: nil)
|
|
20
|
+
def initialize(layout: nil, add_to_head: nil, api_base_url: nil, color_scheme: nil, dark_mode_selector: nil, read_only: false, locale: nil)
|
|
19
21
|
if layout
|
|
20
22
|
layout = File.expand_path(File.join("application", "layout.html.erb"), __dir__) if layout == :default
|
|
21
23
|
@layout = ERB.new(File.read(layout)) if layout
|
|
@@ -27,6 +29,7 @@ module SuperSettings
|
|
|
27
29
|
|
|
28
30
|
@api_base_url = api_base_url
|
|
29
31
|
@color_scheme = color_scheme&.to_sym
|
|
32
|
+
@dark_mode_selector = dark_mode_selector
|
|
30
33
|
@read_only = !!read_only
|
|
31
34
|
@locale = locale || SuperSettings::MiniI18n::DEFAULT_LOCALE
|
|
32
35
|
end
|
|
@@ -42,20 +45,12 @@ module SuperSettings
|
|
|
42
45
|
html
|
|
43
46
|
end
|
|
44
47
|
|
|
45
|
-
# Render the edit form HTML for a single setting.
|
|
46
|
-
#
|
|
47
|
-
# @return [String] the rendered HTML
|
|
48
|
-
def render_edit
|
|
49
|
-
template = ERB.new(File.read(File.expand_path(File.join("application", "edit.html.erb"), __dir__)))
|
|
50
|
-
html = template.result(binding)
|
|
51
|
-
html = render_layout { html } if @layout
|
|
52
|
-
html = html.html_safe if html.respond_to?(:html_safe)
|
|
53
|
-
html
|
|
54
|
-
end
|
|
55
|
-
|
|
56
48
|
private
|
|
57
49
|
|
|
58
|
-
|
|
50
|
+
# The block is not called directly here; it is yielded to from the `<%= yield %>` call
|
|
51
|
+
# in the layout template which is evaluated with this method's binding. The block argument
|
|
52
|
+
# must be declared so that Ruby doesn't warn that the block may be ignored.
|
|
53
|
+
def render_layout(&block)
|
|
59
54
|
@layout&.result(binding)
|
|
60
55
|
end
|
|
61
56
|
end
|
|
@@ -21,13 +21,10 @@ module SuperSettings
|
|
|
21
21
|
# @param value [Object]
|
|
22
22
|
# @return [Boolean]
|
|
23
23
|
def boolean(value)
|
|
24
|
-
if value == false
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
else
|
|
29
|
-
!FALSE_VALUES.include?(value.to_s.downcase)
|
|
30
|
-
end
|
|
24
|
+
return false if value == false
|
|
25
|
+
return nil if blank?(value)
|
|
26
|
+
|
|
27
|
+
!FALSE_VALUES.include?(value.to_s.downcase)
|
|
31
28
|
end
|
|
32
29
|
|
|
33
30
|
# Cast a value to a Time object.
|
|
@@ -12,6 +12,8 @@ module SuperSettings
|
|
|
12
12
|
|
|
13
13
|
# Configuration for the controller.
|
|
14
14
|
class Controller
|
|
15
|
+
VALID_COLOR_SCHEMES = [:light, :dark, :system].freeze
|
|
16
|
+
|
|
15
17
|
# @api private
|
|
16
18
|
attr_reader :enhancement
|
|
17
19
|
|
|
@@ -25,12 +27,13 @@ module SuperSettings
|
|
|
25
27
|
def initialize
|
|
26
28
|
@superclass = nil
|
|
27
29
|
@web_ui_enabled = true
|
|
28
|
-
@color_scheme =
|
|
30
|
+
@color_scheme = nil
|
|
29
31
|
@changed_by_block = nil
|
|
30
32
|
@enhancement = nil
|
|
31
33
|
@application_name = nil
|
|
32
34
|
@application_logo = nil
|
|
33
35
|
@application_link = nil
|
|
36
|
+
@dark_mode_selector = nil
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
def superclass
|
|
@@ -41,10 +44,10 @@ module SuperSettings
|
|
|
41
44
|
end
|
|
42
45
|
end
|
|
43
46
|
|
|
44
|
-
#
|
|
47
|
+
# Optional name of the application displayed in the view.
|
|
45
48
|
attr_accessor :application_name
|
|
46
49
|
|
|
47
|
-
# Optional
|
|
50
|
+
# Optional image URL for the application logo.
|
|
48
51
|
attr_accessor :application_logo
|
|
49
52
|
|
|
50
53
|
# Optional URL for a link back to the rest of the application.
|
|
@@ -69,11 +72,30 @@ module SuperSettings
|
|
|
69
72
|
end
|
|
70
73
|
|
|
71
74
|
# Set dark mode for the web UI. Possible values are :light, :dark, or :system.
|
|
72
|
-
#
|
|
75
|
+
# When the value is not set (or is invalid), theme selection is left dynamic.
|
|
73
76
|
attr_writer :color_scheme
|
|
74
77
|
|
|
78
|
+
# Return the configured color scheme.
|
|
79
|
+
#
|
|
80
|
+
# @return [Symbol, nil]
|
|
75
81
|
def color_scheme
|
|
76
|
-
|
|
82
|
+
scheme = @color_scheme&.to_sym
|
|
83
|
+
VALID_COLOR_SCHEMES.include?(scheme) ? scheme : nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Set a CSS selector that sets dark mode. This is an alternative to using the color_scheme
|
|
87
|
+
# option and can be used if you have a custom implementation for dark mode in your application.
|
|
88
|
+
# Dark mode will be enabled in the settings web UI when the selector matches an element in the page.
|
|
89
|
+
attr_accessor :dark_mode_selector
|
|
90
|
+
|
|
91
|
+
# Return the selector used to enable dark mode from host page state.
|
|
92
|
+
#
|
|
93
|
+
# @return [String, nil]
|
|
94
|
+
def resolved_dark_mode_selector
|
|
95
|
+
return @dark_mode_selector if @dark_mode_selector
|
|
96
|
+
return "[data-theme=dark]" if color_scheme.nil?
|
|
97
|
+
|
|
98
|
+
nil
|
|
77
99
|
end
|
|
78
100
|
|
|
79
101
|
# Enhance the controller. You can define methods or call controller class methods like
|
|
@@ -184,21 +206,28 @@ module SuperSettings
|
|
|
184
206
|
@model = Model.new
|
|
185
207
|
@controller = Controller.new
|
|
186
208
|
@deferred_configs = []
|
|
209
|
+
@called = false
|
|
187
210
|
end
|
|
188
211
|
|
|
189
212
|
# Defer the execution of a block that will be yielded to with the config object. This
|
|
190
213
|
# is needed in a Rails environment during initialization so that all the frameworks can
|
|
191
|
-
# load before loading the settings.
|
|
214
|
+
# load before loading the settings. If the deferred configuration has already been run
|
|
215
|
+
# (i.e. the application has finished initializing), the block is called immediately.
|
|
192
216
|
#
|
|
193
217
|
# @api private
|
|
194
218
|
def defer(&block)
|
|
195
|
-
@
|
|
219
|
+
if @called
|
|
220
|
+
block.call(self)
|
|
221
|
+
else
|
|
222
|
+
@deferred_configs << block
|
|
223
|
+
end
|
|
196
224
|
end
|
|
197
225
|
|
|
198
226
|
# Call the block deferred during initialization.
|
|
199
227
|
#
|
|
200
228
|
# @api private
|
|
201
229
|
def call
|
|
230
|
+
@called = true
|
|
202
231
|
while (block = @deferred_configs.shift)
|
|
203
232
|
block&.call(self)
|
|
204
233
|
end
|
|
@@ -19,8 +19,13 @@ module SuperSettings
|
|
|
19
19
|
|
|
20
20
|
# Render the HTML application for managing settings.
|
|
21
21
|
def root
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
application = SuperSettings::Application.new(
|
|
23
|
+
read_only: super_settings_read_only?,
|
|
24
|
+
locale: I18n.locale,
|
|
25
|
+
color_scheme: SuperSettings.configuration.controller.color_scheme,
|
|
26
|
+
dark_mode_selector: SuperSettings.configuration.controller.resolved_dark_mode_selector
|
|
27
|
+
)
|
|
28
|
+
render html: application.render.html_safe, layout: true
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
# API endpoint for getting active settings. See SuperSettings::RestAPI for details.
|
|
@@ -70,7 +75,12 @@ module SuperSettings
|
|
|
70
75
|
|
|
71
76
|
# API endpoint for getting settings that have changed since specified time. See SuperSettings::RestAPI for details.
|
|
72
77
|
def updated_since
|
|
73
|
-
|
|
78
|
+
result = SuperSettings::RestAPI.updated_since(params[:time])
|
|
79
|
+
if result
|
|
80
|
+
render json: result
|
|
81
|
+
else
|
|
82
|
+
render json: {error: "Invalid time parameter"}, status: 400
|
|
83
|
+
end
|
|
74
84
|
end
|
|
75
85
|
|
|
76
86
|
# API endpoint for checking if the user is authorized to edit settings.
|
|
@@ -10,8 +10,8 @@ module SuperSettings
|
|
|
10
10
|
initializer("SuperSettings") do
|
|
11
11
|
Rails.configuration.middleware.unshift(SuperSettings::Context::RackMiddleware)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
ActiveSupport.on_load(:active_job) do
|
|
14
|
+
around_perform do |job, block|
|
|
15
15
|
SuperSettings.context(&block)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -27,8 +27,8 @@ module SuperSettings
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def initialize(base_url, headers: nil, params: nil, timeout: nil, user: nil, password: nil)
|
|
30
|
-
base_url = "#{base_url}/" unless base_url.end_with?("/")
|
|
31
30
|
@base_uri = URI(base_url)
|
|
31
|
+
@base_uri.path = "#{@base_uri.path}/" unless @base_uri.path.end_with?("/")
|
|
32
32
|
@base_uri.query = query_string(params) if params
|
|
33
33
|
@headers = headers ? DEFAULT_HEADERS.merge(headers) : DEFAULT_HEADERS
|
|
34
34
|
@timeout = timeout || DEFAULT_TIMEOUT
|
|
@@ -53,34 +53,35 @@ module SuperSettings
|
|
|
53
53
|
|
|
54
54
|
def send_request(request)
|
|
55
55
|
set_headers(request)
|
|
56
|
-
response_payload = nil
|
|
57
56
|
attempts = 0
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
58
|
+
begin
|
|
59
|
+
with_connection do |http|
|
|
60
|
+
http.start unless http.started?
|
|
61
|
+
response = http.request(request)
|
|
62
|
+
|
|
63
|
+
begin
|
|
64
|
+
response.value # raises exception unless response is a success
|
|
65
|
+
JSON.parse(response.body)
|
|
66
|
+
rescue Net::ProtocolError
|
|
67
|
+
if [404, 410].include?(response.code.to_i)
|
|
68
|
+
raise NotFoundError.new("#{response.code} #{response.message}")
|
|
69
|
+
elsif response.code.to_i == 422
|
|
70
|
+
raise InvalidRecordError.new("#{response.code} #{response.message}", errors: JSON.parse(response.body)["errors"])
|
|
71
|
+
else
|
|
72
|
+
raise Error.new("#{response.code} #{response.message}")
|
|
73
|
+
end
|
|
74
|
+
rescue JSON::JSONError => e
|
|
75
|
+
raise Error.new(e.message)
|
|
73
76
|
end
|
|
74
|
-
rescue JSON::JSONError => e
|
|
75
|
-
raise Error.new(e.message)
|
|
76
77
|
end
|
|
77
|
-
rescue IOError, Errno::ECONNRESET => connection_error
|
|
78
|
+
rescue IOError, Errno::ECONNRESET, Errno::EPIPE => connection_error
|
|
79
|
+
# Only retry idempotent requests; a POST may have already been processed by
|
|
80
|
+
# the server before the connection failed.
|
|
78
81
|
attempts += 1
|
|
79
|
-
retry if attempts <= 1
|
|
82
|
+
retry if attempts <= 1 && request.is_a?(Net::HTTP::Get)
|
|
80
83
|
raise connection_error
|
|
81
84
|
end
|
|
82
|
-
|
|
83
|
-
response_payload
|
|
84
85
|
end
|
|
85
86
|
|
|
86
87
|
def with_connection(&block)
|