super_settings 0.0.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +313 -0
- data/VERSION +1 -0
- data/app/helpers/super_settings/settings_helper.rb +32 -0
- data/app/views/layouts/super_settings/settings.html.erb +20 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20210414004553_create_super_settings.rb +34 -0
- data/lib/super_settings/application/api.js +88 -0
- data/lib/super_settings/application/helper.rb +119 -0
- data/lib/super_settings/application/images/edit.svg +1 -0
- data/lib/super_settings/application/images/info.svg +1 -0
- data/lib/super_settings/application/images/plus.svg +1 -0
- data/lib/super_settings/application/images/slash.svg +1 -0
- data/lib/super_settings/application/images/trash.svg +1 -0
- data/lib/super_settings/application/index.html.erb +169 -0
- data/lib/super_settings/application/layout.html.erb +22 -0
- data/lib/super_settings/application/layout_styles.css +193 -0
- data/lib/super_settings/application/scripts.js +718 -0
- data/lib/super_settings/application/styles.css +122 -0
- data/lib/super_settings/application.rb +38 -0
- data/lib/super_settings/attributes.rb +24 -0
- data/lib/super_settings/coerce.rb +66 -0
- data/lib/super_settings/configuration.rb +144 -0
- data/lib/super_settings/controller_actions.rb +81 -0
- data/lib/super_settings/encryption.rb +76 -0
- data/lib/super_settings/engine.rb +70 -0
- data/lib/super_settings/history_item.rb +26 -0
- data/lib/super_settings/local_cache.rb +306 -0
- data/lib/super_settings/rack_middleware.rb +210 -0
- data/lib/super_settings/rest_api.rb +195 -0
- data/lib/super_settings/setting.rb +599 -0
- data/lib/super_settings/storage/active_record_storage.rb +123 -0
- data/lib/super_settings/storage/http_storage.rb +279 -0
- data/lib/super_settings/storage/redis_storage.rb +293 -0
- data/lib/super_settings/storage/test_storage.rb +158 -0
- data/lib/super_settings/storage.rb +254 -0
- data/lib/super_settings/version.rb +5 -0
- data/lib/super_settings.rb +213 -0
- data/lib/tasks/super_settings.rake +9 -0
- data/super_settings.gemspec +35 -0
- metadata +113 -0
@@ -0,0 +1,718 @@
|
|
1
|
+
(function() {
|
2
|
+
// Return the table row element for a setting.
|
3
|
+
function findSettingRow(id) {
|
4
|
+
if (id) {
|
5
|
+
return document.querySelector('#settings-table tr[data-id="' + id + '"]');
|
6
|
+
} else {
|
7
|
+
return null;
|
8
|
+
}
|
9
|
+
}
|
10
|
+
|
11
|
+
// Return the number of settings that have been edited.
|
12
|
+
function changesCount() {
|
13
|
+
return document.querySelectorAll("#settings-table tbody tr[data-edited=true]").length;
|
14
|
+
}
|
15
|
+
|
16
|
+
// Set the enabled status of the save button for submitting the form.
|
17
|
+
function enableSaveButton() {
|
18
|
+
const saveButton = document.querySelector("#save-settings");
|
19
|
+
const discardButton = document.querySelector("#discard-changes");
|
20
|
+
if (saveButton) {
|
21
|
+
const count = changesCount();
|
22
|
+
const countSpan = saveButton.querySelector(".count");
|
23
|
+
if (count === 0) {
|
24
|
+
saveButton.disabled = true;
|
25
|
+
countSpan.innerHTML = "";
|
26
|
+
discardButton.disabled = true;
|
27
|
+
} else {
|
28
|
+
saveButton.disabled = false;
|
29
|
+
countSpan.innerHTML = count;
|
30
|
+
discardButton.disabled = false;
|
31
|
+
}
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
// Set the display value for a setting.
|
36
|
+
function setSettingDisplayValue(element, setting) {
|
37
|
+
if (setting.value === null || setting.value === undefined) {
|
38
|
+
element.innerText = "";
|
39
|
+
} else if (Array.isArray(setting.value)) {
|
40
|
+
let arrayHTML = "";
|
41
|
+
setting.value.map(function(val) {
|
42
|
+
arrayHTML += `<div>${escapeHTML(val)}</div>`;
|
43
|
+
});
|
44
|
+
element.innerHTML = arrayHTML;
|
45
|
+
} else if (setting.value_type === "datetime") {
|
46
|
+
try {
|
47
|
+
const datetime = new Date(Date.parse(setting.value));
|
48
|
+
element.innerText = datetime.toUTCString().replace("GMT", "UTC");
|
49
|
+
} catch (e) {
|
50
|
+
element.innerText = "" + setting.value
|
51
|
+
}
|
52
|
+
} else if (setting.value_type === "secret") {
|
53
|
+
let placeholder = "••••••••••••••••••••••••";
|
54
|
+
if (!setting.encrypted) {
|
55
|
+
placeholder += '<br><span class="text-danger">not encrypted</span>'
|
56
|
+
}
|
57
|
+
element.innerHTML = placeholder;
|
58
|
+
} else {
|
59
|
+
element.innerText = "" + setting.value
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
// Get the value of a setting from the edit form field.
|
64
|
+
function getSettingEditValue(row) {
|
65
|
+
if (row.querySelector(".super-settings-value input.js-setting-value[type=checkbox]")) {
|
66
|
+
return row.querySelector(".super-settings-value input.js-setting-value[type=checkbox]").checked;
|
67
|
+
} else {
|
68
|
+
return row.querySelector(".super-settings-value .js-setting-value").value;
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
// Helper function to pad time values with a zero for making ISO-8601 date and time formats.
|
73
|
+
function padTimeVal(val) {
|
74
|
+
return ("" + val).padStart(2, "0");
|
75
|
+
}
|
76
|
+
|
77
|
+
// Escape special HTML characters in text.
|
78
|
+
function escapeHTML(text) {
|
79
|
+
if (text === null || text === undefined) {
|
80
|
+
return "";
|
81
|
+
}
|
82
|
+
const htmlEscapes = {'&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/'};
|
83
|
+
const htmlEscaper = /[&<>"'\/]/g;
|
84
|
+
return ('' + text).replace(htmlEscaper, function(match) {
|
85
|
+
return htmlEscapes[match];
|
86
|
+
});
|
87
|
+
}
|
88
|
+
|
89
|
+
// Helper function for use with templates to replace the text {{id}} with a setting's id value.
|
90
|
+
function mustacheSubstitute(html, setting) {
|
91
|
+
return html.replaceAll("{{id}}", escapeHTML(setting.id));
|
92
|
+
}
|
93
|
+
|
94
|
+
// Extract a new DOM element from a <template> element on the page.
|
95
|
+
function elementFromSettingTemplate(setting, templateSelector) {
|
96
|
+
let html = document.querySelector(templateSelector).innerHTML;
|
97
|
+
html = mustacheSubstitute(html, setting);
|
98
|
+
const template = document.createElement('template');
|
99
|
+
template.innerHTML = html.trim();
|
100
|
+
return template.content.firstChild;
|
101
|
+
}
|
102
|
+
|
103
|
+
// Create a table row element for displaying a setting.
|
104
|
+
function settingRow(setting) {
|
105
|
+
const row = elementFromSettingTemplate(setting, "#setting-row-template");
|
106
|
+
row.dataset.id = setting.id
|
107
|
+
row.dataset.key = setting.key
|
108
|
+
row.querySelector(".js-setting-key").value = setting.key;
|
109
|
+
if (setting.deleted) {
|
110
|
+
row.dataset.edited = true
|
111
|
+
row.dataset.deleted = true
|
112
|
+
row.querySelector(".js-setting-deleted").value = "1";
|
113
|
+
}
|
114
|
+
if (setting.key !== null && setting.key !== undefined) {
|
115
|
+
row.querySelector(".super-settings-key .js-value-placeholder").innerText = setting.key;
|
116
|
+
}
|
117
|
+
if (setting.value !== null && setting.value !== undefined) {
|
118
|
+
setSettingDisplayValue(row.querySelector(".super-settings-value .js-value-placeholder"), setting);
|
119
|
+
}
|
120
|
+
if (setting.value_type !== null && setting.value_type !== undefined) {
|
121
|
+
row.querySelector(".super-settings-value-type .js-value-placeholder").innerText = setting.value_type;
|
122
|
+
}
|
123
|
+
if (setting.description !== null && setting.description !== undefined) {
|
124
|
+
row.querySelector(".super-settings-description .js-value-placeholder").innerHTML = escapeHTML(setting.description).replaceAll("\n", "<br>");
|
125
|
+
}
|
126
|
+
|
127
|
+
return row
|
128
|
+
}
|
129
|
+
|
130
|
+
// Create an input element from a template depending on the value type.
|
131
|
+
function createValueInputElement(setting) {
|
132
|
+
let templateName = null;
|
133
|
+
if (setting.value_type === "integer") {
|
134
|
+
templateName = "#setting-value-field-integer-template";
|
135
|
+
} else if (setting.value_type === "float") {
|
136
|
+
templateName = "#setting-value-field-float-template";
|
137
|
+
} else if (setting.value_type === "datetime") {
|
138
|
+
templateName = "#setting-value-field-datetime-template";
|
139
|
+
} else if (setting.value_type === "boolean") {
|
140
|
+
templateName = "#setting-value-field-boolean-template";
|
141
|
+
} else if (setting.value_type === "array") {
|
142
|
+
templateName = "#setting-value-field-array-template";
|
143
|
+
} else {
|
144
|
+
templateName = "#setting-value-field-template";
|
145
|
+
}
|
146
|
+
const html = mustacheSubstitute(document.querySelector(templateName).innerHTML, setting);
|
147
|
+
const template = document.createElement('template');
|
148
|
+
template.innerHTML = html.trim();
|
149
|
+
return template.content.firstChild;
|
150
|
+
}
|
151
|
+
|
152
|
+
// Create the elements needed to edit a setting value and set the element value.
|
153
|
+
function valueInputElement(setting) {
|
154
|
+
const element = createValueInputElement(setting);
|
155
|
+
if (setting.value_type === "boolean") {
|
156
|
+
const checked = (`${setting.value}` === "true" || parseInt(setting.value) > 0);
|
157
|
+
const checkbox = element.querySelector('input[type="checkbox"]');
|
158
|
+
checkbox.checked = checked;
|
159
|
+
} else if (setting.value_type === "array") {
|
160
|
+
if (Array.isArray(setting.value)) {
|
161
|
+
element.value = setting.value.join("\n");
|
162
|
+
} else {
|
163
|
+
element.value = setting.value;
|
164
|
+
}
|
165
|
+
} else if (setting.value_type === "datetime") {
|
166
|
+
try {
|
167
|
+
const datetime = new Date(Date.parse(setting.value));
|
168
|
+
const isoDate = `${datetime.getUTCFullYear()}-${padTimeVal(datetime.getUTCMonth() + 1)}-${padTimeVal(datetime.getUTCDate())}`;
|
169
|
+
const isoTime = `${padTimeVal(datetime.getUTCHours())}:${padTimeVal(datetime.getUTCMinutes())}:${padTimeVal(datetime.getUTCSeconds())}`;
|
170
|
+
element.querySelector('input[type="date"]').value = isoDate;
|
171
|
+
element.querySelector('input[type="time"]').value = isoTime;
|
172
|
+
element.querySelector(".js-setting-value").value = datetime.toUTCString().replace("GMT", "UTC");
|
173
|
+
} catch(e) {
|
174
|
+
// ignore bad date format
|
175
|
+
}
|
176
|
+
} else if (setting.value_type === "integer") {
|
177
|
+
element.value = "" + parseInt("" + setting.value, 10);
|
178
|
+
} else if (setting.value_type === "float") {
|
179
|
+
element.value = "" + parseFloat("" + setting.value);
|
180
|
+
} else {
|
181
|
+
element.value = setting.value;
|
182
|
+
}
|
183
|
+
|
184
|
+
return element;
|
185
|
+
}
|
186
|
+
|
187
|
+
// Create a table row with form elements for editing a setting.
|
188
|
+
function editSettingRow(setting) {
|
189
|
+
const row = elementFromSettingTemplate(setting, "#setting-row-edit-template");
|
190
|
+
row.dataset.id = setting.id
|
191
|
+
|
192
|
+
row.querySelector(".super-settings-key input").value = setting.key;
|
193
|
+
if (setting.description) {
|
194
|
+
row.querySelector(".super-settings-description textarea").value = setting.description;
|
195
|
+
}
|
196
|
+
|
197
|
+
const valueInput = valueInputElement(setting);
|
198
|
+
const valuePlaceholder = row.querySelector(".super-settings-value .js-value-placeholder");
|
199
|
+
valuePlaceholder.innerHTML = "";
|
200
|
+
valuePlaceholder.appendChild(valueInput);
|
201
|
+
|
202
|
+
const valueType = row.querySelector(".super-settings-value-type select");
|
203
|
+
for (let i = 0; i < valueType.options.length; i++) {
|
204
|
+
if (valueType.options[i].value === setting.value_type) {
|
205
|
+
valueType.selectedIndex = i;
|
206
|
+
break;
|
207
|
+
}
|
208
|
+
}
|
209
|
+
|
210
|
+
if (setting.errors && setting.errors.length > 0) {
|
211
|
+
let errorsHTML = "";
|
212
|
+
setting.errors.forEach(function(error) {
|
213
|
+
errorsHTML += `<div>${escapeHTML(error)}</div>`
|
214
|
+
});
|
215
|
+
row.querySelector(".js-setting-errors").innerHTML = errorsHTML;
|
216
|
+
}
|
217
|
+
|
218
|
+
if (setting.new_record) {
|
219
|
+
row.dataset.newrecord = "true";
|
220
|
+
}
|
221
|
+
|
222
|
+
return row
|
223
|
+
}
|
224
|
+
|
225
|
+
// Create a table row with form elements for creating a new setting.
|
226
|
+
function newSettingRow() {
|
227
|
+
const randomId = "new" + Math.floor((Math.random() * 0xFFFFFFFFFFFFFF)).toString(16);
|
228
|
+
const setting = {id: randomId, key: "", value: "", value_type: "string", new_record: true}
|
229
|
+
row = editSettingRow(setting);
|
230
|
+
return row;
|
231
|
+
}
|
232
|
+
|
233
|
+
// Add a setting table row the table of settings.
|
234
|
+
function addRowToTable(row) {
|
235
|
+
const existingRow = findSettingRow(row.dataset.id);
|
236
|
+
if (existingRow) {
|
237
|
+
existingRow.replaceWith(row);
|
238
|
+
} else {
|
239
|
+
document.querySelector("#settings-table tbody").prepend(row);
|
240
|
+
}
|
241
|
+
bindSettingControlEvents(row);
|
242
|
+
filterSettings(document.querySelector("#filter").value);
|
243
|
+
row.scrollIntoView({block: "nearest"});
|
244
|
+
enableSaveButton();
|
245
|
+
return row;
|
246
|
+
}
|
247
|
+
|
248
|
+
// Update the window location URL to reflect the current filter text.
|
249
|
+
function updateFilterURL(filter) {
|
250
|
+
const queryParams = new URLSearchParams(window.location.search);
|
251
|
+
if (filter === "") {
|
252
|
+
queryParams.delete("filter");
|
253
|
+
} else {
|
254
|
+
queryParams.set("filter", filter);
|
255
|
+
}
|
256
|
+
if (queryParams.toString() !== "") {
|
257
|
+
history.replaceState(null, null, "?" + queryParams.toString());
|
258
|
+
} else {
|
259
|
+
history.replaceState(null, null, window.location.pathname);
|
260
|
+
}
|
261
|
+
}
|
262
|
+
|
263
|
+
// Apply the given filter to only show settings that have a key, value, or description
|
264
|
+
// that includes the filter text. Settings that are currently being edited will also be shown.
|
265
|
+
function filterSettings(filterText) {
|
266
|
+
const filters = [];
|
267
|
+
filterText.split(" ").forEach(function(filter) {
|
268
|
+
filter = filter.toUpperCase();
|
269
|
+
filters.push(function(tr) {
|
270
|
+
let text = "";
|
271
|
+
const settingKey = tr.querySelector(".super-settings-key");
|
272
|
+
if (settingKey) {
|
273
|
+
text += " " + settingKey.textContent.toUpperCase();
|
274
|
+
}
|
275
|
+
const settingValue = tr.querySelector(".super-settings-value");
|
276
|
+
if (settingValue) {
|
277
|
+
text += " " + settingValue.textContent.toUpperCase();
|
278
|
+
}
|
279
|
+
const settingDescription = tr.querySelector(".setting-description");
|
280
|
+
if (settingDescription) {
|
281
|
+
text += " " + settingDescription.textContent.toUpperCase();
|
282
|
+
}
|
283
|
+
return (text.indexOf(filter) > -1);
|
284
|
+
});
|
285
|
+
});
|
286
|
+
|
287
|
+
document.querySelectorAll("#settings-table tbody tr").forEach(function(tr) {
|
288
|
+
let matched = true;
|
289
|
+
if (!tr.dataset.edited) {
|
290
|
+
filters.forEach(function(filter) {
|
291
|
+
matched = matched && filter(tr);
|
292
|
+
});
|
293
|
+
}
|
294
|
+
if (matched) {
|
295
|
+
tr.style.display = "table-row";
|
296
|
+
} else {
|
297
|
+
tr.style.display = "none";
|
298
|
+
}
|
299
|
+
});
|
300
|
+
}
|
301
|
+
|
302
|
+
// Programatically apply the filter again to keep it up to date with other changes.
|
303
|
+
function applyFilter(value) {
|
304
|
+
const filter = document.querySelector("#filter");
|
305
|
+
if (filter) {
|
306
|
+
if (value) {
|
307
|
+
filter.value = value;
|
308
|
+
}
|
309
|
+
}
|
310
|
+
}
|
311
|
+
|
312
|
+
// Display validation errors on settings form.
|
313
|
+
function showValidationErrors(errors) {
|
314
|
+
const table = document.querySelector("#settings-table");
|
315
|
+
Object.keys(errors).forEach(function(key) {
|
316
|
+
table.querySelectorAll(".super-settings-edit-row").forEach(function(row) {
|
317
|
+
const settingKey = row.querySelector(".js-setting-key");
|
318
|
+
if (settingKey && settingKey.value === key) {
|
319
|
+
const errorsElement = row.querySelector(".js-setting-errors");
|
320
|
+
if (errorsElement) {
|
321
|
+
errorsElement.innerText = errors[key].join("; ");
|
322
|
+
errorsElement.style.display = "block";
|
323
|
+
}
|
324
|
+
}
|
325
|
+
});
|
326
|
+
});
|
327
|
+
}
|
328
|
+
|
329
|
+
// Show a temporary message to give the user feedback that an operation has succeeded or not.
|
330
|
+
function showFlash(message, success) {
|
331
|
+
const flash = document.querySelector(".js-flash");
|
332
|
+
if (success) {
|
333
|
+
flash.classList.add("text-success");
|
334
|
+
flash.classList.remove("text-danger");
|
335
|
+
} else {
|
336
|
+
flash.classList.add("text-danger");
|
337
|
+
flash.classList.remove("text-success");
|
338
|
+
}
|
339
|
+
flash.innerText = message;
|
340
|
+
flash.style.display = "inline-block";
|
341
|
+
dismissFlash();
|
342
|
+
}
|
343
|
+
|
344
|
+
// Automatically hide the flash message displaying the results of the last save operation.
|
345
|
+
function dismissFlash() {
|
346
|
+
if (document.querySelector(".js-flash")) {
|
347
|
+
setTimeout(function(){
|
348
|
+
document.querySelectorAll(".js-flash").forEach(function(element) {
|
349
|
+
element.style.display = "none";
|
350
|
+
});
|
351
|
+
}, 3000);
|
352
|
+
}
|
353
|
+
}
|
354
|
+
|
355
|
+
// Render a setting's history in a table.
|
356
|
+
function renderHistoryTable(parent, payload) {
|
357
|
+
parent.innerHTML = document.querySelector("#setting-history-table").innerHTML.trim();
|
358
|
+
parent.querySelector(".super-settings-history-key").innerText = payload.key;
|
359
|
+
const tbody = parent.querySelector("tbody");
|
360
|
+
let rowsHTML = "";
|
361
|
+
payload.histories.forEach(function(history) {
|
362
|
+
const date = (new Date(Date.parse(history.created_at))).toUTCString().replace("GMT", "UTC");
|
363
|
+
const value = (payload.encrypted ? "<em>n/a</em>" : escapeHTML(history.value));
|
364
|
+
rowsHTML += `<tr><td class="super-settings-text-nowrap">${escapeHTML(date)}</td><td>${escapeHTML(history.changed_by)}</td><td>${value}</td></tr>`;
|
365
|
+
});
|
366
|
+
tbody.insertAdjacentHTML("beforeend", rowsHTML);
|
367
|
+
|
368
|
+
if (payload.previous_page_params || payload.next_page_params) {
|
369
|
+
let paginationHTML = `<div class="align-center">`;
|
370
|
+
if (payload.previous_page_params) {
|
371
|
+
paginationHTML += `<div style="float:left;"><a href="#" class="js-show-history" title="Newer" data-offset="${payload.previous_page_params.offset}" data-limit="${payload.previous_page_params.limit}" data-key="${payload.previous_page_params.key}")>← Newer</a></div>`;
|
372
|
+
}
|
373
|
+
if (payload.next_page_params) {
|
374
|
+
paginationHTML += `<div style="float:right;"><a href="#" class="js-show-history" title="Older" data-offset="${payload.next_page_params.offset}" data-limit="${payload.next_page_params.limit}" data-key="${payload.next_page_params.key}")>Older →</a></div>`;
|
375
|
+
}
|
376
|
+
paginationHTML += '<div style="clear:both;"></div>';
|
377
|
+
parent.querySelector("table").insertAdjacentHTML("afterend", paginationHTML);
|
378
|
+
}
|
379
|
+
addListener(parent.querySelectorAll(".js-show-history"), "click", showHistoryModal);
|
380
|
+
}
|
381
|
+
|
382
|
+
// Show a modal window overlayed on the page.
|
383
|
+
function showModal() {
|
384
|
+
const modal = document.querySelector("#modal");
|
385
|
+
const content = document.querySelector(".super-settings-modal-content");
|
386
|
+
modal.style.display = "block";
|
387
|
+
modal.setAttribute("aria-hidden", "false");
|
388
|
+
modal.activator = document.activeElement;
|
389
|
+
focusableElements(document).forEach(function(element) {
|
390
|
+
if (!modal.contains(element)) {
|
391
|
+
element.dataset.saveTabIndex = element.getAttribute("tabindex");
|
392
|
+
element.setAttribute("tabindex", -1);
|
393
|
+
}
|
394
|
+
});
|
395
|
+
document.querySelector("body").style.overflow = "hidden";
|
396
|
+
}
|
397
|
+
|
398
|
+
// Hide the modal window overlayed on the page.
|
399
|
+
function hideModal() {
|
400
|
+
const modal = document.querySelector("#modal");
|
401
|
+
const content = document.querySelector(".super-settings-modal-content");
|
402
|
+
modal.style.display = "none";
|
403
|
+
modal.setAttribute("aria-hidden", "true");
|
404
|
+
focusableElements(document).forEach(function(element) {
|
405
|
+
const tabIndex = element.dataset.saveTabIndex;
|
406
|
+
delete element.dataset.saveTabIndex;
|
407
|
+
if (tabIndex) {
|
408
|
+
element.setAttribute("tabindex", tabIndex);
|
409
|
+
}
|
410
|
+
});
|
411
|
+
if (modal.activator) {
|
412
|
+
modal.activator.focus();
|
413
|
+
delete modal.activator;
|
414
|
+
}
|
415
|
+
content.innerHTML = "";
|
416
|
+
document.querySelector("body").style.overflow = "visible";
|
417
|
+
}
|
418
|
+
|
419
|
+
// Returns a list of all focusable elements so that they can be set to not take the focus
|
420
|
+
// when a modal is opened.
|
421
|
+
function focusableElements(parent) {
|
422
|
+
return parent.querySelectorAll("a[href], area[href], button, input:not([type=hidden]), select, textarea, iframe, [tabindex], [contentEditable=true]")
|
423
|
+
}
|
424
|
+
|
425
|
+
// Find a setting by key.
|
426
|
+
function findSetting(id) {
|
427
|
+
let found = null;
|
428
|
+
id = "" + id;
|
429
|
+
activeSettings.forEach(function(setting) {
|
430
|
+
if ("" + setting.id === id) {
|
431
|
+
found = setting;
|
432
|
+
return;
|
433
|
+
}
|
434
|
+
});
|
435
|
+
return found;
|
436
|
+
}
|
437
|
+
|
438
|
+
/*** Event Listeners ***/
|
439
|
+
|
440
|
+
// Listener for showing the setting history modal.
|
441
|
+
function showHistoryModal(event) {
|
442
|
+
event.preventDefault();
|
443
|
+
if (!event.target.dataset) {
|
444
|
+
return;
|
445
|
+
}
|
446
|
+
|
447
|
+
const modal = document.querySelector("#modal");
|
448
|
+
const content = document.querySelector(".super-settings-modal-content");
|
449
|
+
let key = event.target.dataset.key;
|
450
|
+
if (!key) {
|
451
|
+
const row = event.target.closest("tr");
|
452
|
+
if (row) {
|
453
|
+
const id = row.dataset.id;
|
454
|
+
const setting = findSetting(id);
|
455
|
+
if (setting) {
|
456
|
+
key = setting.key;
|
457
|
+
if (!key) {
|
458
|
+
return;
|
459
|
+
}
|
460
|
+
}
|
461
|
+
}
|
462
|
+
}
|
463
|
+
const params = {key: key, limit: 25};
|
464
|
+
if (event.target.dataset.limit) {
|
465
|
+
params["limit"] = event.target.dataset.limit;
|
466
|
+
}
|
467
|
+
if (event.target.dataset.offset) {
|
468
|
+
params["offset"] = event.target.dataset.offset;
|
469
|
+
}
|
470
|
+
SuperSettingsAPI.fetchHistory(params, function(settingHistory){
|
471
|
+
renderHistoryTable(content, settingHistory);
|
472
|
+
showModal();
|
473
|
+
});
|
474
|
+
}
|
475
|
+
|
476
|
+
// Listener for closing the modal window overlay.
|
477
|
+
function closeModal(event) {
|
478
|
+
if (event.target.classList.contains("js-close-modal")) {
|
479
|
+
event.preventDefault();
|
480
|
+
hideModal();
|
481
|
+
}
|
482
|
+
}
|
483
|
+
|
484
|
+
// Listener to just capture events.
|
485
|
+
function noOp(event) {
|
486
|
+
event.preventDefault();
|
487
|
+
}
|
488
|
+
|
489
|
+
// Listener for changing the setting value type select element. Different types will have
|
490
|
+
// different input elements for the setting value.
|
491
|
+
function changeSettingType(event) {
|
492
|
+
event.preventDefault();
|
493
|
+
const row = event.target.closest("tr");
|
494
|
+
const valueType = event.target.options[event.target.selectedIndex].value;
|
495
|
+
var setting = {
|
496
|
+
id: row.dataset.id,
|
497
|
+
key: row.querySelector(".super-settings-key input").value,
|
498
|
+
value: getSettingEditValue(row),
|
499
|
+
value_type: valueType,
|
500
|
+
description: row.querySelector(".super-settings-description textarea").value,
|
501
|
+
new_record: row.dataset.newrecord
|
502
|
+
}
|
503
|
+
const addedRow = addRowToTable(editSettingRow(setting));
|
504
|
+
if (addedRow.querySelector(".super-settings-value .js-date-input")) {
|
505
|
+
addedRow.querySelector(".super-settings-value .js-date-input").focus();
|
506
|
+
} else {
|
507
|
+
addedRow.querySelector(".super-settings-value .js-setting-value").focus();
|
508
|
+
}
|
509
|
+
}
|
510
|
+
|
511
|
+
// Listener for date and time input elements the combine the values into a hidden datetime field.
|
512
|
+
function changeDateTime(event) {
|
513
|
+
const parentNode = event.target.closest("span")
|
514
|
+
const dateValue = parentNode.querySelector(".js-date-input").value;
|
515
|
+
let timeValue = parentNode.querySelector(".js-time-input").value;
|
516
|
+
if (timeValue === "") {
|
517
|
+
timeValue = "00:00:00";
|
518
|
+
}
|
519
|
+
parentNode.querySelector(".js-setting-value").value = `${dateValue}T${timeValue}Z`
|
520
|
+
}
|
521
|
+
|
522
|
+
// Listener for the add setting button.
|
523
|
+
function addSetting(event) {
|
524
|
+
event.preventDefault();
|
525
|
+
const row = addRowToTable(newSettingRow());
|
526
|
+
row.querySelector(".super-settings-key input").focus();
|
527
|
+
}
|
528
|
+
|
529
|
+
// Listener for the edit setting button.
|
530
|
+
function editSetting(event) {
|
531
|
+
event.preventDefault();
|
532
|
+
const id = event.target.closest("tr").dataset.id;
|
533
|
+
const setting = findSetting(id);
|
534
|
+
const row = addRowToTable(editSettingRow(setting));
|
535
|
+
if (row.querySelector(".super-settings-value .js-date-input")) {
|
536
|
+
row.querySelector(".super-settings-value .js-date-input").focus();
|
537
|
+
} else {
|
538
|
+
row.querySelector(".super-settings-value .js-setting-value").focus();
|
539
|
+
}
|
540
|
+
}
|
541
|
+
|
542
|
+
// Listener for the restore setting button.
|
543
|
+
function restoreSetting(event) {
|
544
|
+
event.preventDefault();
|
545
|
+
const row = event.target.closest("tr");
|
546
|
+
const id = row.dataset.id;
|
547
|
+
const setting = findSetting(id);
|
548
|
+
if (setting) {
|
549
|
+
const newRow = settingRow(setting);
|
550
|
+
bindSettingControlEvents(newRow);
|
551
|
+
row.replaceWith(newRow);
|
552
|
+
} else {
|
553
|
+
row.remove();
|
554
|
+
}
|
555
|
+
enableSaveButton();
|
556
|
+
}
|
557
|
+
|
558
|
+
// Listener for the remove setting button.
|
559
|
+
function removeSetting(event) {
|
560
|
+
event.preventDefault();
|
561
|
+
const settingRow = event.target.closest("tr");
|
562
|
+
if (settingRow.dataset["id"]) {
|
563
|
+
settingRow.querySelector("input.js-setting-deleted").value = "1";
|
564
|
+
settingRow.dataset.edited = true;
|
565
|
+
settingRow.dataset.deleted = true;
|
566
|
+
settingRow.querySelector(".js-remove-setting").style.display = "none";
|
567
|
+
settingRow.querySelector(".js-restore-setting").style.display = "inline-block";
|
568
|
+
} else {
|
569
|
+
settingRow.remove();
|
570
|
+
}
|
571
|
+
enableSaveButton();
|
572
|
+
}
|
573
|
+
|
574
|
+
// Update the settings via the API.
|
575
|
+
function updateSettings(event) {
|
576
|
+
event.preventDefault();
|
577
|
+
event.target.disabled = true;
|
578
|
+
const settingsData = [];
|
579
|
+
document.querySelectorAll("#settings-table tbody tr[data-edited=true]").forEach(function(row) {
|
580
|
+
const data = {};
|
581
|
+
settingsData.push(data);
|
582
|
+
data.key = row.querySelector(".js-setting-key").value;
|
583
|
+
const deleted = row.querySelector(".js-setting-deleted");
|
584
|
+
if (deleted && deleted.value === "1") {
|
585
|
+
data.deleted = true;
|
586
|
+
} else {
|
587
|
+
if (row.querySelector(".js-setting-value")) {
|
588
|
+
data.value = getSettingEditValue(row);
|
589
|
+
}
|
590
|
+
if (row.querySelector(".js-setting-value-type")) {
|
591
|
+
const valueTypeSelect = row.querySelector(".js-setting-value-type");
|
592
|
+
data.value_type = valueTypeSelect.options[valueTypeSelect.selectedIndex].value;
|
593
|
+
}
|
594
|
+
if (row.querySelector(".super-settings-description textarea")) {
|
595
|
+
data.description = row.querySelector(".super-settings-description textarea").value;
|
596
|
+
}
|
597
|
+
}
|
598
|
+
});
|
599
|
+
|
600
|
+
SuperSettingsAPI.updateSettings({settings: settingsData}, function(results) {
|
601
|
+
if (results.success) {
|
602
|
+
fetchActiveSettings();
|
603
|
+
showFlash("Settings saved", true)
|
604
|
+
} else {
|
605
|
+
event.target.disabled = false;
|
606
|
+
showFlash("Failed to save settings", false)
|
607
|
+
if (results.errors) {
|
608
|
+
showValidationErrors(results.errors)
|
609
|
+
}
|
610
|
+
}
|
611
|
+
});
|
612
|
+
}
|
613
|
+
|
614
|
+
// Listener for the filter input field.
|
615
|
+
function filterListener(event) {
|
616
|
+
const filter = event.target.value;
|
617
|
+
filterSettings(filter);
|
618
|
+
updateFilterURL(filter);
|
619
|
+
}
|
620
|
+
|
621
|
+
// Listener for refresh page button.
|
622
|
+
function refreshPage(event) {
|
623
|
+
event.preventDefault();
|
624
|
+
let url = window.location.href.replace(/\?.*/, "");
|
625
|
+
const filter = document.querySelector("#filter").value;
|
626
|
+
if (filter !== "") {
|
627
|
+
url += "?filter=" + escape(filter);
|
628
|
+
}
|
629
|
+
window.location = url;
|
630
|
+
}
|
631
|
+
|
632
|
+
// Attach event listener to one or more elements.
|
633
|
+
function addListener(elements, event, handler) {
|
634
|
+
if (elements.addEventListener) {
|
635
|
+
elements = [elements];
|
636
|
+
}
|
637
|
+
elements.forEach(function(element) {
|
638
|
+
if (element) {
|
639
|
+
element.addEventListener(event, handler);
|
640
|
+
}
|
641
|
+
});
|
642
|
+
}
|
643
|
+
|
644
|
+
// Bind event listeners for setting controls on a setting table row.
|
645
|
+
function bindSettingControlEvents(parent) {
|
646
|
+
addListener(parent.querySelectorAll(".js-remove-setting"), "click", removeSetting);
|
647
|
+
addListener(parent.querySelectorAll(".js-edit-setting"), "click", editSetting);
|
648
|
+
addListener(parent.querySelectorAll(".js-restore-setting"), "click", restoreSetting);
|
649
|
+
addListener(parent.querySelectorAll(".js-show-history"), "click", showHistoryModal);
|
650
|
+
addListener(parent.querySelectorAll(".js-no-op"), "click", noOp);
|
651
|
+
addListener(parent.querySelectorAll(".js-setting-value-type"), "change", changeSettingType);
|
652
|
+
addListener(parent.querySelectorAll(".js-date-input"), "change", changeDateTime);
|
653
|
+
addListener(parent.querySelectorAll(".js-time-input"), "change", changeDateTime);
|
654
|
+
}
|
655
|
+
|
656
|
+
// Initialize the table with all the settings plus any changes from a failed form submission.
|
657
|
+
function renderSettingsTable(settings) {
|
658
|
+
const tbody = document.querySelector("#settings-table tbody");
|
659
|
+
tbody.innerHTML = "";
|
660
|
+
let count = settings.length;
|
661
|
+
settings.forEach(function(setting) {
|
662
|
+
const randomId = "setting" + Math.floor((Math.random() * 0xFFFFFFFFFFFFFF)).toString(16);
|
663
|
+
setting.id = (setting.id || randomId);
|
664
|
+
const row = settingRow(setting);
|
665
|
+
tbody.appendChild(row);
|
666
|
+
bindSettingControlEvents(row);
|
667
|
+
});
|
668
|
+
document.querySelector(".js-settings-count").textContent = `${count} ${count === 1 ? "Setting" : "Settings"}`;
|
669
|
+
|
670
|
+
const filter = document.querySelector("#filter");
|
671
|
+
if (filter) {
|
672
|
+
filter.dispatchEvent(new Event("input"));
|
673
|
+
}
|
674
|
+
}
|
675
|
+
|
676
|
+
function promptUnsavedChanges(event) {
|
677
|
+
if (changesCount() > 0) {
|
678
|
+
return "Are you sure you want to leave?";
|
679
|
+
} else {
|
680
|
+
return undefined;
|
681
|
+
}
|
682
|
+
}
|
683
|
+
|
684
|
+
// Run the supplied function when the document has been marked ready.
|
685
|
+
function docReady(fn) {
|
686
|
+
if (document.readyState === "complete" || document.readyState === "interactive") {
|
687
|
+
setTimeout(fn, 1);
|
688
|
+
} else {
|
689
|
+
document.addEventListener("DOMContentLoaded", fn);
|
690
|
+
}
|
691
|
+
}
|
692
|
+
|
693
|
+
function fetchActiveSettings() {
|
694
|
+
SuperSettingsAPI.fetchSettings(function(settings_hash) {
|
695
|
+
const settings = settings_hash["settings"];
|
696
|
+
activeSettings = settings;
|
697
|
+
renderSettingsTable(settings);
|
698
|
+
enableSaveButton();
|
699
|
+
});
|
700
|
+
}
|
701
|
+
|
702
|
+
let activeSettings = [];
|
703
|
+
|
704
|
+
docReady(function() {
|
705
|
+
addListener(document.querySelector("#filter"), "input", filterListener);
|
706
|
+
addListener(document.querySelector("#add-setting"), "click", addSetting);
|
707
|
+
addListener(document.querySelector("#discard-changes"), "click", refreshPage);
|
708
|
+
addListener(document.querySelector("#save-settings"), "click", updateSettings);
|
709
|
+
addListener(document.querySelector("#modal"), "click", closeModal);
|
710
|
+
|
711
|
+
const queryParams = new URLSearchParams(window.location.search);
|
712
|
+
applyFilter(queryParams.get("filter"));
|
713
|
+
|
714
|
+
fetchActiveSettings();
|
715
|
+
|
716
|
+
window.onbeforeunload = promptUnsavedChanges;
|
717
|
+
})
|
718
|
+
})();
|