ultra_settings 2.8.0 → 2.9.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 +4 -4
- data/CHANGELOG.md +24 -1
- data/MIT-LICENSE.txt +1 -1
- data/README.md +108 -1
- data/VERSION +1 -1
- data/app/AGENTS.md +7 -0
- data/app/_config_description.html.erb +22 -25
- data/app/_config_list.html.erb +2 -14
- data/app/_data_source.html.erb +53 -0
- data/app/application.css +1078 -259
- data/app/application.js +818 -91
- data/app/application_vars.css.erb +136 -81
- data/app/configuration.html.erb +60 -107
- data/app/index.html.erb +164 -20
- data/app/layout.css +81 -16
- data/app/layout.html.erb +67 -5
- data/app/layout_vars.css.erb +29 -5
- data/app/locales/ar.json +71 -0
- data/app/locales/cs.json +71 -0
- data/app/locales/da.json +71 -0
- data/app/locales/de.json +71 -0
- data/app/locales/el.json +71 -0
- data/app/locales/en.json +85 -0
- data/app/locales/es.json +71 -0
- data/app/locales/fa.json +71 -0
- data/app/locales/fr.json +71 -0
- data/app/locales/gd.json +71 -0
- data/app/locales/he.json +71 -0
- data/app/locales/hi.json +71 -0
- data/app/locales/it.json +71 -0
- data/app/locales/ja.json +71 -0
- data/app/locales/ko.json +71 -0
- data/app/locales/lt.json +71 -0
- data/app/locales/nb.json +71 -0
- data/app/locales/nl.json +71 -0
- data/app/locales/pl.json +71 -0
- data/app/locales/pt-br.json +71 -0
- data/app/locales/pt.json +71 -0
- data/app/locales/ru.json +71 -0
- data/app/locales/sv.json +71 -0
- data/app/locales/ta.json +71 -0
- data/app/locales/tr.json +71 -0
- data/app/locales/uk.json +71 -0
- data/app/locales/ur.json +71 -0
- data/app/locales/vi.json +71 -0
- data/app/locales/zh-cn.json +71 -0
- data/app/locales/zh-tw.json +71 -0
- data/lib/ultra_settings/application_view.rb +21 -3
- data/lib/ultra_settings/audit_data_sources.rb +98 -0
- data/lib/ultra_settings/coerce.rb +0 -6
- data/lib/ultra_settings/config_helper.rb +4 -4
- data/lib/ultra_settings/configuration.rb +28 -7
- data/lib/ultra_settings/configuration_view.rb +117 -56
- data/lib/ultra_settings/mini_i18n.rb +110 -0
- data/lib/ultra_settings/rack_app.rb +51 -1
- data/lib/ultra_settings/railtie.rb +8 -0
- data/lib/ultra_settings/tasks/audit_data_sources.rake +76 -0
- data/lib/ultra_settings/tasks/utils.rb +23 -0
- data/lib/ultra_settings/version.rb +1 -1
- data/lib/ultra_settings/web_view.rb +33 -2
- data/lib/ultra_settings.rb +56 -22
- data/ultra_settings.gemspec +3 -0
- metadata +38 -3
- data/app/_select_menu.html.erb +0 -53
data/app/application.js
CHANGED
|
@@ -1,124 +1,851 @@
|
|
|
1
1
|
document.addEventListener("DOMContentLoaded", () => {
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const searchInput = document.getElementById("config-search");
|
|
6
|
-
const items = document.querySelectorAll(".ultra-settings-dropdown-item");
|
|
7
|
-
const configurations = document.querySelectorAll(".ultra-settings-configuration");
|
|
8
|
-
const configList = document.querySelector(".ultra-settings-configuration-list");
|
|
9
|
-
|
|
10
|
-
// If no dropdown, we might be in single config mode or no configs.
|
|
11
|
-
if (!dropdown) {
|
|
12
|
-
// If there is exactly one configuration, show it.
|
|
13
|
-
if (configurations.length === 1) {
|
|
14
|
-
configurations[0].style.display = "block";
|
|
15
|
-
}
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
2
|
+
// ── i18n helper ──
|
|
3
|
+
const _i18n = window.__ultraSettingsI18n || {};
|
|
4
|
+
const t = (key) => _i18n[key] || key;
|
|
18
5
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
6
|
+
const root = document.querySelector(".ultra-settings");
|
|
7
|
+
const searchInput = document.getElementById("ultra-settings-search-input");
|
|
8
|
+
const searchClear = document.getElementById("ultra-settings-search-clear");
|
|
9
|
+
const configList = document.getElementById("ultra-settings-config-list");
|
|
10
|
+
const configDetail = document.getElementById("ultra-settings-config-detail");
|
|
11
|
+
const configListItems = document.querySelectorAll(".ultra-settings-config-list-item");
|
|
12
|
+
const sections = document.querySelectorAll(".ultra-settings-config-section");
|
|
13
|
+
const panelBg = document.getElementById("ultra-settings-panel-bg");
|
|
14
|
+
const detailPanel = document.getElementById("ultra-settings-detail-panel");
|
|
15
|
+
const dpTitle = document.getElementById("ultra-settings-dp-title");
|
|
16
|
+
const dpValue = document.getElementById("ultra-settings-dp-value");
|
|
17
|
+
const dpMeta = document.getElementById("ultra-settings-dp-meta");
|
|
18
|
+
const dpClose = document.getElementById("ultra-settings-dp-close");
|
|
19
|
+
const languageMenu = document.getElementById("ultra-settings-language-menu");
|
|
20
|
+
const languageOptions = document.querySelectorAll(".ultra-settings-language-option");
|
|
28
21
|
|
|
29
|
-
const
|
|
30
|
-
|
|
22
|
+
const closeLanguageMenu = () => {
|
|
23
|
+
if (languageMenu) languageMenu.removeAttribute("open");
|
|
31
24
|
};
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
const lowerQuery = query.toLowerCase();
|
|
35
|
-
items.forEach(item => {
|
|
36
|
-
const label = item.getAttribute("data-search").toLowerCase();
|
|
37
|
-
if (label.includes(lowerQuery)) {
|
|
38
|
-
item.style.display = "flex";
|
|
39
|
-
} else {
|
|
40
|
-
item.style.display = "none";
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
};
|
|
26
|
+
if (!root) return;
|
|
44
27
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
28
|
+
const singleConfig = root.dataset.singleConfig || null;
|
|
29
|
+
let selectedConfigId = null;
|
|
30
|
+
let initialLoad = true;
|
|
31
|
+
|
|
32
|
+
// ── View transition helper ──
|
|
33
|
+
let animating = false;
|
|
34
|
+
const animateView = (outEl, inEl, callback) => {
|
|
35
|
+
if (animating) return;
|
|
36
|
+
if (!outEl || !inEl) { if (callback) callback(); return; }
|
|
37
|
+
|
|
38
|
+
// Skip animations entirely when duration is forced to 0 (e.g. in tests)
|
|
39
|
+
const duration = parseFloat(getComputedStyle(outEl).animationDuration);
|
|
40
|
+
if (duration === 0) {
|
|
41
|
+
outEl.style.display = "none";
|
|
42
|
+
inEl.style.display = "";
|
|
43
|
+
if (callback) callback();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
animating = true;
|
|
48
|
+
// Ensure the incoming element is hidden until the exit animation finishes
|
|
49
|
+
inEl.style.display = "none";
|
|
50
|
+
outEl.classList.add("ultra-settings-view-exit");
|
|
51
|
+
outEl.addEventListener("animationend", function handler() {
|
|
52
|
+
outEl.removeEventListener("animationend", handler);
|
|
53
|
+
outEl.classList.remove("ultra-settings-view-exit");
|
|
54
|
+
outEl.style.display = "none";
|
|
55
|
+
inEl.style.display = "";
|
|
56
|
+
inEl.classList.add("ultra-settings-view-enter");
|
|
57
|
+
inEl.addEventListener("animationend", function handler2() {
|
|
58
|
+
inEl.removeEventListener("animationend", handler2);
|
|
59
|
+
inEl.classList.remove("ultra-settings-view-enter");
|
|
60
|
+
animating = false;
|
|
61
|
+
}, { once: true });
|
|
62
|
+
if (callback) callback();
|
|
63
|
+
}, { once: true });
|
|
51
64
|
};
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
// ── Config Selection ──
|
|
67
|
+
const selectConfig = (configId) => {
|
|
68
|
+
selectedConfigId = configId;
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
// Show only the selected section
|
|
71
|
+
sections.forEach(s => {
|
|
72
|
+
s.style.display = (s.dataset.configId === configId) ? "" : "none";
|
|
58
73
|
});
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
// Update search field
|
|
76
|
+
if (searchInput) {
|
|
77
|
+
const configName = configId.replace(/^section-/, "");
|
|
78
|
+
searchInput.value = configName;
|
|
79
|
+
searchInput.readOnly = true;
|
|
80
|
+
}
|
|
81
|
+
if (searchClear) searchClear.classList.add("visible");
|
|
82
|
+
|
|
83
|
+
// Update hash
|
|
84
|
+
const configName = configId.replace(/^section-/, "");
|
|
85
|
+
if (window.location.hash !== "#" + configName) {
|
|
86
|
+
history.replaceState(null, "", "#" + configName);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Scroll to top
|
|
90
|
+
window.scrollTo(0, 0);
|
|
91
|
+
|
|
92
|
+
// Animate list → detail
|
|
93
|
+
if (configList && configDetail && !initialLoad) {
|
|
94
|
+
if (configList.style.display === "none") {
|
|
95
|
+
// Already showing detail, just swap content
|
|
96
|
+
configDetail.classList.add("active");
|
|
64
97
|
} else {
|
|
65
|
-
|
|
98
|
+
configDetail.classList.add("active");
|
|
99
|
+
animateView(configList, configDetail);
|
|
66
100
|
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
101
|
+
} else {
|
|
102
|
+
if (configList) configList.style.display = "none";
|
|
103
|
+
if (configDetail) configDetail.classList.add("active");
|
|
104
|
+
}
|
|
70
105
|
};
|
|
71
106
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
e.stopPropagation();
|
|
75
|
-
toggleMenu();
|
|
76
|
-
});
|
|
107
|
+
const clearSelection = () => {
|
|
108
|
+
selectedConfigId = null;
|
|
77
109
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
110
|
+
// Hide all sections
|
|
111
|
+
sections.forEach(s => { s.style.display = "none"; });
|
|
112
|
+
|
|
113
|
+
// Reset search field
|
|
114
|
+
if (searchInput) {
|
|
115
|
+
searchInput.value = "";
|
|
116
|
+
searchInput.readOnly = false;
|
|
81
117
|
}
|
|
82
|
-
|
|
118
|
+
if (searchClear) searchClear.classList.remove("visible");
|
|
83
119
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
120
|
+
// Show all list items (clear any filter)
|
|
121
|
+
configListItems.forEach(item => item.classList.remove("hidden"));
|
|
87
122
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const configId = item.getAttribute("data-value");
|
|
91
|
-
const hash = configId.replace(/^config-/, "");
|
|
123
|
+
// Clear hash
|
|
124
|
+
history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
92
125
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
126
|
+
// Animate detail → list
|
|
127
|
+
if (configDetail && configList) {
|
|
128
|
+
animateView(configDetail, configList, () => {
|
|
129
|
+
// Clean up: let CSS classes control display again
|
|
130
|
+
configDetail.classList.remove("active");
|
|
131
|
+
configDetail.style.display = "";
|
|
132
|
+
configList.style.display = "";
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
if (configList) configList.style.display = "";
|
|
136
|
+
if (configDetail) configDetail.classList.remove("active");
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// ── Config List Item Handlers ──
|
|
141
|
+
configListItems.forEach(item => {
|
|
142
|
+
item.addEventListener("click", () => selectConfig(item.dataset.configId));
|
|
143
|
+
item.addEventListener("keydown", (e) => {
|
|
144
|
+
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); selectConfig(item.dataset.configId); }
|
|
100
145
|
});
|
|
101
146
|
});
|
|
102
147
|
|
|
103
|
-
//
|
|
104
|
-
|
|
148
|
+
// ── Search Filter (list view) ──
|
|
149
|
+
if (searchInput) {
|
|
150
|
+
searchInput.addEventListener("input", function() {
|
|
151
|
+
if (selectedConfigId) return; // Don't filter when a config is selected
|
|
152
|
+
|
|
153
|
+
const q = this.value.toLowerCase().trim();
|
|
154
|
+
configListItems.forEach(item => {
|
|
155
|
+
const searchData = item.dataset.search || "";
|
|
156
|
+
const match = !q || searchData.includes(q);
|
|
157
|
+
item.classList.toggle("hidden", !match);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Clear Button ──
|
|
163
|
+
if (searchClear) {
|
|
164
|
+
searchClear.addEventListener("click", clearSelection);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Hash-based Navigation ──
|
|
168
|
+
const handleHash = () => {
|
|
105
169
|
const hash = window.location.hash.replace(/^#/, "");
|
|
106
170
|
if (hash) {
|
|
107
|
-
const configId =
|
|
108
|
-
|
|
109
|
-
const exists = Array.from(items).some(item => item.getAttribute("data-value") === configId);
|
|
171
|
+
const configId = "section-" + hash;
|
|
172
|
+
const exists = Array.from(sections).some(s => s.dataset.configId === configId);
|
|
110
173
|
if (exists) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
174
|
+
selectConfig(configId);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// No valid hash — if not single config, show list
|
|
179
|
+
if (!singleConfig && selectedConfigId) {
|
|
180
|
+
clearSelection();
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
window.addEventListener("hashchange", handleHash);
|
|
185
|
+
|
|
186
|
+
// ── Single Config Auto-Select ──
|
|
187
|
+
if (singleConfig) {
|
|
188
|
+
selectConfig(singleConfig);
|
|
189
|
+
} else {
|
|
190
|
+
// Check for stored config (after SuperSettings save reload)
|
|
191
|
+
const storedConfig = sessionStorage.getItem("ultra-settings-selected-config");
|
|
192
|
+
if (storedConfig) {
|
|
193
|
+
sessionStorage.removeItem("ultra-settings-selected-config");
|
|
194
|
+
const exists = Array.from(sections).some(s => s.dataset.configId === storedConfig);
|
|
195
|
+
if (exists) {
|
|
196
|
+
selectConfig(storedConfig);
|
|
114
197
|
}
|
|
115
198
|
} else {
|
|
116
|
-
|
|
199
|
+
handleHash();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
initialLoad = false;
|
|
204
|
+
|
|
205
|
+
// ── Detail Panel ──
|
|
206
|
+
const openPanel = (name, value, type, isSecret) => {
|
|
207
|
+
if (dpTitle) dpTitle.textContent = name;
|
|
208
|
+
if (dpValue) dpValue.textContent = isSecret === "true" ? t("detail.secret_value") : value;
|
|
209
|
+
if (dpMeta) dpMeta.innerHTML = t("detail.type_label") + " <span>" + escapeHtml(type.toUpperCase()) + "</span>" + (isSecret === "true" ? ' \u00B7 <span style="color:var(--badge-secret-text)">' + t("detail.secret_badge") + "</span>" : "");
|
|
210
|
+
if (panelBg) panelBg.classList.add("open");
|
|
211
|
+
if (detailPanel) detailPanel.classList.add("open");
|
|
212
|
+
document.body.style.overflow = "hidden";
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const closePanel = () => {
|
|
216
|
+
if (panelBg) panelBg.classList.remove("open");
|
|
217
|
+
if (detailPanel) detailPanel.classList.remove("open");
|
|
218
|
+
document.body.style.overflow = "";
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (panelBg) panelBg.addEventListener("click", closePanel);
|
|
222
|
+
if (dpClose) dpClose.addEventListener("click", closePanel);
|
|
223
|
+
|
|
224
|
+
// Delegate click on field values to open panel
|
|
225
|
+
document.addEventListener("click", (e) => {
|
|
226
|
+
const target = e.target.closest(".ultra-settings-field-value");
|
|
227
|
+
if (target) {
|
|
228
|
+
// If we're inside the full app shell with panel, use panel
|
|
229
|
+
if (detailPanel) {
|
|
230
|
+
openPanel(
|
|
231
|
+
target.dataset.name || "",
|
|
232
|
+
target.dataset.value || "",
|
|
233
|
+
target.dataset.type || "",
|
|
234
|
+
target.dataset.secret || "false"
|
|
235
|
+
);
|
|
236
|
+
} else {
|
|
237
|
+
// Fallback: use dialog if available
|
|
238
|
+
const block = target.closest(".ultra-settings-block");
|
|
239
|
+
if (block) {
|
|
240
|
+
const dialog = block.querySelector(".ultra-settings-dialog");
|
|
241
|
+
if (dialog) {
|
|
242
|
+
const title = dialog.querySelector(".ultra-settings-dialog-title");
|
|
243
|
+
const value = dialog.querySelector(".ultra-settings-dialog-value");
|
|
244
|
+
if (title) title.textContent = target.dataset.name || "";
|
|
245
|
+
if (value) value.textContent = target.dataset.value || "";
|
|
246
|
+
dialog.showModal();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
117
250
|
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ── Keyboard Shortcuts ──
|
|
254
|
+
let ssSubmittingRef = () => false;
|
|
255
|
+
document.addEventListener("keydown", (e) => {
|
|
256
|
+
if (e.key === "Escape") {
|
|
257
|
+
if (ssSubmittingRef()) return;
|
|
258
|
+
closeLanguageMenu();
|
|
259
|
+
closePanel();
|
|
260
|
+
// Close SuperSettings edit panel if open
|
|
261
|
+
const ssBg = document.getElementById("ultra-settings-ss-panel-bg");
|
|
262
|
+
const ssP = document.getElementById("ultra-settings-ss-panel");
|
|
263
|
+
if (ssBg) ssBg.classList.remove("open");
|
|
264
|
+
if (ssP) ssP.classList.remove("open");
|
|
265
|
+
document.body.style.overflow = "";
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ── Equalize chip widths ──
|
|
270
|
+
const equalizeChipWidths = () => {
|
|
271
|
+
const chips = document.querySelectorAll(".ultra-settings-source-chip");
|
|
272
|
+
if (!chips.length) return;
|
|
273
|
+
chips.forEach(c => c.style.minWidth = "auto");
|
|
274
|
+
let max = 0;
|
|
275
|
+
chips.forEach(c => { max = Math.max(max, c.offsetWidth); });
|
|
276
|
+
const container = document.querySelector(".ultra-settings");
|
|
277
|
+
if (container) container.style.setProperty("--chip-width", max + "px");
|
|
278
|
+
chips.forEach(c => c.style.minWidth = "");
|
|
118
279
|
};
|
|
119
280
|
|
|
120
|
-
|
|
281
|
+
equalizeChipWidths();
|
|
282
|
+
|
|
283
|
+
// ── Restore selection & flash after setting save ──
|
|
284
|
+
const changedKey = sessionStorage.getItem("ultra-settings-changed-key");
|
|
285
|
+
if (changedKey) {
|
|
286
|
+
sessionStorage.removeItem("ultra-settings-changed-key");
|
|
287
|
+
const changedSection = sessionStorage.getItem("ultra-settings-changed-section");
|
|
288
|
+
sessionStorage.removeItem("ultra-settings-changed-section");
|
|
289
|
+
// Find the edit button with the matching key and highlight its field card
|
|
290
|
+
const scope = (changedSection && document.getElementById(changedSection)) || document;
|
|
291
|
+
const editBtn = scope.querySelector('.ultra-settings-ss-edit-btn[data-ss-key="' + CSS.escape(changedKey) + '"]');
|
|
292
|
+
if (editBtn) {
|
|
293
|
+
const card = editBtn.closest(".ultra-settings-field-card");
|
|
294
|
+
if (card) {
|
|
295
|
+
card.classList.add("ultra-settings-changed");
|
|
296
|
+
card.addEventListener("animationend", () => card.classList.remove("ultra-settings-changed"), { once: true });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ══════════════════════════════════════════
|
|
302
|
+
// SuperSettings Inline Editing
|
|
303
|
+
// ══════════════════════════════════════════
|
|
304
|
+
const ssApiPath = root ? root.dataset.ssApiPath : null;
|
|
305
|
+
if (ssApiPath) {
|
|
306
|
+
// Load the SuperSettings api.js client library and check access
|
|
307
|
+
const apiBaseEl = document.createElement("div");
|
|
308
|
+
apiBaseEl.className = "super-settings";
|
|
309
|
+
apiBaseEl.dataset.apiBaseUrl = ssApiPath;
|
|
310
|
+
apiBaseEl.style.display = "none";
|
|
311
|
+
document.body.appendChild(apiBaseEl);
|
|
312
|
+
|
|
313
|
+
const script = document.createElement("script");
|
|
314
|
+
script.src = ssApiPath + "/api.js";
|
|
315
|
+
script.onload = () => {
|
|
316
|
+
if (!window.SuperSettingsAPI || !SuperSettingsAPI.authorized) {
|
|
317
|
+
console.log("SuperSettings: api.js loaded but SuperSettingsAPI not available");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
SuperSettingsAPI.baseUrl = ssApiPath;
|
|
322
|
+
SuperSettingsAPI.authorized(
|
|
323
|
+
(permission) => {
|
|
324
|
+
if (permission !== "read-write") {
|
|
325
|
+
console.log("SuperSettings: authorization is '" + permission + "', editing disabled (requires read-write)");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
enableSuperSettingsUI();
|
|
329
|
+
},
|
|
330
|
+
(err) => {
|
|
331
|
+
console.log("SuperSettings: access check failed, editing disabled", err);
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
};
|
|
335
|
+
script.onerror = () => {
|
|
336
|
+
console.log("SuperSettings: failed to load api.js, editing disabled");
|
|
337
|
+
};
|
|
338
|
+
document.head.appendChild(script);
|
|
339
|
+
|
|
340
|
+
// Enable the editing UI after api.js is loaded and access is confirmed
|
|
341
|
+
const enableSuperSettingsUI = () => {
|
|
342
|
+
const ssPanel = document.getElementById("ultra-settings-ss-panel");
|
|
343
|
+
const ssPanelBg = document.getElementById("ultra-settings-ss-panel-bg");
|
|
344
|
+
const ssForm = document.getElementById("ultra-settings-ss-form");
|
|
345
|
+
const ssLoading = document.getElementById("ultra-settings-ss-loading");
|
|
346
|
+
const ssErrors = document.getElementById("ultra-settings-ss-errors");
|
|
347
|
+
const ssKeyInput = document.getElementById("ultra-settings-ss-key");
|
|
348
|
+
const ssTitle = document.getElementById("ultra-settings-ss-title");
|
|
349
|
+
const ssValueTypeSelect = document.getElementById("ultra-settings-ss-value-type");
|
|
350
|
+
const ssValueTextarea = document.getElementById("ultra-settings-ss-value");
|
|
351
|
+
const ssValueField = document.getElementById("ultra-settings-ss-value-field");
|
|
352
|
+
const ssIntegerField = document.getElementById("ultra-settings-ss-integer-field");
|
|
353
|
+
const ssIntegerInput = document.getElementById("ultra-settings-ss-integer-value");
|
|
354
|
+
const ssFloatField = document.getElementById("ultra-settings-ss-float-field");
|
|
355
|
+
const ssFloatInput = document.getElementById("ultra-settings-ss-float-value");
|
|
356
|
+
const ssBooleanField = document.getElementById("ultra-settings-ss-boolean-field");
|
|
357
|
+
const ssBooleanCheckbox = document.getElementById("ultra-settings-ss-boolean-value");
|
|
358
|
+
const ssDatetimeField = document.getElementById("ultra-settings-ss-datetime-field");
|
|
359
|
+
const ssDatetimeInput = document.getElementById("ultra-settings-ss-datetime-value");
|
|
360
|
+
const ssTzLabel = document.getElementById("ultra-settings-ss-tz-label");
|
|
361
|
+
const ssDescriptionInput = document.getElementById("ultra-settings-ss-description");
|
|
362
|
+
const ssSaveBtn = document.getElementById("ultra-settings-ss-save");
|
|
363
|
+
const ssCancelBtn = document.getElementById("ultra-settings-ss-cancel");
|
|
364
|
+
const ssCloseBtn = document.getElementById("ultra-settings-ss-panel-close");
|
|
365
|
+
const ssExternalLink = document.getElementById("ultra-settings-ss-external-link");
|
|
366
|
+
const ssRuntimeUrlTemplate = root.dataset.runtimeSettingsUrl || "";
|
|
367
|
+
|
|
368
|
+
// Show all edit buttons (they are rendered hidden by default)
|
|
369
|
+
document.querySelectorAll(".ultra-settings-ss-edit-btn").forEach(btn => {
|
|
370
|
+
btn.style.display = "";
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Get the local timezone name for display
|
|
374
|
+
const localTz = (() => {
|
|
375
|
+
try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch(e) { return "UTC"; }
|
|
376
|
+
})();
|
|
377
|
+
if (ssTzLabel) ssTzLabel.textContent = localTz;
|
|
378
|
+
|
|
379
|
+
// Show/hide value fields based on type
|
|
380
|
+
const updateValueField = (type) => {
|
|
381
|
+
ssValueField.style.display = "none";
|
|
382
|
+
ssIntegerField.style.display = "none";
|
|
383
|
+
ssFloatField.style.display = "none";
|
|
384
|
+
ssBooleanField.style.display = "none";
|
|
385
|
+
ssDatetimeField.style.display = "none";
|
|
386
|
+
|
|
387
|
+
if (type === "boolean") {
|
|
388
|
+
ssBooleanField.style.display = "";
|
|
389
|
+
} else if (type === "integer") {
|
|
390
|
+
ssIntegerField.style.display = "";
|
|
391
|
+
} else if (type === "float") {
|
|
392
|
+
ssFloatField.style.display = "";
|
|
393
|
+
} else if (type === "datetime") {
|
|
394
|
+
ssDatetimeField.style.display = "";
|
|
395
|
+
} else if (type === "array") {
|
|
396
|
+
ssValueField.style.display = "";
|
|
397
|
+
ssValueTextarea.rows = 6;
|
|
398
|
+
ssValueTextarea.placeholder = t("edit.placeholder_array");
|
|
399
|
+
} else {
|
|
400
|
+
ssValueField.style.display = "";
|
|
401
|
+
ssValueTextarea.rows = 3;
|
|
402
|
+
ssValueTextarea.placeholder = "";
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
ssValueTypeSelect.addEventListener("change", () => {
|
|
407
|
+
updateValueField(ssValueTypeSelect.value);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Enforce integer-only input: strip non-integer characters as the user types
|
|
411
|
+
if (ssIntegerInput) {
|
|
412
|
+
ssIntegerInput.addEventListener("input", () => {
|
|
413
|
+
const raw = ssIntegerInput.value;
|
|
414
|
+
// Allow empty, sole minus sign while typing, or valid integer
|
|
415
|
+
if (raw === "" || raw === "-") return;
|
|
416
|
+
const parsed = parseInt(raw, 10);
|
|
417
|
+
if (isNaN(parsed)) {
|
|
418
|
+
ssIntegerInput.value = "";
|
|
419
|
+
} else if (String(parsed) !== raw) {
|
|
420
|
+
ssIntegerInput.value = parsed;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Convert the datetime-local input value (local time) to a UTC ISO 8601 string
|
|
426
|
+
const datetimeToISO = () => {
|
|
427
|
+
const localVal = ssDatetimeInput.value; // e.g. "2025-01-15T10:30:00"
|
|
428
|
+
if (!localVal) return "";
|
|
429
|
+
const d = new Date(localVal);
|
|
430
|
+
if (isNaN(d.getTime())) return localVal;
|
|
431
|
+
return d.toISOString();
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// Parse a UTC ISO 8601 string and populate the datetime-local input in local time
|
|
435
|
+
const populateDatetime = (isoStr) => {
|
|
436
|
+
if (!isoStr) {
|
|
437
|
+
ssDatetimeInput.value = "";
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
let str = String(isoStr).trim();
|
|
441
|
+
// Normalize Ruby Time#to_json format ("2026-03-20 01:07:56 UTC") to ISO 8601
|
|
442
|
+
str = str.replace(/ UTC$/, "Z").replace(/ /, "T");
|
|
443
|
+
// Ensure the string is treated as UTC if no timezone indicator present
|
|
444
|
+
if (!str.endsWith("Z") && !/[+-]\d{2}:?\d{2}$/.test(str)) {
|
|
445
|
+
str += "Z";
|
|
446
|
+
}
|
|
447
|
+
const d = new Date(str);
|
|
448
|
+
if (isNaN(d.getTime())) {
|
|
449
|
+
ssDatetimeInput.value = "";
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// Format as local datetime-local value: YYYY-MM-DDTHH:MM:SS
|
|
453
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
454
|
+
ssDatetimeInput.value = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) +
|
|
455
|
+
"T" + pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds());
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const getFormValue = () => {
|
|
459
|
+
if (ssValueTypeSelect.value === "boolean") {
|
|
460
|
+
return ssBooleanCheckbox.checked ? "true" : "false";
|
|
461
|
+
}
|
|
462
|
+
if (ssValueTypeSelect.value === "integer") {
|
|
463
|
+
return ssIntegerInput.value;
|
|
464
|
+
}
|
|
465
|
+
if (ssValueTypeSelect.value === "float") {
|
|
466
|
+
return ssFloatInput.value;
|
|
467
|
+
}
|
|
468
|
+
if (ssValueTypeSelect.value === "datetime") {
|
|
469
|
+
return datetimeToISO();
|
|
470
|
+
}
|
|
471
|
+
return ssValueTextarea.value;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
// Fetch a setting using the SuperSettings API client
|
|
475
|
+
const fetchSetting = (key, callback) => {
|
|
476
|
+
SuperSettingsAPI.fetchSetting(key, callback, (status) => {
|
|
477
|
+
if (status === 404) {
|
|
478
|
+
callback(null);
|
|
479
|
+
} else {
|
|
480
|
+
console.error("SuperSettings: error fetching setting, status " + status);
|
|
481
|
+
callback(null);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// Save a setting via the SuperSettings API
|
|
487
|
+
const saveSetting = (params, callback) => {
|
|
488
|
+
SuperSettingsAPI.updateSettings(
|
|
489
|
+
{settings: [params]},
|
|
490
|
+
(data) => {
|
|
491
|
+
callback({status: 200, ok: true, data: data});
|
|
492
|
+
},
|
|
493
|
+
(error) => {
|
|
494
|
+
console.error("SuperSettings: error saving setting", error);
|
|
495
|
+
callback({status: 0, ok: false, data: {success: false, errors: {_http: [t("edit.network_error")]}}});
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// Open the edit panel
|
|
501
|
+
const openSsPanel = (key, defaultType, defaultDescription) => {
|
|
502
|
+
if (!ssPanel) return;
|
|
503
|
+
|
|
504
|
+
// Close the detail panel if open
|
|
505
|
+
closePanel();
|
|
506
|
+
|
|
507
|
+
// Reset form
|
|
508
|
+
ssKeyInput.value = key;
|
|
509
|
+
if (ssTitle) ssTitle.textContent = key;
|
|
510
|
+
ssValueTextarea.value = "";
|
|
511
|
+
ssIntegerInput.value = "";
|
|
512
|
+
ssFloatInput.value = "";
|
|
513
|
+
ssDatetimeInput.value = "";
|
|
514
|
+
ssBooleanCheckbox.checked = false;
|
|
515
|
+
ssDescriptionInput.value = defaultDescription || "";
|
|
516
|
+
ssValueTypeSelect.value = defaultType || "string";
|
|
517
|
+
updateValueField(ssValueTypeSelect.value);
|
|
518
|
+
ssErrors.style.display = "none";
|
|
519
|
+
ssErrors.textContent = "";
|
|
520
|
+
ssForm.style.display = "none";
|
|
521
|
+
ssLoading.style.display = "";
|
|
522
|
+
ssSaveBtn.disabled = false;
|
|
523
|
+
ssSaveBtn.textContent = t("edit.save");
|
|
524
|
+
|
|
525
|
+
// Build and show external link if runtime_settings_url is configured
|
|
526
|
+
if (ssExternalLink && ssRuntimeUrlTemplate) {
|
|
527
|
+
const externalUrl = ssRuntimeUrlTemplate
|
|
528
|
+
.replace("${name}", encodeURIComponent(key))
|
|
529
|
+
.replace("${type}", encodeURIComponent(defaultType || ""))
|
|
530
|
+
.replace("${description}", encodeURIComponent(defaultDescription || ""));
|
|
531
|
+
ssExternalLink.href = externalUrl;
|
|
532
|
+
ssExternalLink.style.display = "";
|
|
533
|
+
} else if (ssExternalLink) {
|
|
534
|
+
ssExternalLink.style.display = "none";
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Reset history view (always start on edit form)
|
|
538
|
+
if (ssHistoryContainer) ssHistoryContainer.style.display = "none";
|
|
539
|
+
|
|
540
|
+
ssPanelBg.classList.add("open");
|
|
541
|
+
ssPanel.classList.add("open");
|
|
542
|
+
document.body.style.overflow = "hidden";
|
|
543
|
+
|
|
544
|
+
// Fetch existing setting
|
|
545
|
+
fetchSetting(key, (setting) => {
|
|
546
|
+
if (setting && !setting.error) {
|
|
547
|
+
// Existing setting — populate form with current values
|
|
548
|
+
ssValueTypeSelect.value = setting.value_type || defaultType || "string";
|
|
549
|
+
updateValueField(ssValueTypeSelect.value);
|
|
550
|
+
|
|
551
|
+
if (setting.value_type === "boolean") {
|
|
552
|
+
ssBooleanCheckbox.checked = (setting.value === true || setting.value === "true");
|
|
553
|
+
} else if (setting.value_type === "integer") {
|
|
554
|
+
ssIntegerInput.value = (setting.value != null) ? String(setting.value) : "";
|
|
555
|
+
} else if (setting.value_type === "float") {
|
|
556
|
+
ssFloatInput.value = (setting.value != null) ? String(setting.value) : "";
|
|
557
|
+
} else if (setting.value_type === "datetime") {
|
|
558
|
+
populateDatetime((setting.value != null) ? String(setting.value) : "");
|
|
559
|
+
} else if (setting.value_type === "array" && Array.isArray(setting.value)) {
|
|
560
|
+
ssValueTextarea.value = setting.value.join("\n");
|
|
561
|
+
} else {
|
|
562
|
+
ssValueTextarea.value = (setting.value != null) ? String(setting.value) : "";
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (setting.description) {
|
|
566
|
+
ssDescriptionInput.value = setting.description;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// If not found, defaults already applied
|
|
570
|
+
|
|
571
|
+
ssLoading.style.display = "none";
|
|
572
|
+
ssForm.style.display = "";
|
|
573
|
+
});
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
let ssSubmitting = false;
|
|
577
|
+
ssSubmittingRef = () => ssSubmitting;
|
|
578
|
+
|
|
579
|
+
const closeSsPanel = () => {
|
|
580
|
+
if (ssSubmitting) return;
|
|
581
|
+
if (ssPanelBg) ssPanelBg.classList.remove("open");
|
|
582
|
+
if (ssPanel) ssPanel.classList.remove("open");
|
|
583
|
+
document.body.style.overflow = "";
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// Handle save
|
|
587
|
+
ssSaveBtn.addEventListener("click", () => {
|
|
588
|
+
const params = {
|
|
589
|
+
key: ssKeyInput.value,
|
|
590
|
+
value: getFormValue(),
|
|
591
|
+
value_type: ssValueTypeSelect.value,
|
|
592
|
+
description: ssDescriptionInput.value
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
ssSubmitting = true;
|
|
596
|
+
ssSaveBtn.disabled = true;
|
|
597
|
+
ssCancelBtn.disabled = true;
|
|
598
|
+
if (ssCloseBtn) ssCloseBtn.style.display = "none";
|
|
599
|
+
ssSaveBtn.innerHTML = '<svg class="ultra-settings-spinner" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-dasharray="31.4 31.4" /></svg>';
|
|
600
|
+
ssErrors.style.display = "none";
|
|
601
|
+
|
|
602
|
+
saveSetting(params, (result) => {
|
|
603
|
+
if (result.data && result.data.success) {
|
|
604
|
+
// Store selected config so we can restore it after reload
|
|
605
|
+
if (selectedConfigId) {
|
|
606
|
+
sessionStorage.setItem("ultra-settings-selected-config", selectedConfigId);
|
|
607
|
+
}
|
|
608
|
+
sessionStorage.setItem("ultra-settings-changed-key", params.key);
|
|
609
|
+
const activeSection = document.querySelector(".ultra-settings-config-section[style='']");
|
|
610
|
+
if (activeSection) {
|
|
611
|
+
sessionStorage.setItem("ultra-settings-changed-section", activeSection.id);
|
|
612
|
+
}
|
|
613
|
+
window.location.reload();
|
|
614
|
+
} else {
|
|
615
|
+
ssSubmitting = false;
|
|
616
|
+
ssSaveBtn.disabled = false;
|
|
617
|
+
ssCancelBtn.disabled = false;
|
|
618
|
+
if (ssCloseBtn) ssCloseBtn.style.display = "";
|
|
619
|
+
ssSaveBtn.textContent = t("edit.save");
|
|
620
|
+
|
|
621
|
+
// Collect all error messages from the response
|
|
622
|
+
const msgs = [];
|
|
623
|
+
if (result.data && result.data.errors) {
|
|
624
|
+
Object.entries(result.data.errors).forEach(([key, errs]) => {
|
|
625
|
+
if (Array.isArray(errs)) {
|
|
626
|
+
errs.forEach(e => msgs.push(String(e)));
|
|
627
|
+
} else {
|
|
628
|
+
msgs.push(String(errs));
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
} else if (result.data && result.data.error) {
|
|
632
|
+
msgs.push(String(result.data.error));
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Build the alert content
|
|
636
|
+
ssErrors.innerHTML = "";
|
|
637
|
+
if (msgs.length === 0) {
|
|
638
|
+
msgs.push(t("edit.save_error"));
|
|
639
|
+
}
|
|
640
|
+
if (msgs.length === 1) {
|
|
641
|
+
ssErrors.textContent = msgs[0];
|
|
642
|
+
} else {
|
|
643
|
+
const ul = document.createElement("ul");
|
|
644
|
+
ul.className = "ultra-settings-ss-error-list";
|
|
645
|
+
msgs.forEach(msg => {
|
|
646
|
+
const li = document.createElement("li");
|
|
647
|
+
li.textContent = msg;
|
|
648
|
+
ul.appendChild(li);
|
|
649
|
+
});
|
|
650
|
+
ssErrors.appendChild(ul);
|
|
651
|
+
}
|
|
652
|
+
ssErrors.style.display = "";
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// Handle cancel / close
|
|
658
|
+
ssCancelBtn.addEventListener("click", closeSsPanel);
|
|
659
|
+
ssCloseBtn.addEventListener("click", closeSsPanel);
|
|
660
|
+
if (ssPanelBg) ssPanelBg.addEventListener("click", closeSsPanel);
|
|
121
661
|
|
|
122
|
-
|
|
123
|
-
|
|
662
|
+
// ── History view ──
|
|
663
|
+
const ssHistoryLink = document.getElementById("ultra-settings-ss-history-link");
|
|
664
|
+
const ssHistoryContainer = document.getElementById("ultra-settings-ss-history");
|
|
665
|
+
const ssHistoryEntries = document.getElementById("ultra-settings-ss-history-entries");
|
|
666
|
+
const ssHistoryPagination = document.getElementById("ultra-settings-ss-history-pagination");
|
|
667
|
+
const ssHistoryPrevBtn = document.getElementById("ultra-settings-ss-history-prev");
|
|
668
|
+
const ssHistoryNextBtn = document.getElementById("ultra-settings-ss-history-next");
|
|
669
|
+
const ssHistoryBackLink = document.getElementById("ultra-settings-ss-history-back");
|
|
670
|
+
let ssHistoryCurrentKey = null;
|
|
671
|
+
|
|
672
|
+
const formatHistoryTime = (timestamp) => {
|
|
673
|
+
if (!timestamp) return "";
|
|
674
|
+
let str = String(timestamp).trim();
|
|
675
|
+
str = str.replace(/ UTC$/, "Z").replace(/ /, "T");
|
|
676
|
+
if (!str.endsWith("Z") && !/[+-]\d{2}:?\d{2}$/.test(str)) {
|
|
677
|
+
str += "Z";
|
|
678
|
+
}
|
|
679
|
+
const d = new Date(str);
|
|
680
|
+
if (isNaN(d.getTime())) return String(timestamp);
|
|
681
|
+
return d.toLocaleString();
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const renderHistoryEntries = (entries) => {
|
|
685
|
+
ssHistoryEntries.innerHTML = "";
|
|
686
|
+
if (!entries || entries.length === 0) {
|
|
687
|
+
const empty = document.createElement("div");
|
|
688
|
+
empty.className = "ultra-settings-ss-history-empty";
|
|
689
|
+
empty.textContent = t("edit.history_empty");
|
|
690
|
+
ssHistoryEntries.appendChild(empty);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
entries.forEach((entry) => {
|
|
694
|
+
const div = document.createElement("div");
|
|
695
|
+
div.className = "ultra-settings-ss-history-entry";
|
|
696
|
+
|
|
697
|
+
const header = document.createElement("div");
|
|
698
|
+
header.className = "ultra-settings-ss-history-entry-header";
|
|
699
|
+
|
|
700
|
+
const timeSpan = document.createElement("span");
|
|
701
|
+
timeSpan.className = "ultra-settings-ss-history-entry-time";
|
|
702
|
+
timeSpan.textContent = formatHistoryTime(entry.created_at);
|
|
703
|
+
header.appendChild(timeSpan);
|
|
704
|
+
|
|
705
|
+
if (entry.changed_by) {
|
|
706
|
+
const bySpan = document.createElement("span");
|
|
707
|
+
bySpan.textContent = t("edit.history_by") + " " + entry.changed_by;
|
|
708
|
+
bySpan.className = "ultra-settings-ss-history-entry-who";
|
|
709
|
+
header.appendChild(bySpan);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (entry.deleted) {
|
|
713
|
+
const badge = document.createElement("span");
|
|
714
|
+
badge.className = "ultra-settings-ss-history-entry-badge deleted";
|
|
715
|
+
badge.textContent = t("edit.history_deleted");
|
|
716
|
+
header.appendChild(badge);
|
|
717
|
+
} else if (entries.indexOf(entry) === entries.length - 1) {
|
|
718
|
+
const badge = document.createElement("span");
|
|
719
|
+
badge.className = "ultra-settings-ss-history-entry-badge created";
|
|
720
|
+
badge.textContent = t("edit.history_created");
|
|
721
|
+
header.appendChild(badge);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
div.appendChild(header);
|
|
725
|
+
|
|
726
|
+
if (!entry.deleted) {
|
|
727
|
+
const val = document.createElement("div");
|
|
728
|
+
val.className = "ultra-settings-ss-history-entry-value";
|
|
729
|
+
if (entry.value == null) {
|
|
730
|
+
val.classList.add("nil");
|
|
731
|
+
val.textContent = "nil";
|
|
732
|
+
} else {
|
|
733
|
+
val.textContent = String(entry.value);
|
|
734
|
+
}
|
|
735
|
+
div.appendChild(val);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
ssHistoryEntries.appendChild(div);
|
|
739
|
+
});
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const HISTORY_LIMIT = 25;
|
|
743
|
+
|
|
744
|
+
const loadHistory = (key, offset) => {
|
|
745
|
+
ssHistoryEntries.innerHTML = "";
|
|
746
|
+
const loading = document.createElement("div");
|
|
747
|
+
loading.className = "ultra-settings-ss-history-empty";
|
|
748
|
+
loading.textContent = t("edit.history_loading");
|
|
749
|
+
ssHistoryEntries.appendChild(loading);
|
|
750
|
+
ssHistoryPagination.style.display = "none";
|
|
751
|
+
|
|
752
|
+
const params = {key: key, limit: HISTORY_LIMIT, offset: offset};
|
|
753
|
+
|
|
754
|
+
SuperSettingsAPI.fetchHistory(params, (data) => {
|
|
755
|
+
const entries = data.histories || data.history || [];
|
|
756
|
+
renderHistoryEntries(entries);
|
|
757
|
+
|
|
758
|
+
// Pagination based on whether we have a previous or next page
|
|
759
|
+
const hasPrev = offset > 0;
|
|
760
|
+
const hasNext = entries.length >= HISTORY_LIMIT;
|
|
761
|
+
|
|
762
|
+
if (hasPrev || hasNext) {
|
|
763
|
+
ssHistoryPagination.style.display = "";
|
|
764
|
+
ssHistoryPrevBtn.disabled = !hasPrev;
|
|
765
|
+
ssHistoryNextBtn.disabled = !hasNext;
|
|
766
|
+
|
|
767
|
+
ssHistoryPrevBtn.onclick = () => { loadHistory(key, Math.max(0, offset - HISTORY_LIMIT)); };
|
|
768
|
+
ssHistoryNextBtn.onclick = () => { loadHistory(key, offset + HISTORY_LIMIT); };
|
|
769
|
+
} else {
|
|
770
|
+
ssHistoryPagination.style.display = "none";
|
|
771
|
+
}
|
|
772
|
+
}, () => {
|
|
773
|
+
ssHistoryEntries.innerHTML = "";
|
|
774
|
+
const err = document.createElement("div");
|
|
775
|
+
err.className = "ultra-settings-ss-history-empty";
|
|
776
|
+
err.textContent = t("edit.history_empty");
|
|
777
|
+
ssHistoryEntries.appendChild(err);
|
|
778
|
+
ssHistoryPagination.style.display = "none";
|
|
779
|
+
});
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
const showHistory = (key) => {
|
|
783
|
+
ssHistoryCurrentKey = key;
|
|
784
|
+
ssForm.style.display = "none";
|
|
785
|
+
ssHistoryContainer.style.display = "";
|
|
786
|
+
loadHistory(key, 0);
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const hideHistory = () => {
|
|
790
|
+
ssHistoryContainer.style.display = "none";
|
|
791
|
+
ssForm.style.display = "";
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
if (ssHistoryLink) {
|
|
795
|
+
ssHistoryLink.addEventListener("click", (e) => {
|
|
796
|
+
e.preventDefault();
|
|
797
|
+
showHistory(ssKeyInput.value);
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (ssHistoryBackLink) {
|
|
802
|
+
ssHistoryBackLink.addEventListener("click", (e) => {
|
|
803
|
+
e.preventDefault();
|
|
804
|
+
hideHistory();
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Delegate clicks on edit buttons
|
|
809
|
+
document.addEventListener("click", (e) => {
|
|
810
|
+
const btn = e.target.closest(".ultra-settings-ss-edit-btn");
|
|
811
|
+
if (btn) {
|
|
812
|
+
e.preventDefault();
|
|
813
|
+
openSsPanel(
|
|
814
|
+
btn.dataset.ssKey || "",
|
|
815
|
+
btn.dataset.ssDefaultType || "string",
|
|
816
|
+
btn.dataset.ssDefaultDescription || ""
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// ── Language Menu ──
|
|
824
|
+
if (languageMenu && languageOptions.length > 0) {
|
|
825
|
+
languageOptions.forEach((option) => {
|
|
826
|
+
option.addEventListener("click", () => {
|
|
827
|
+
const locale = option.dataset.locale;
|
|
828
|
+
if (!locale) return;
|
|
829
|
+
|
|
830
|
+
closeLanguageMenu();
|
|
831
|
+
|
|
832
|
+
// Persist the choice in a cookie (accessible server-side)
|
|
833
|
+
document.cookie = "ultra_settings_locale=" + encodeURIComponent(locale) + ";path=/;max-age=31536000;SameSite=Lax";
|
|
834
|
+
|
|
835
|
+
// Also store in localStorage for client-side persistence
|
|
836
|
+
try { localStorage.setItem("ultra_settings_locale", locale); } catch(e) {}
|
|
837
|
+
|
|
838
|
+
// Reload with lang query param so server picks it up immediately
|
|
839
|
+
const url = new URL(window.location.href);
|
|
840
|
+
url.searchParams.set("lang", locale);
|
|
841
|
+
window.location.href = url.toString();
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
document.addEventListener("click", (e) => {
|
|
846
|
+
if (languageMenu.hasAttribute("open") && !e.target.closest("#ultra-settings-language-menu")) {
|
|
847
|
+
closeLanguageMenu();
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
}
|
|
124
851
|
});
|