diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index c6d118a87b..705d110542 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -935,6 +935,7 @@ "name": "persistent_scripts", "type": "table", "label": "Persistent Scripts", + "content_setting": true, "help": "Add the URLs for scripts that you would like to ensure are always running in your domain.", "can_add_new_rows": true, "columns": [ @@ -1105,6 +1106,7 @@ "label": "Zones", "help": "In this table you can define a set of zones in which you can specify various audio properties.", "numbered": false, + "content_setting": true, "can_add_new_rows": true, "key": { "name": "name", @@ -1155,6 +1157,7 @@ "type": "table", "label": "Attenuation Coefficients", "help": "In this table you can set custom attenuation coefficients between audio zones", + "content_setting": true, "numbered": true, "can_order": true, "can_add_new_rows": true, @@ -1185,6 +1188,7 @@ "label": "Reverb Settings", "help": "In this table you can set reverb levels for audio zones. For a medium-sized (e.g., 100 square meter) meeting room, try a decay time of around 1.5 seconds and a wet/dry mix of 25%. For an airplane hangar or cathedral, try a decay time of 4 seconds and a wet/dry mix of 50%.", "numbered": true, + "content_setting": true, "can_add_new_rows": true, "columns": [ { diff --git a/domain-server/resources/web/base-settings-scripts.html b/domain-server/resources/web/base-settings-scripts.html new file mode 100644 index 0000000000..fe370c4675 --- /dev/null +++ b/domain-server/resources/web/base-settings-scripts.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/domain-server/resources/web/base-settings.html b/domain-server/resources/web/base-settings.html new file mode 100644 index 0000000000..13787b5fbe --- /dev/null +++ b/domain-server/resources/web/base-settings.html @@ -0,0 +1,51 @@ +
+
+
+ +
+
+ +
+ +
+ + +
+ +
+
+
+
+
diff --git a/domain-server/resources/web/content/index.shtml b/domain-server/resources/web/content/index.shtml index 0e48c1eff8..9b507f7826 100644 --- a/domain-server/resources/web/content/index.shtml +++ b/domain-server/resources/web/content/index.shtml @@ -1,45 +1,19 @@ -
-
-
- -
-
+ -
-
-
-
-

Upload Entities File

-
-
-
-

- Upload an entities file (e.g.: models.json.gz) to replace the content of this domain.
- Note: Your domain's content will be replaced by the content you upload, but the backup files of your domain's content will not immediately be changed. -

-

If your domain has any content that you would like to re-use at a later date, save a manual backup of your models.json.gz file, which is usually stored at the following paths:

- -
C:/Users/[username]/AppData/Roaming/High Fidelity/assignment-client/entities/models.json.gz
- -
/Users/[username]/Library/Application Support/High Fidelity/assignment-client/entities/models.json.gz
- -
/home/[username]/.local/share/High Fidelity/assignment-client/entities/models.json.gz
-
- -
-
- -
-
-
-
-
+ - - + + + + + diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 2e6e084164..7b2ed37b15 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -1,5 +1,7 @@ $(document).ready(function(){ + Settings.afterReloadActions = function() {}; + function showSpinnerAlert(title) { swal({ title: title, diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 158008fc2b..22de75a778 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -18,6 +18,14 @@ body { margin-top: 70px; } +/* unfortunate hack so that anchors go to the right place with fixed navbar */ +:target:before { + content: " "; + display: block; + height: 70px; + margin-top: -70px; +} + [hidden] { display: none !important; } @@ -345,3 +353,75 @@ table .headers + .headers td { text-align: center; margin-top: 20px; } + +@media (min-width: 768px) { + ul.nav li.dropdown-on-hover:hover ul.dropdown-menu { + display: block; + } +} + +ul.nav li.dropdown ul.dropdown-menu { + padding: 0px 0px; +} + +ul.nav li.dropdown li a { + padding-top: 7px; + padding-bottom: 7px; +} + +ul.nav li.dropdown li a:hover { + color: white; + background-color: #337ab7; +} + +ul.nav li.dropdown ul.dropdown-menu .divider { + margin: 0px 0; +} + +#visit-domain-link { + background-color: transparent; +} + +.navbar-btn { + margin-left: 10px; +} + +#save-settings-xs-button { + float: right; + margin-right: 10px; +} + +#button-bars { + display: inline-block; + float: left; +} + +#hamburger-badge { + position: relative; + top: -2px; + float: left; + margin-right: 10px; + margin-left: 0px; +} + +#restart-server { + margin-left: 0px; +} + +#restart-server:hover { + text-decoration: none; +} + +.badge { + margin-left: 5px; +} + +.panel-title { + display: inline-block; +} + +#visit-hmd-icon { + width: 25px; + position: relative; + top: -1px; +} diff --git a/domain-server/resources/web/header.html b/domain-server/resources/web/header.html index 1e32e9f02f..aff82d557e 100644 --- a/domain-server/resources/web/header.html +++ b/domain-server/resources/web/header.html @@ -17,30 +17,47 @@
- diff --git a/domain-server/resources/web/images/hmd-w-eyes.svg b/domain-server/resources/web/images/hmd-w-eyes.svg new file mode 100644 index 0000000000..c100de2f4e --- /dev/null +++ b/domain-server/resources/web/images/hmd-w-eyes.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/domain-server/resources/web/js/base-settings.js b/domain-server/resources/web/js/base-settings.js new file mode 100644 index 0000000000..5e32463d61 --- /dev/null +++ b/domain-server/resources/web/js/base-settings.js @@ -0,0 +1,1140 @@ +var DomainInfo = null; + +var viewHelpers = { + getFormGroup: function(keypath, setting, values, isAdvanced) { + form_group = "
"; + setting_value = _(values).valueForKeyPath(keypath); + + if (_.isUndefined(setting_value) || _.isNull(setting_value)) { + if (_.has(setting, 'default')) { + setting_value = setting.default; + } else { + setting_value = ""; + } + } + + label_class = 'control-label'; + + function common_attrs(extra_classes) { + extra_classes = (!_.isUndefined(extra_classes) ? extra_classes : ""); + return " class='" + (setting.type !== 'checkbox' ? 'form-control' : '') + + " " + Settings.TRIGGER_CHANGE_CLASS + " " + extra_classes + "' data-short-name='" + + setting.name + "' name='" + keypath + "' " + + "id='" + (!_.isUndefined(setting.html_id) ? setting.html_id : keypath) + "'"; + } + + if (setting.type === 'checkbox') { + if (setting.label) { + form_group += "" + } + + form_group += "
" + form_group += "" + + if (setting.help) { + form_group += "" + setting.help + ""; + } + + form_group += "
" + } else { + input_type = _.has(setting, 'type') ? setting.type : "text" + + if (setting.label) { + form_group += ""; + } + + if (input_type === 'table') { + form_group += makeTable(setting, keypath, setting_value) + } else { + if (input_type === 'select') { + form_group += "" + + form_group += "" + } else if (input_type === 'button') { + // Is this a button that should link to something directly? + // If so, we use an anchor tag instead of a button tag + + if (setting.href) { + form_group += "" + + setting.button_label + ""; + } else { + form_group += ""; + } + + } else { + + if (input_type == 'integer') { + input_type = "text" + } + + form_group += "" + } + + form_group += "" + setting.help + "" + } + } + + form_group += "
" + return form_group + } +} + +function reloadSettings(callback) { + $.getJSON(Settings.endpoint, function(data){ + _.extend(data, viewHelpers) + + $('#panels').html(Settings.panelsTemplate(data)) + + Settings.data = data; + Settings.initialValues = form2js('settings-form', ".", false, cleanupFormValues, true); + + Settings.afterReloadActions(); + + // setup any bootstrap switches + $('.toggle-checkbox').bootstrapSwitch(); + + $('[data-toggle="tooltip"]').tooltip(); + + // call the callback now that settings are loaded + callback(true); + }).fail(function() { + // call the failure object since settings load faild + callback(false) + }); +} + +function postSettings(jsonSettings) { + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved + $.ajax(Settings.endpoint, { + data: JSON.stringify(jsonSettings), + contentType: 'application/json', + type: 'POST' + }).done(function(data){ + if (data.status == "success") { + if ($(".save-button").html() === SAVE_BUTTON_LABEL_RESTART) { + showRestartModal(); + } else { + location.reload(true); + } + } else { + showErrorMessage("Error", SETTINGS_ERROR_MESSAGE) + reloadSettings(); + } + }).fail(function(){ + showErrorMessage("Error", SETTINGS_ERROR_MESSAGE) + reloadSettings(); + }); +} + +$(document).ready(function(){ + + $('.save-button.navbar-btn').show(); + + $.ajaxSetup({ + timeout: 20000, + }); + + reloadSettings(function(success){ + if (success) { + // if there was a hash in the URL, jump to it now + if (location.hash) { + location.href = location.hash; + } + } else { + swal({ + title: '', + type: 'error', + text: Strings.LOADING_SETTINGS_ERROR + }); + } + }); + + $('#' + Settings.FORM_ID).on('click', '.' + Settings.ADD_ROW_BUTTON_CLASS, function(){ + addTableRow($(this).closest('tr')); + }); + + $('#' + Settings.FORM_ID).on('click', '.' + Settings.DEL_ROW_BUTTON_CLASS, function(){ + deleteTableRow($(this).closest('tr')); + }); + + $('#' + Settings.FORM_ID).on('click', '.' + Settings.ADD_CATEGORY_BUTTON_CLASS, function(){ + addTableCategory($(this).closest('tr')); + }); + + $('#' + Settings.FORM_ID).on('click', '.' + Settings.DEL_CATEGORY_BUTTON_CLASS, function(){ + deleteTableCategory($(this).closest('tr')); + }); + + $('#' + Settings.FORM_ID).on('click', '.' + Settings.TOGGLE_CATEGORY_COLUMN_CLASS, function(){ + toggleTableCategory($(this).closest('tr')); + }); + + $('#' + Settings.FORM_ID).on('click', '.' + Settings.MOVE_UP_BUTTON_CLASS, function(){ + moveTableRow($(this).closest('tr'), true); + }); + + $('#' + Settings.FORM_ID).on('click', '.' + Settings.MOVE_DOWN_BUTTON_CLASS, function(){ + moveTableRow($(this).closest('tr'), false); + }); + + $('#' + Settings.FORM_ID).on('keyup', function(e){ + var $target = $(e.target); + if (e.keyCode == 13) { + if ($target.is('table input')) { + // capture enter in table input + // if we have a sibling next to us that has an input, jump to it, otherwise check if we have a glyphicon for add to click + sibling = $target.parent('td').next(); + + if (sibling.hasClass(Settings.DATA_COL_CLASS)) { + // set focus to next input + sibling.find('input').focus(); + } else { + + // jump over the re-order row, if that's what we're on + if (sibling.hasClass(Settings.REORDER_BUTTONS_CLASS)) { + sibling = sibling.next(); + } + + // for tables with categories we add the entry and setup the new row on enter + if (sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).length) { + sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).click(); + + // set focus to the first input in the new row + $target.closest('table').find('tr.inputs input:first').focus(); + } + + var tableRows = sibling.parent(); + var tableBody = tableRows.parent(); + + // if theres no more siblings, we should jump to a new row + if (sibling.next().length == 0 && tableRows.nextAll().length == 1) { + tableBody.find("." + Settings.ADD_ROW_BUTTON_CLASS).click(); + } + } + + } else if ($target.is('input')) { + $target.change().blur(); + } + + e.preventDefault(); + } + }); + + $('#' + Settings.FORM_ID).on('keypress', function(e){ + if (e.keyCode == 13) { + + e.preventDefault(); + } + }); + + $('#' + Settings.FORM_ID).on('change keyup paste', '.' + Settings.TRIGGER_CHANGE_CLASS , function(){ + // this input was changed, add the changed data attribute to it + $(this).attr('data-changed', true); + + badgeForDifferences($(this)); + }); + + $('#' + Settings.FORM_ID).on('switchChange.bootstrapSwitch', 'input.toggle-checkbox', function(){ + // this checkbox was changed, add the changed data attribute to it + $(this).attr('data-changed', true); + + badgeForDifferences($(this)); + }); + + // Bootstrap switch in table + $('#' + Settings.FORM_ID).on('change', 'input.table-checkbox', function () { + // Bootstrap switches in table: set the changed data attribute for all rows in table. + var row = $(this).closest('tr'); + if (row.hasClass("value-row")) { // Don't set attribute on input row switches prior to it being added to table. + row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true); + updateDataChangedForSiblingRows(row, true); + badgeForDifferences($(this)); + } + }); + + $('#' + Settings.FORM_ID).on('change', 'input.table-time', function() { + // Bootstrap switches in table: set the changed data attribute for all rows in table. + var row = $(this).closest('tr'); + if (row.hasClass("value-row")) { // Don't set attribute on input row switches prior to it being added to table. + row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true); + updateDataChangedForSiblingRows(row, true); + badgeForDifferences($(this)); + } + }); + + $('#' + Settings.FORM_ID).on('change', 'select', function(){ + $("input[name='" + $(this).attr('data-hidden-input') + "']").val($(this).val()).change(); + }); + + var panelsSource = $('#panels-template').html(); + Settings.panelsTemplate = _.template(panelsSource); +}); + +function dynamicButton(button_id, text) { + return $(""); +} + +function showSpinnerAlert(title) { + swal({ + title: title, + text: '
', + html: true, + showConfirmButton: false, + allowEscapeKey: false + }); +} + +function parseJSONResponse(xhr) { + try { + return JSON.parse(xhr.responseText); + } catch (e) { + } + return null; +} + +function validateInputs() { + // check if any new values are bad + var tables = $('table'); + + var inputsValid = true; + + var tables = $('table'); + + // clear any current invalid rows + $('tr.' + Settings.INVALID_ROW_CLASS).removeClass(Settings.INVALID_ROW_CLASS); + + function markParentRowInvalid(rowChild) { + $(rowChild).closest('tr').addClass(Settings.INVALID_ROW_CLASS); + } + + _.each(tables, function(table) { + // validate keys specificially for spaces and equality to an existing key + var newKeys = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ' td.key'); + + var keyWithSpaces = false; + var empty = false; + var duplicateKey = false; + + _.each(newKeys, function(keyCell) { + var keyVal = $(keyCell).children('input').val(); + + if (keyVal.indexOf(' ') !== -1) { + keyWithSpaces = true; + markParentRowInvalid(keyCell); + return; + } + + // make sure the key isn't empty + if (keyVal.length === 0) { + empty = true + + markParentRowInvalid(input); + return; + } + + // make sure we don't have duplicate keys in the table + var otherKeys = $(table).find('td.key').not(keyCell); + _.each(otherKeys, function(otherKeyCell) { + var keyInput = $(otherKeyCell).children('input'); + + if (keyInput.length) { + if ($(keyInput).val() == keyVal) { + duplicateKey = true; + } + } else if ($(otherKeyCell).html() == keyVal) { + duplicateKey = true; + } + + if (duplicateKey) { + markParentRowInvalid(keyCell); + return; + } + }); + + }); + + if (keyWithSpaces) { + showErrorMessage("Error", "Key contains spaces"); + inputsValid = false; + return + } + + if (empty) { + showErrorMessage("Error", "Empty field(s)"); + inputsValid = false; + return + } + + if (duplicateKey) { + showErrorMessage("Error", "Two keys cannot be identical"); + inputsValid = false; + return; + } + }); + + return inputsValid; +} + +var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!"; + +function saveSettings() { + + if (validateInputs()) { + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved + var canPost = true; + + // disable any inputs not changed + $("input:not([data-changed])").each(function () { + $(this).prop('disabled', true); + }); + + // grab a JSON representation of the form via form2js + var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); + + // re-enable all inputs + $("input").each(function () { + $(this).prop('disabled', false); + }); + + // remove focus from the button + $(this).blur(); + + if (Settings.handlePostSettings === undefined) { + console.log("----- saveSettings() called ------"); + console.log(formJSON); + + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved + postSettings(formJSON); + } else { + Settings.handlePostSettings(formJSON) + } + } +} + +$('body').on('click', '.save-button', function(e){ + saveSettings(); + return false; +}); + +function makeTable(setting, keypath, setting_value) { + var isArray = !_.has(setting, 'key'); + var categoryKey = setting.categorize_by_key; + var isCategorized = !!categoryKey && isArray; + + if (!isArray && setting.can_order) { + setting.can_order = false; + } + + var html = ""; + + if (setting.help) { + html += "" + setting.help + "" + } + + var nonDeletableRowKey = setting["non-deletable-row-key"]; + var nonDeletableRowValues = setting["non-deletable-row-values"]; + + html += ""; + + if (setting.caption) { + html += "" + } + + // Column groups + if (setting.groups) { + html += "" + _.each(setting.groups, function (group) { + html += "" + }) + if (!setting.read_only) { + if (setting.can_order) { + html += ""; + } + html += "" + } + html += "" + } + + // Column names + html += "" + + if (setting.numbered === true) { + html += "" // Row number + } + + if (setting.key) { + html += "" // Key + } + + var numVisibleColumns = 0; + _.each(setting.columns, function(col) { + if (!col.hidden) numVisibleColumns++; + html += "" // Data + }) + + if (!setting.read_only) { + if (setting.can_order) { + numVisibleColumns++; + html += ""; + } + numVisibleColumns++; + html += ""; + } + + // populate rows in the table from existing values + var row_num = 1; + + if (keypath.length > 0 && _.size(setting_value) > 0) { + var rowIsObject = setting.columns.length > 1; + + _.each(setting_value, function(row, rowIndexOrName) { + var categoryPair = {}; + var categoryValue = ""; + if (isCategorized) { + categoryValue = rowIsObject ? row[categoryKey] : row; + categoryPair[categoryKey] = categoryValue; + if (_.findIndex(setting_value, categoryPair) === rowIndexOrName) { + html += makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns, setting.can_add_new_categories, ""); + } + } + + html += ""; + + if (setting.numbered === true) { + html += "" + } + + if (setting.key) { + html += "" + } + + var isNonDeletableRow = !setting.can_add_new_rows; + + _.each(setting.columns, function(col) { + + var colValue, colName; + if (isArray) { + colValue = rowIsObject ? row[col.name] : row; + colName = keypath + "[" + rowIndexOrName + "]" + (rowIsObject ? "." + col.name : ""); + } else { + colValue = row[col.name]; + colName = keypath + "." + rowIndexOrName + "." + col.name; + } + + isNonDeletableRow = isNonDeletableRow + || (nonDeletableRowKey === col.name && nonDeletableRowValues.indexOf(colValue) !== -1); + + if (isArray && col.type === "checkbox" && col.editable) { + html += + ""; + } else if (isArray && col.type === "time" && col.editable) { + html += + ""; + } else { + // Use a hidden input so that the values are posted. + html += + ""; + } + + }); + + if (!setting.read_only) { + if (setting.can_order) { + html += "" + } + if (isNonDeletableRow) { + html += ""; + } else { + html += ""; + } + } + + html += "" + + if (isCategorized && setting.can_add_new_rows && _.findLastIndex(setting_value, categoryPair) === rowIndexOrName) { + html += makeTableInputs(setting, categoryPair, categoryValue); + } + + row_num++ + }); + } + + // populate inputs in the table for new values + if (!setting.read_only) { + if (setting.can_add_new_categories) { + html += makeTableCategoryInput(setting, numVisibleColumns); + } + + if (setting.can_add_new_rows || setting.can_add_new_categories) { + html += makeTableHiddenInputs(setting, {}, ""); + } + } + html += "
" + setting.caption + "
" + group.label + "
#" + setting.key.label + "" + col.label + "
" + row_num + "" + rowIndexOrName + "" + + "" + + "" + + "" + + "" + + colValue + + "" + + "" + + "
" + + return html; +} + +function makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns, canRemove, message) { + var html = + "" + + "" + + "" + + "" + categoryValue + "" + + "" + + ((canRemove) ? ( + "" + + "" + + "" + ) : ( + "" + )) + + ""; + return html; +} + +function makeTableHiddenInputs(setting, initialValues, categoryValue) { + var html = ""; + + if (setting.numbered === true) { + html += ""; + } + + if (setting.key) { + html += "\ + \ + " + } + + _.each(setting.columns, function(col) { + var defaultValue = _.has(initialValues, col.name) ? initialValues[col.name] : col.default; + if (col.type === "checkbox") { + html += + "" + + "" + + ""; + } else if (col.type === "select") { + html += "" + html += ""; + html += ""; + } else { + html += + "" + + "" + + ""; + } + }) + + if (setting.can_order) { + html += "" + } + html += "" + html += "" + + return html +} + +function makeTableCategoryInput(setting, numVisibleColumns) { + var canAddRows = setting.can_add_new_rows; + var categoryKey = setting.categorize_by_key; + var placeholder = setting.new_category_placeholder || ""; + var message = setting.new_category_message || ""; + var html = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + return html; +} + +function getDescriptionForKey(key) { + for (var i in Settings.data.descriptions) { + if (Settings.data.descriptions[i].name === key) { + return Settings.data.descriptions[i]; + } + } +} + +var SAVE_BUTTON_LABEL_SAVE = "Save"; +var SAVE_BUTTON_LABEL_RESTART = "Save and restart"; +var reasonsForRestart = []; +var numChangesBySection = {}; + +function badgeForDifferences(changedElement) { + // figure out which group this input is in + var panelParentID = changedElement.closest('.panel').attr('id'); + + // if the panel contains non-grouped settings, the initial value is Settings.initialValues + var isGrouped = $('#' + panelParentID).hasClass('grouped'); + + if (isGrouped) { + var initialPanelJSON = Settings.initialValues[panelParentID] + ? Settings.initialValues[panelParentID] + : {}; + + // get a JSON representation of that section + var panelJSON = form2js(panelParentID, ".", false, cleanupFormValues, true)[panelParentID]; + } else { + var initialPanelJSON = Settings.initialValues; + + // get a JSON representation of that section + var panelJSON = form2js(panelParentID, ".", false, cleanupFormValues, true); + } + + var badgeValue = 0 + var description = getDescriptionForKey(panelParentID); + + // badge for any settings we have that are not the same or are not present in initialValues + for (var setting in panelJSON) { + if ((!_.has(initialPanelJSON, setting) && panelJSON[setting] !== "") || + (!_.isEqual(panelJSON[setting], initialPanelJSON[setting]) + && (panelJSON[setting] !== "" || _.has(initialPanelJSON, setting)))) { + badgeValue += 1; + + // add a reason to restart + if (description && description.restart != false) { + reasonsForRestart.push(setting); + } + } else { + // remove a reason to restart + if (description && description.restart != false) { + reasonsForRestart = $.grep(reasonsForRestart, function(v) { return v != setting; }); + } + } + } + + // update the list-group-item badge to have the new value + if (badgeValue == 0) { + badgeValue = "" + } + + numChangesBySection[panelParentID] = badgeValue; + + var hasChanges = badgeValue > 0; + + if (!hasChanges) { + for (var key in numChangesBySection) { + if (numChangesBySection[key] > 0) { + hasChanges = true; + break; + } + } + } + + $(".save-button").prop("disabled", !hasChanges); + $(".save-button").html(reasonsForRestart.length > 0 ? SAVE_BUTTON_LABEL_RESTART : SAVE_BUTTON_LABEL_SAVE); + + // add the badge to the navbar item and the panel header + $("a[href='" + settingsGroupAnchor(Settings.path, panelParentID) + "'] .badge").html(badgeValue); + $("#" + panelParentID + " .panel-heading .badge").html(badgeValue); + + // make the navbar dropdown show a badge that is the total of the badges of all groups + var totalChanges = 0; + $('.panel-heading .badge').each(function(index){ + if (this.innerHTML.length > 0) { + totalChanges += parseInt(this.innerHTML); + } + }); + + if (totalChanges == 0) { + totalChanges = "" + } + + var totalBadgeClass = Settings.content_settings ? '.content-settings-badge' : '.domain-settings-badge'; + + $(totalBadgeClass).html(totalChanges); + $('#hamburger-badge').html(totalChanges); +} + +function addTableRow(row) { + var table = row.parents('table'); + var isArray = table.data('setting-type') === 'array'; + var keepField = row.data("keep-field"); + + var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS); + + var input_clone = row.clone(); + + // Change input row to data row + var table = row.parents("table"); + var setting_name = table.attr("name"); + row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS); + + if (!isArray) { + // show the key input + var keyInput = row.children(".key").children("input"); + + // whenever the keyInput changes, re-badge for differences + keyInput.on('change keyup paste', function(){ + // update siblings in the row to have the correct name + var currentKey = $(this).val(); + + $(this).closest('tr').find('.value-col input').each(function(index){ + var input = $(this); + + if (currentKey.length > 0) { + input.attr("name", setting_name + "." + currentKey + "." + input.parent().attr('name')); + } else { + input.removeAttr("name"); + } + }) + + badgeForDifferences($(this)); + }); + } + + // if this is an array, add the row index (which is the index of the last row + 1) + // as a data attribute to the row + var row_index = 0; + if (isArray) { + var previous_row = row.siblings('.' + Settings.DATA_ROW_CLASS + ':last'); + + if (previous_row.length > 0) { + row_index = parseInt(previous_row.attr(Settings.DATA_ROW_INDEX), 10) + 1; + } else { + row_index = 0; + } + + row.attr(Settings.DATA_ROW_INDEX, row_index); + } + + var focusChanged = false; + + _.each(row.children(), function(element) { + if ($(element).hasClass("numbered")) { + // Index row + var numbers = columns.children(".numbered") + if (numbers.length > 0) { + $(element).html(parseInt(numbers.last().text()) + 1) + } else { + $(element).html(1) + } + } else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) { + $(element).html("") + } else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { + // Change buttons + var anchor = $(element).children("a") + anchor.removeClass(Settings.ADD_ROW_SPAN_CLASSES) + anchor.addClass(Settings.DEL_ROW_SPAN_CLASSES) + } else if ($(element).hasClass("key")) { + var input = $(element).children("input") + input.show(); + } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) { + // show inputs + var input = $(element).find("input"); + input.show(); + + var isCheckbox = input.hasClass("table-checkbox"); + var isDropdown = input.hasClass("table-dropdown"); + + if (isArray) { + var key = $(element).attr('name'); + + // are there multiple columns or just one? + // with multiple we have an array of Objects, with one we have an array of whatever the value type is + var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length + var newName = setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""); + + input.attr("name", newName); + + if (isDropdown) { + // default values for hidden inputs inside child selects gets cleared so we need to remind it + var selectElement = $(element).children("select"); + selectElement.attr("data-hidden-input", newName); + $(element).children("input").val(selectElement.val()); + } + } + + if (isArray && !focusChanged) { + input.focus(); + focusChanged = true; + } + + // if we are adding a dropdown, we should go ahead and make its select + // element is visible + if (isDropdown) { + $(element).children("select").attr("style", ""); + } + + if (isCheckbox) { + $(input).find("input").attr("data-changed", "true"); + } else { + input.attr("data-changed", "true"); + } + } else { + console.log("Unknown table element"); + } + }); + + if (!isArray) { + keyInput.focus(); + } + + input_clone.children('td').each(function () { + if ($(this).attr("name") !== keepField) { + $(this).find("input").val($(this).children('input').attr('data-default')); + } + }); + + if (isArray) { + updateDataChangedForSiblingRows(row, true) + + // the addition of any table row should remove the empty-array-row + row.siblings('.empty-array-row').remove() + } + + badgeForDifferences($(table)) + + row.after(input_clone) +} + +function deleteTableRow($row) { + var $table = $row.closest('table'); + var categoryName = $row.data("category"); + var isArray = $table.data('setting-type') === 'array'; + + $row.empty(); + + if (!isArray) { + if ($row.attr('name')) { + $row.html(""); + } else { + // for rows that didn't have a key, simply remove the row + $row.remove(); + } + } else { + if ($table.find('.' + Settings.DATA_ROW_CLASS + "[data-category='" + categoryName + "']").length <= 1) { + // This is the last row of the category, so delete the header + $table.find('.' + Settings.DATA_CATEGORY_CLASS + "[data-category='" + categoryName + "']").remove(); + } + + if ($table.find('.' + Settings.DATA_ROW_CLASS).length > 1) { + updateDataChangedForSiblingRows($row); + + // this isn't the last row - we can just remove it + $row.remove(); + } else { + // this is the last row, we can't remove it completely since we need to post an empty array + $row + .removeClass(Settings.DATA_ROW_CLASS) + .removeClass(Settings.NEW_ROW_CLASS) + .removeAttr("data-category") + .addClass('empty-array-row') + .html(""); + } + } + + // we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated + badgeForDifferences($table); +} + +function addTableCategory($categoryInputRow) { + var $input = $categoryInputRow.find("input").first(); + var categoryValue = $input.prop("value"); + if (!categoryValue || $categoryInputRow.closest("table").find("tr[data-category='" + categoryValue + "']").length !== 0) { + $categoryInputRow.addClass("has-warning"); + + setTimeout(function () { + $categoryInputRow.removeClass("has-warning"); + }, 400); + + return; + } + + var $rowInput = $categoryInputRow.next(".inputs").clone(); + if (!$rowInput) { + console.error("Error cloning inputs"); + } + + var canAddRows = $categoryInputRow.data("can-add-rows"); + var message = $categoryInputRow.data("message"); + var categoryKey = $categoryInputRow.data("key"); + var width = 0; + $categoryInputRow + .children("td") + .each(function () { + width += $(this).prop("colSpan") || 1; + }); + + $input + .prop("value", "") + .focus(); + + $rowInput.find("td[name='" + categoryKey + "'] > input").first() + .prop("value", categoryValue); + $rowInput + .attr("data-category", categoryValue) + .addClass(Settings.NEW_ROW_CLASS); + + var $newCategoryRow = $(makeTableCategoryHeader(categoryKey, categoryValue, width, true, " - " + message)); + $newCategoryRow.addClass(Settings.NEW_ROW_CLASS); + + $categoryInputRow + .before($newCategoryRow) + .before($rowInput); + + if (canAddRows) { + $rowInput.removeAttr("hidden"); + } else { + addTableRow($rowInput); + } +} + +function deleteTableCategory($categoryHeaderRow) { + var categoryName = $categoryHeaderRow.data("category"); + + $categoryHeaderRow + .closest("table") + .find("tr[data-category='" + categoryName + "']") + .each(function () { + if ($(this).hasClass(Settings.DATA_ROW_CLASS)) { + deleteTableRow($(this)); + } else { + $(this).remove(); + } + }); +} + +function toggleTableCategory($categoryHeaderRow) { + var $icon = $categoryHeaderRow.find("." + Settings.TOGGLE_CATEGORY_SPAN_CLASS).first(); + var categoryName = $categoryHeaderRow.data("category"); + var wasExpanded = $icon.hasClass(Settings.TOGGLE_CATEGORY_EXPANDED_CLASS); + if (wasExpanded) { + $icon + .addClass(Settings.TOGGLE_CATEGORY_CONTRACTED_CLASS) + .removeClass(Settings.TOGGLE_CATEGORY_EXPANDED_CLASS); + } else { + $icon + .addClass(Settings.TOGGLE_CATEGORY_EXPANDED_CLASS) + .removeClass(Settings.TOGGLE_CATEGORY_CONTRACTED_CLASS); + } + $categoryHeaderRow + .closest("table") + .find("tr[data-category='" + categoryName + "']") + .toggleClass("contracted", wasExpanded); +} + +function moveTableRow(row, move_up) { + var table = $(row).closest('table') + var isArray = table.data('setting-type') === 'array' + if (!isArray) { + return; + } + + if (move_up) { + var prev_row = row.prev() + if (prev_row.hasClass(Settings.DATA_ROW_CLASS)) { + prev_row.before(row) + } + } else { + var next_row = row.next() + if (next_row.hasClass(Settings.DATA_ROW_CLASS)) { + next_row.after(row) + } + } + + // we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated + badgeForDifferences($(table)) +} + +function updateDataChangedForSiblingRows(row, forceTrue) { + // anytime a new row is added to an array we need to set data-changed for all sibling row inputs to true + // unless it matches the inital set of values + + if (!forceTrue) { + // figure out which group this row is in + var panelParentID = row.closest('.panel').attr('id') + // get the short name for the setting from the table + var tableShortName = row.closest('table').data('short-name') + + // get a JSON representation of that section + var panelSettingJSON = form2js(panelParentID, ".", false, cleanupFormValues, true)[panelParentID][tableShortName] + if (Settings.initialValues[panelParentID]) { + var initialPanelSettingJSON = Settings.initialValues[panelParentID][tableShortName] + } else { + var initialPanelSettingJSON = {}; + } + + // if they are equal, we don't need data-changed + isTrue = !_.isEqual(panelSettingJSON, initialPanelSettingJSON) + } else { + isTrue = true + } + + row.siblings('.' + Settings.DATA_ROW_CLASS).each(function(){ + var hiddenInput = $(this).find('td.' + Settings.DATA_COL_CLASS + ' input') + if (isTrue) { + hiddenInput.attr('data-changed', isTrue) + } else { + hiddenInput.removeAttr('data-changed') + } + }) +} + +function cleanupFormValues(node) { + if (node.type && node.type === 'checkbox') { + return { name: node.name, value: node.checked ? true : false }; + } else { + return false; + } +} + +function showErrorMessage(title, message) { + swal(title, message) +} diff --git a/domain-server/resources/web/settings/js/bootstrap-switch.min.js b/domain-server/resources/web/js/bootstrap-switch.min.js similarity index 100% rename from domain-server/resources/web/settings/js/bootstrap-switch.min.js rename to domain-server/resources/web/js/bootstrap-switch.min.js diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index ad1509b038..aa658bce3f 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -23,16 +23,22 @@ function showRestartModal() { }, 1000); } +function settingsGroupAnchor(base, html_id) { + return base + "#" + html_id + "_group" +} + $(document).ready(function(){ var url = window.location; + // Will only work if string in href matches with location $('ul.nav a[href="'+ url +'"]').parent().addClass('active'); // Will also work for relative and absolute hrefs $('ul.nav a').filter(function() { return this.href == url; - }).parent().addClass('active'); - $('body').on('click', '#restart-server', function(e) { + }).parent().addClass('active'); + + $('body').on('click', '#restart-server', function(e) { swal( { title: "Are you sure?", text: "This will restart your domain server, causing your domain to be briefly offline.", @@ -45,4 +51,35 @@ $(document).ready(function(){ }); return false; }); + + var $contentDropdown = $('#content-settings-nav-dropdown'); + var $settingsDropdown = $('#domain-settings-nav-dropdown'); + + // for pages that have the settings dropdowns + if ($contentDropdown.length && $settingsDropdown.length) { + // make a JSON request to get the dropdown menus for content and settings + // we don't error handle here because the top level menu is still clickable and usables if this fails + $.getJSON('/settings-menu-groups.json', function(data){ + function makeGroupDropdownElement(group, base) { + var html_id = group.html_id ? group.html_id : group.name; + return "
  • " + group.label + "
  • "; + } + + $.each(data.content_settings, function(index, group){ + if (index > 0) { + $contentDropdown.append(""); + } + + $contentDropdown.append(makeGroupDropdownElement(group, "/content/")); + }); + + $.each(data.domain_settings, function(index, group){ + if (index > 0) { + $settingsDropdown.append(""); + } + + $settingsDropdown.append(makeGroupDropdownElement(group, "/settings/")); + }); + }); + } }); diff --git a/domain-server/resources/web/settings/js/form2js.min.js b/domain-server/resources/web/js/form2js.min.js similarity index 100% rename from domain-server/resources/web/settings/js/form2js.min.js rename to domain-server/resources/web/js/form2js.min.js diff --git a/domain-server/resources/web/js/shared.js b/domain-server/resources/web/js/shared.js index 00f699fa4e..e1870a2fa8 100644 --- a/domain-server/resources/web/js/shared.js +++ b/domain-server/resources/web/js/shared.js @@ -1,6 +1,4 @@ -var Settings = { - showAdvanced: false, - ADVANCED_CLASS: 'advanced-setting', +Object.assign(Settings, { DEPRECATED_CLASS: 'deprecated-setting', TRIGGER_CHANGE_CLASS: 'trigger-change', DATA_ROW_CLASS: 'value-row', @@ -41,7 +39,7 @@ var Settings = { FORM_ID: 'settings-form', INVALID_ROW_CLASS: 'invalid-input', DATA_ROW_INDEX: 'data-row-index' -}; +}); var URLs = { // STABLE METAVERSE_URL: https://metaverse.highfidelity.com diff --git a/domain-server/resources/web/js/underscore-min.js b/domain-server/resources/web/js/underscore-min.js index f01025b7bc..3c3eec027b 100644 --- a/domain-server/resources/web/js/underscore-min.js +++ b/domain-server/resources/web/js/underscore-min.js @@ -3,4 +3,3 @@ // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. (function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); -//# sourceMappingURL=underscore-min.map \ No newline at end of file diff --git a/domain-server/resources/web/settings/index.shtml b/domain-server/resources/web/settings/index.shtml index d36330375a..d71692523a 100644 --- a/domain-server/resources/web/settings/index.shtml +++ b/domain-server/resources/web/settings/index.shtml @@ -1,104 +1,21 @@ -
    -
    -
    - -
    -
    + -
    -
    -
    - - - - - - - -
    -
    - -
    -
    - - - -
    - - -
    -
    -
    -
    - - -
    + - - - + - - - - + + + + + diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 3faeff4482..9a31b766a6 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -1,1262 +1,20 @@ -var DomainInfo = null; - -var viewHelpers = { - getFormGroup: function(keypath, setting, values, isAdvanced) { - form_group = "
    "; - setting_value = _(values).valueForKeyPath(keypath); - - if (_.isUndefined(setting_value) || _.isNull(setting_value)) { - if (_.has(setting, 'default')) { - setting_value = setting.default; - } else { - setting_value = ""; - } - } - - label_class = 'control-label'; - - function common_attrs(extra_classes) { - extra_classes = (!_.isUndefined(extra_classes) ? extra_classes : ""); - return " class='" + (setting.type !== 'checkbox' ? 'form-control' : '') - + " " + Settings.TRIGGER_CHANGE_CLASS + " " + extra_classes + "' data-short-name='" - + setting.name + "' name='" + keypath + "' " - + "id='" + (!_.isUndefined(setting.html_id) ? setting.html_id : keypath) + "'"; - } - - if (setting.type === 'checkbox') { - if (setting.label) { - form_group += "" - } - - form_group += "
    " - form_group += "" - - if (setting.help) { - form_group += "" + setting.help + ""; - } - - form_group += "
    " - } else { - input_type = _.has(setting, 'type') ? setting.type : "text" - - if (setting.label) { - form_group += ""; - } - - if (input_type === 'table') { - form_group += makeTable(setting, keypath, setting_value) - } else { - if (input_type === 'select') { - form_group += "" - - form_group += "" - } else if (input_type === 'button') { - // Is this a button that should link to something directly? - // If so, we use an anchor tag instead of a button tag - - if (setting.href) { - form_group += "" - + setting.button_label + ""; - } else { - form_group += ""; - } - - } else { - - if (input_type == 'integer') { - input_type = "text" - } - - form_group += "" - } - - form_group += "" + setting.help + "" - } - } - - form_group += "
    " - return form_group - } -} - -var qs = (function(a) { - if (a == "") return {}; - var b = {}; - for (var i = 0; i < a.length; ++i) - { - var p=a[i].split('=', 2); - if (p.length == 1) { - b[p[0]] = ""; - } else { - b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " ")); - } - } - return b; -})(window.location.search.substr(1).split('&')); - $(document).ready(function(){ - /* - * Clamped-width. - * Usage: - *
    This long content will force clamped width
    - * - * Author: LV - */ - - $.ajaxSetup({ - timeout: 20000, - }); - - $('[data-clampedwidth]').each(function () { - var elem = $(this); - var parentPanel = elem.data('clampedwidth'); - var resizeFn = function () { - var sideBarNavWidth = $(parentPanel).width() - parseInt(elem.css('paddingLeft')) - parseInt(elem.css('paddingRight')) - parseInt(elem.css('marginLeft')) - parseInt(elem.css('marginRight')) - parseInt(elem.css('borderLeftWidth')) - parseInt(elem.css('borderRightWidth')); - elem.css('width', sideBarNavWidth); - }; - - resizeFn(); - $(window).resize(resizeFn); - }); - - $('#' + Settings.FORM_ID).on('click', '.' + Settings.ADD_ROW_BUTTON_CLASS, function(){ - addTableRow($(this).closest('tr')); - }); - - $('#' + Settings.FORM_ID).on('click', '.' + Settings.DEL_ROW_BUTTON_CLASS, function(){ - deleteTableRow($(this).closest('tr')); - }); - - $('#' + Settings.FORM_ID).on('click', '.' + Settings.ADD_CATEGORY_BUTTON_CLASS, function(){ - addTableCategory($(this).closest('tr')); - }); - - $('#' + Settings.FORM_ID).on('click', '.' + Settings.DEL_CATEGORY_BUTTON_CLASS, function(){ - deleteTableCategory($(this).closest('tr')); - }); - - $('#' + Settings.FORM_ID).on('click', '.' + Settings.TOGGLE_CATEGORY_COLUMN_CLASS, function(){ - toggleTableCategory($(this).closest('tr')); - }); - - $('#' + Settings.FORM_ID).on('click', '.' + Settings.MOVE_UP_BUTTON_CLASS, function(){ - moveTableRow($(this).closest('tr'), true); - }); - - $('#' + Settings.FORM_ID).on('click', '.' + Settings.MOVE_DOWN_BUTTON_CLASS, function(){ - moveTableRow($(this).closest('tr'), false); - }); - - $('#' + Settings.FORM_ID).on('keyup', function(e){ - var $target = $(e.target); - if (e.keyCode == 13) { - if ($target.is('table input')) { - // capture enter in table input - // if we have a sibling next to us that has an input, jump to it, otherwise check if we have a glyphicon for add to click - sibling = $target.parent('td').next(); - - if (sibling.hasClass(Settings.DATA_COL_CLASS)) { - // set focus to next input - sibling.find('input').focus(); - } else { - - // jump over the re-order row, if that's what we're on - if (sibling.hasClass(Settings.REORDER_BUTTONS_CLASS)) { - sibling = sibling.next(); - } - - // for tables with categories we add the entry and setup the new row on enter - if (sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).length) { - sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).click(); - - // set focus to the first input in the new row - $target.closest('table').find('tr.inputs input:first').focus(); - } - - var tableRows = sibling.parent(); - var tableBody = tableRows.parent(); - - // if theres no more siblings, we should jump to a new row - if (sibling.next().length == 0 && tableRows.nextAll().length == 1) { - tableBody.find("." + Settings.ADD_ROW_BUTTON_CLASS).click(); - } - } - - } else if ($target.is('input')) { - $target.change().blur(); - } - - e.preventDefault(); - } - }); - - $('#' + Settings.FORM_ID).on('keypress', function(e){ - if (e.keyCode == 13) { - - e.preventDefault(); - } - }); - - $('#' + Settings.FORM_ID).on('change', '.' + Settings.TRIGGER_CHANGE_CLASS , function(){ - // this input was changed, add the changed data attribute to it - $(this).attr('data-changed', true); - - badgeSidebarForDifferences($(this)); - }); - - $('#' + Settings.FORM_ID).on('switchChange.bootstrapSwitch', 'input.toggle-checkbox', function(){ - // this checkbox was changed, add the changed data attribute to it - $(this).attr('data-changed', true); - - badgeSidebarForDifferences($(this)); - }); - - // Bootstrap switch in table - $('#' + Settings.FORM_ID).on('change', 'input.table-checkbox', function () { - // Bootstrap switches in table: set the changed data attribute for all rows in table. - var row = $(this).closest('tr'); - if (row.hasClass("value-row")) { // Don't set attribute on input row switches prior to it being added to table. - row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true); - updateDataChangedForSiblingRows(row, true); - badgeSidebarForDifferences($(this)); - } - }); - - $('#' + Settings.FORM_ID).on('change', 'input.table-time', function() { - // Bootstrap switches in table: set the changed data attribute for all rows in table. - var row = $(this).closest('tr'); - if (row.hasClass("value-row")) { // Don't set attribute on input row switches prior to it being added to table. - row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true); - updateDataChangedForSiblingRows(row, true); - badgeSidebarForDifferences($(this)); - } - }); - - $('.advanced-toggle').click(function(){ - Settings.showAdvanced = !Settings.showAdvanced - var advancedSelector = $('.' + Settings.ADVANCED_CLASS) - - if (Settings.showAdvanced) { - advancedSelector.show(); - $(this).html("Hide advanced") - } else { - advancedSelector.hide(); - $(this).html("Show advanced") - } - - $(this).blur(); - }) - - $('#' + Settings.FORM_ID).on('click', '#' + Settings.CREATE_DOMAIN_ID_BTN_ID, function(){ - $(this).blur(); - showDomainCreationAlert(false); - }) - - $('#' + Settings.FORM_ID).on('click', '#' + Settings.CHOOSE_DOMAIN_ID_BTN_ID, function(){ - $(this).blur(); - chooseFromHighFidelityDomains($(this)) - }); - - $('#' + Settings.FORM_ID).on('click', '#' + Settings.GET_TEMPORARY_NAME_BTN_ID, function(){ - $(this).blur(); - createTemporaryDomain(); - }); - - - $('#' + Settings.FORM_ID).on('change', 'select', function(){ - $("input[name='" + $(this).attr('data-hidden-input') + "']").val($(this).val()).change() - }); - - $('#' + Settings.FORM_ID).on('click', '#' + Settings.DISCONNECT_ACCOUNT_BTN_ID, function(e){ - $(this).blur(); - disonnectHighFidelityAccount(); - e.preventDefault(); - }); - - $('#' + Settings.FORM_ID).on('click', '#' + Settings.CONNECT_ACCOUNT_BTN_ID, function(e){ - $(this).blur(); - prepareAccessTokenPrompt(function(accessToken) { - // we have an access token - set the access token input with this and save settings - $(Settings.ACCESS_TOKEN_SELECTOR).val(accessToken).change(); - saveSettings(); - }); - }); - - var panelsSource = $('#panels-template').html() - Settings.panelsTemplate = _.template(panelsSource) - - var sidebarTemplate = $('#list-group-template').html() - Settings.sidebarTemplate = _.template(sidebarTemplate) - - var navbarHeight = $('.navbar').outerHeight(true); - - $('#setup-sidebar').affix({ - offset: { - top: 1, - bottom: navbarHeight - } - }); - - reloadSettings(function(success){ - if (success) { - handleAction(); - } else { - swal({ - title: '', - type: 'error', - text: Strings.LOADING_SETTINGS_ERROR - }); - } - $('body').scrollspy({ - target: '#setup-sidebar', - offset: navbarHeight - }); - }); -}); - -function getShareName(callback) { - getDomainFromAPI(function(data){ - // check if we have owner_places (for a real domain) or a name (for a temporary domain) - if (data && data.status == "success") { - var shareName; - if (data.domain.default_place_name) { - shareName = data.domain.default_place_name; - } else if (data.domain.name) { - shareName = data.domain.name; - } else if (data.domain.network_address) { - shareName = data.domain.network_address; - if (data.domain.network_port !== 40102) { - shareName += ':' + data.domain.network_port; - } - } - - callback(true, shareName); - } else { - callback(false); - } - }) -} - -function handleAction() { - // check if we were passed an action to handle - var action = qs["action"]; - - if (action == "share") { - // figure out if we already have a stored domain ID - if (Settings.data.values.metaverse.id.length > 0) { - // we need to ask the API what a shareable name for this domain is - getShareName(function(success, shareName){ - if (success) { - var shareLink = "hifi://" + shareName; - - console.log(shareLink); - - // show a dialog with a copiable share URL - swal({ - title: "Share", - type: "input", - inputPlaceholder: shareLink, - inputValue: shareLink, - text: "Copy this URL to invite friends to your domain.", - closeOnConfirm: true - }); - - $('.sweet-alert input').select(); - - } else { - // show an error alert - swal({ - title: '', - type: 'error', - text: "There was a problem retreiving domain information from High Fidelity API.", - confirmButtonText: 'Try again', - showCancelButton: true, - closeOnConfirm: false - }, function(isConfirm){ - if (isConfirm) { - // they want to try getting domain share info again - showSpinnerAlert("Requesting domain information...") - handleAction(); - } else { - swal.close(); - } - }); - } - }); - } else { - // no domain ID present, just show the share dialog - createTemporaryDomain(); - } - } -} - -function dynamicButton(button_id, text) { - return $(""); -} - -function postSettings(jsonSettings) { - // POST the form JSON to the domain-server settings.json endpoint so the settings are saved - $.ajax('/settings.json', { - data: JSON.stringify(jsonSettings), - contentType: 'application/json', - type: 'POST' - }).done(function(data){ - if (data.status == "success") { - if ($(".save-button").html() === SAVE_BUTTON_LABEL_RESTART) { - showRestartModal(); - } else { - location.reload(true); - } - } else { - showErrorMessage("Error", SETTINGS_ERROR_MESSAGE) - reloadSettings(); - } - }).fail(function(){ - showErrorMessage("Error", SETTINGS_ERROR_MESSAGE) - reloadSettings(); - }); -} - -function accessTokenIsSet() { - return Settings.data.values.metaverse.access_token.length > 0; -} - -function setupHFAccountButton() { - - var hasAccessToken = accessTokenIsSet(); - var el; - - if (hasAccessToken) { - el = "

    "; - el += ""; - el += ""; - el += "

    "; - el = $(el); - } else { - // setup an object for the settings we want our button to have - var buttonSetting = { - type: 'button', - name: 'connected_account', - label: 'Connected Account', - } - buttonSetting.help = ""; - buttonSetting.classes = "btn-primary"; - buttonSetting.button_label = "Connect High Fidelity Account"; - buttonSetting.html_id = Settings.CONNECT_ACCOUNT_BTN_ID; - - buttonSetting.href = URLs.METAVERSE_URL + "/user/tokens/new?for_domain_server=true"; - - // since we do not have an access token we change hide domain ID and auto networking settings - // without an access token niether of them can do anything - $("[data-keypath='metaverse.id']").hide(); - - // use the existing getFormGroup helper to ask for a button - el = viewHelpers.getFormGroup('', buttonSetting, Settings.data.values); - } - - // add the button group to the top of the metaverse panel - $('#metaverse .panel-body').prepend(el); -} - -function disonnectHighFidelityAccount() { - // the user clicked on the disconnect account btn - give them a sweet alert to make sure this is what they want to do - swal({ - title: "Are you sure?", - text: "This will remove your domain-server OAuth access token." - + "

    This could cause your domain to appear offline and no longer be reachable via any place names.", - type: "warning", - html: true, - showCancelButton: true - }, function(){ - // we need to post to settings to clear the access-token - $(Settings.ACCESS_TOKEN_SELECTOR).val('').change(); - // reset the domain id to get a new temporary name - $(Settings.DOMAIN_ID_SELECTOR).val('').change(); - saveSettings(); - }); -} - -function showSpinnerAlert(title) { - swal({ - title: title, - text: '
    ', - html: true, - showConfirmButton: false, - allowEscapeKey: false - }); -} - -function showDomainCreationAlert(justConnected) { - swal({ - title: 'Create new domain ID', - type: 'input', - text: 'Enter a label this machine.

    This will help you identify which domain ID belongs to which machine.

    ', - showCancelButton: true, - confirmButtonText: "Create", - closeOnConfirm: false, - html: true - }, function(inputValue){ - if (inputValue === false) { - swal.close(); - - // user cancelled domain ID creation - if we're supposed to save after cancel then save here - if (justConnected) { - saveSettings(); - } - } else { - // we're going to change the alert to a new one with a spinner while we create this domain - showSpinnerAlert('Creating domain ID'); - createNewDomainID(inputValue, justConnected); - } - }); -} - -function createNewDomainID(label, justConnected) { - // get the JSON object ready that we'll use to create a new domain - var domainJSON = { - "label": label - } - - $.post("/api/domains", domainJSON, function(data){ - // we successfully created a domain ID, set it on that field - var domainID = data.domain.id; - console.log("Setting domain id to ", data, domainID); - $(Settings.DOMAIN_ID_SELECTOR).val(domainID).change(); - - if (justConnected) { - var successText = Strings.CREATE_DOMAIN_SUCCESS_JUST_CONNECTED - } else { - var successText = Strings.CREATE_DOMAIN_SUCCESS; - } - - successText += "

    Click the button below to save your new settings and restart your domain-server."; - - // show a sweet alert to say we are all finished up and that we need to save - swal({ - title: 'Success!', - type: 'success', - text: successText, - html: true, - confirmButtonText: 'Save' - }, function(){ - saveSettings(); - }); - }, 'json').fail(function(){ - - var errorText = "There was a problem creating your new domain ID. Do you want to try again or"; - - if (justConnected) { - errorText += " just save your new access token?

    You can always create a new domain ID later."; - } else { - errorText += " cancel?" - } - - // we failed to create the new domain ID, show a sweet-alert that lets them try again or cancel - swal({ - title: '', - type: 'error', - text: errorText, - html: true, - confirmButtonText: 'Try again', - showCancelButton: true, - closeOnConfirm: false - }, function(isConfirm){ - if (isConfirm) { - // they want to try creating a domain ID again - showDomainCreationAlert(justConnected); - } else { - // they want to cancel - if (justConnected) { - // since they just connected we need to save the access token here - saveSettings(); - } - } - }); - }); -} - -function createDomainSpinner() { - var spinner = ''; - return spinner; -} - -function createDomainLoadingError(message) { - var errorEl = $(""); - errorEl.append(message + " "); - - var retryLink = $("Please click here to try again."); - retryLink.click(function(ev) { - ev.preventDefault(); - reloadDomainInfo(); - }); - errorEl.append(retryLink); - - return errorEl; -} - -function parseJSONResponse(xhr) { - try { - return JSON.parse(xhr.responseText); - } catch (e) { - } - return null; -} - -function showOrHideLabel() { - var type = getCurrentDomainIDType(); - var shouldShow = accessTokenIsSet() && (type === DOMAIN_ID_TYPE_FULL || type === DOMAIN_ID_TYPE_UNKNOWN); - $(".panel#label").toggle(shouldShow); - $("li a[href='#label']").parent().toggle(shouldShow); - return shouldShow; -} - -function setupDomainLabelSetting() { - showOrHideLabel(); - - var html = "
    " - html += " Edit"; - html += ""; - html += "
    "; - - html = $(html); - - html.find('a').click(function(ev) { - ev.preventDefault(); - - var label = DomainInfo.label === null ? "" : DomainInfo.label; - var modal_body = "
    "; - modal_body += ""; - modal_body += ""; - modal_body += "
    "; - modal_body += "
    "; - - var dialog = bootbox.dialog({ - title: 'Edit Label', - message: modal_body, - closeButton: false, - onEscape: true, - buttons: [ - { - label: 'Cancel', - className: 'edit-label-cancel-btn', - callback: function() { - dialog.modal('hide'); - } - }, - { - label: 'Save', - className: 'edit-label-save-btn btn btn-primary', - callback: function() { - var data = { - label: $('#domain-label-input').val() - }; - - $('.edit-label-cancel-btn').attr('disabled', 'disabled'); - $('.edit-label-save-btn').attr('disabled', 'disabled'); - $('.edit-label-save-btn').html("Saving..."); - - $('.error-message').hide(); - - $.ajax({ - url: '/api/domains', - type: 'PUT', - data: data, - success: function(xhr) { - dialog.modal('hide'); - reloadDomainInfo(); - }, - error: function(xhr) { - var data = parseJSONResponse(xhr); - console.log(data, data.status, data.data); - if (data.status === "fail") { - for (var key in data.data) { - var errorMsg = data.data[key]; - var errorEl = $('.error-message[data-property="' + key + '"'); - errorEl.html(errorMsg); - errorEl.show(); - } - } - $('.edit-label-cancel-btn').removeAttr('disabled'); - $('.edit-label-save-btn').removeAttr('disabled'); - $('.edit-label-save-btn').html("Save"); - } - }); - return false; - } - } - ], - callback: function(result) { - console.log("result: ", result); - } - }); - }); - - var spinner = createDomainSpinner(); - var errorEl = createDomainLoadingError("Error loading label."); - - html.append(spinner); - html.append(errorEl); - - $('div#label .panel-body').append(html); -} - -function showOrHideAutomaticNetworking() { - var type = getCurrentDomainIDType(); - if (!accessTokenIsSet() || (type !== DOMAIN_ID_TYPE_FULL && type !== DOMAIN_ID_TYPE_UNKNOWN)) { - $("[data-keypath='metaverse.automatic_networking']").hide(); - return false; - } - $("[data-keypath='metaverse.automatic_networking']").show(); - return true; -} - -function setupDomainNetworkingSettings() { - if (!showOrHideAutomaticNetworking()) { - return; - } - - var autoNetworkingSetting = Settings.data.values.metaverse.automatic_networking; - if (autoNetworkingSetting === 'full') { - return; - } - - var includeAddress = autoNetworkingSetting === 'disabled'; - - if (includeAddress) { - var label = "Network Address:Port"; - } else { - var label = "Network Port"; - } - - var lowerName = name.toLowerCase(); - var form = '
    '; - form += ''; - form += ' Edit'; - form += ''; - form += '
    This defines how nodes will connect to your domain. You can read more about automatic networking here.
    '; - form += '
    '; - - form = $(form); - - form.find('#edit-network-address-port').click(function(ev) { - ev.preventDefault(); - - var address = DomainInfo.network_address === null ? '' : DomainInfo.network_address; - var port = DomainInfo.network_port === null ? '' : DomainInfo.network_port; - var modal_body = "
    "; - if (includeAddress) { - modal_body += ""; - modal_body += ""; - modal_body += "
    "; - } - modal_body += ""; - modal_body += ""; - modal_body += "
    "; - modal_body += "
    "; - - var dialog = bootbox.dialog({ - title: 'Edit Network', - message: modal_body, - closeButton: false, - onEscape: true, - buttons: [ - { - label: 'Cancel', - className: 'edit-network-cancel-btn', - callback: function() { - dialog.modal('hide'); - } - }, - { - label: 'Save', - className: 'edit-network-save-btn btn btn-primary', - callback: function() { - var data = { - network_port: $('#network-port-input').val() - }; - if (includeAddress) { - data.network_address = $('#network-address-input').val(); - } - - $('.edit-network-cancel-btn').attr('disabled', 'disabled'); - $('.edit-network-save-btn').attr('disabled', 'disabled'); - $('.edit-network-save-btn').html("Saving..."); - - console.log('data', data); - - $('.error-message').hide(); - - $.ajax({ - url: '/api/domains', - type: 'PUT', - data: data, - success: function(xhr) { - console.log(xhr, parseJSONResponse(xhr)); - dialog.modal('hide'); - reloadDomainInfo(); - }, - error:function(xhr) { - var data = parseJSONResponse(xhr); - console.log(data, data.status, data.data); - if (data.status === "fail") { - for (var key in data.data) { - var errorMsg = data.data[key]; - console.log(key, errorMsg); - var errorEl = $('.error-message[data-property="' + key + '"'); - console.log(errorEl); - errorEl.html(errorMsg); - errorEl.show(); - } - } - $('.edit-network-cancel-btn').removeAttr('disabled'); - $('.edit-network-save-btn').removeAttr('disabled'); - $('.edit-network-save-btn').html("Save"); - } - }); - return false; - } - } - ], - callback: function(result) { - console.log("result: ", result); - } - }); - }); - - var spinner = createDomainSpinner(); - - var errorMessage = '' - if (includeAddress) { - errorMessage = "We were unable to load the network address and port."; - } else { - errorMessage = "We were unable to load the network port." - } - var errorEl = createDomainLoadingError(errorMessage); - - var autoNetworkingEl = $('div[data-keypath="metaverse.automatic_networking"]'); - autoNetworkingEl.after(spinner); - autoNetworkingEl.after(errorEl); - autoNetworkingEl.after(form); -} - - -function setupPlacesTable() { - // create a dummy table using our view helper - var placesTableSetting = { - type: 'table', - name: 'places', - label: 'Places', - html_id: Settings.PLACES_TABLE_ID, - help: "The following places currently point to this domain.
    To point places to this domain, " - + " go to the My Places " - + "page in your High Fidelity Metaverse account.", - read_only: true, - can_add_new_rows: false, - columns: [ + var qs = (function(a) { + if (a == "") return {}; + var b = {}; + for (var i = 0; i < a.length; ++i) { - "name": "name", - "label": "Name" - }, - { - "name": "path", - "label": "Viewpoint or Path" - }, - { - "name": "remove", - "label": "", - "class": "buttons" - } - ] - } - - // get a table for the places - var placesTableGroup = viewHelpers.getFormGroup('', placesTableSetting, Settings.data.values); - - // append the places table in the right place - $('#places_paths .panel-body').prepend(placesTableGroup); - //$('#' + Settings.PLACES_TABLE_ID).append(""); - - var spinner = createDomainSpinner(); - $('#' + Settings.PLACES_TABLE_ID).after($(spinner)); - - var errorEl = createDomainLoadingError("There was an error retreiving your places."); - $("#" + Settings.PLACES_TABLE_ID).after(errorEl); - - // do we have a domain ID? - if (Settings.data.values.metaverse.id.length == 0) { - // we don't have a domain ID - add a button to offer the user a chance to get a temporary one - var temporaryPlaceButton = dynamicButton(Settings.GET_TEMPORARY_NAME_BTN_ID, 'Get a temporary place name'); - $('#' + Settings.PLACES_TABLE_ID).after(temporaryPlaceButton); - } - if (accessTokenIsSet()) { - appendAddButtonToPlacesTable(); - } -} - -function placeTableRow(name, path, isTemporary, placeID) { - var name_link = "" + (isTemporary ? name + " (temporary)" : name) + ""; - - function placeEditClicked() { - editHighFidelityPlace(placeID, name, path); - } - - function placeDeleteClicked() { - var el = $(this); - var confirmString = Strings.REMOVE_PLACE_TITLE.replace("{{place}}", name); - var dialog = bootbox.dialog({ - message: confirmString, - closeButton: false, - onEscape: true, - buttons: [ - { - label: Strings.REMOVE_PLACE_CANCEL_BUTTON, - className: "delete-place-cancel-btn", - callback: function() { - dialog.modal('hide'); - } - }, - { - label: Strings.REMOVE_PLACE_DELETE_BUTTON, - className: "delete-place-confirm-btn btn btn-danger", - callback: function() { - $('.delete-place-cancel-btn').attr('disabled', 'disabled'); - $('.delete-place-confirm-btn').attr('disabled', 'disabled'); - $('.delete-place-confirm-btn').html(Strings.REMOVE_PLACE_DELETE_BUTTON_PENDING); - sendUpdatePlaceRequest( - placeID, - '', - null, - true, - function() { - reloadDomainInfo(); - dialog.modal('hide'); - }, function() { - $('.delete-place-cancel-btn').removeAttr('disabled'); - $('.delete-place-confirm-btn').removeAttr('disabled'); - $('.delete-place-confirm-btn').html(Strings.REMOVE_PLACE_DELETE_BUTTON); - bootbox.alert(Strings.REMOVE_PLACE_ERROR); - }); - return false; - } - }, - ] - }); - } - - if (isTemporary) { - var editLink = ""; - var deleteColumn = ""; - } else { - var editLink = " Edit"; - var deleteColumn = ""; - } - - var row = $("" + name_link + "" + path + editLink + "" + deleteColumn + ""); - row.find(".place-edit").click(placeEditClicked); - row.find(".place-delete").click(placeDeleteClicked); - - return row; -} - -function placeTableRowForPlaceObject(place) { - var placePathOrIndex = (place.path ? place.path : "/"); - return placeTableRow(place.name, placePathOrIndex, false, place.id); -} - -function reloadDomainInfo() { - $('#' + Settings.PLACES_TABLE_ID + " tbody tr").not('.headers').remove(); - - $('.domain-loading-show').show(); - $('.domain-loading-hide').hide(); - $('.domain-loading-error').hide(); - $('.loading-domain-info-spinner').show(); - $('#' + Settings.PLACES_TABLE_ID).append("Hello"); - - getDomainFromAPI(function(data){ - $('.loading-domain-info-spinner').hide(); - $('.domain-loading-show').hide(); - - // check if we have owner_places (for a real domain) or a name (for a temporary domain) - if (data.status == "success") { - $('.domain-loading-hide').show(); - if (data.domain.owner_places) { - // add a table row for each of these names - _.each(data.domain.owner_places, function(place){ - $('#' + Settings.PLACES_TABLE_ID + " tbody").append(placeTableRowForPlaceObject(place)); - }); - } else if (data.domain.name) { - // add a table row for this temporary domain name - $('#' + Settings.PLACES_TABLE_ID + " tbody").append(placeTableRow(data.domain.name, '/', true)); - } - - // Update label - if (showOrHideLabel()) { - var label = data.domain.label; - label = label === null ? '' : label; - $('#network-label').val(label); - } - - // Update automatic networking - if (showOrHideAutomaticNetworking()) { - var autoNetworkingSetting = Settings.data.values.metaverse.automatic_networking; - var address = data.domain.network_address === null ? "" : data.domain.network_address; - var port = data.domain.network_port === null ? "" : data.domain.network_port; - if (autoNetworkingSetting === 'disabled') { - $('#network-address-port input').val(address + ":" + port); - } else if (autoNetworkingSetting === 'ip') { - $('#network-address-port input').val(port); - } - } - - if (accessTokenIsSet()) { - appendAddButtonToPlacesTable(); - } - - } else { - $('.domain-loading-error').show(); - } - }) -} - -function appendDomainIDButtons() { - var domainIDInput = $(Settings.DOMAIN_ID_SELECTOR); - - var createButton = dynamicButton(Settings.CREATE_DOMAIN_ID_BTN_ID, Strings.CREATE_DOMAIN_BUTTON); - createButton.css('margin-top', '10px'); - var chooseButton = dynamicButton(Settings.CHOOSE_DOMAIN_ID_BTN_ID, Strings.CHOOSE_DOMAIN_BUTTON); - chooseButton.css('margin', '10px 0px 0px 10px'); - - domainIDInput.after(chooseButton); - domainIDInput.after(createButton); -} - -function editHighFidelityPlace(placeID, name, path) { - var dialog; - - var modal_body = "
    "; - modal_body += ""; - modal_body += "
    "; - - var modal_buttons = [ - { - label: Strings.EDIT_PLACE_CANCEL_BUTTON, - className: "edit-place-cancel-button", - callback: function() { - dialog.modal('hide'); - } - }, - { - label: Strings.EDIT_PLACE_CONFIRM_BUTTON, - className: 'edit-place-save-button btn btn-primary', - callback: function() { - var placePath = $('#place-path-input').val(); - - if (path == placePath) { - return true; - } - - $('.edit-place-cancel-button').attr('disabled', 'disabled'); - $('.edit-place-save-button').attr('disabled', 'disabled'); - $('.edit-place-save-button').html(Strings.EDIT_PLACE_BUTTON_PENDING); - - sendUpdatePlaceRequest( - placeID, - placePath, - null, - false, - function() { - dialog.modal('hide') - reloadDomainInfo(); - }, - function() { - $('.edit-place-cancel-button').removeAttr('disabled'); - $('.edit-place-save-button').removeAttr('disabled'); - $('.edit-place-save-button').html(Strings.EDIT_PLACE_CONFIRM_BUTTON); - } - ); - - return false; - } - } - ]; - - dialog = bootbox.dialog({ - title: Strings.EDIT_PLACE_TITLE, - closeButton: false, - onEscape: true, - message: modal_body, - buttons: modal_buttons - }) -} - -function appendAddButtonToPlacesTable() { - var addRow = $(" "); - addRow.find(".place-add").click(function(ev) { - ev.preventDefault(); - chooseFromHighFidelityPlaces(Settings.initialValues.metaverse.access_token, null, function(placeName, newDomainID) { - if (newDomainID) { - Settings.data.values.metaverse.id = newDomainID; - var domainIDEl = $("[data-keypath='metaverse.id']"); - domainIDEl.val(newDomainID); - Settings.initialValues.metaverse.id = newDomainID; - badgeSidebarForDifferences(domainIDEl); - } - reloadDomainInfo(); - }); - }); - $('#' + Settings.PLACES_TABLE_ID + " tbody").append(addRow); -} - -function chooseFromHighFidelityDomains(clickedButton) { - // setup the modal to help user pick their domain - if (Settings.initialValues.metaverse.access_token) { - - // add a spinner to the choose button - clickedButton.html("Loading domains..."); - clickedButton.attr('disabled', 'disabled'); - - // get a list of user domains from data-web - $.ajax({ - url: "/api/domains", - dataType: 'json', - jsonp: false, - success: function(data){ - - var modal_buttons = { - cancel: { - label: 'Cancel', - className: 'btn-default' - } - } - - if (data.data.domains.length) { - // setup a select box for the returned domains - modal_body = "

    Choose the High Fidelity domain you want this domain-server to represent.
    This will set your domain ID on the settings page.

    "; - domain_select = $(""); - _.each(data.data.domains, function(domain){ - var domainString = ""; - - if (domain.label) { - domainString += '"' + domain.label+ '" - '; - } - - domainString += domain.id; - - domain_select.append(""); - }) - modal_body += "" + domain_select[0].outerHTML - modal_buttons["success"] = { - label: 'Choose domain', - callback: function() { - domainID = $('#domain-name-select').val() - // set the domain ID on the form - $(Settings.DOMAIN_ID_SELECTOR).val(domainID).change(); - } - } + var p=a[i].split('=', 2); + if (p.length == 1) { + b[p[0]] = ""; } else { - modal_buttons["success"] = { - label: 'Create new domain', - callback: function() { - window.open(URLs.METAVERSE_URL + "/user/domains", '_blank'); - } - } - modal_body = "

    You do not have any domains in your High Fidelity account." + - "

    Go to your domains page to create a new one. Once your domain is created re-open this dialog to select it.

    " + b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " ")); } - - bootbox.dialog({ - title: "Choose matching domain", - onEscape: true, - message: modal_body, - buttons: modal_buttons - }) - }, - error: function() { - bootbox.alert("Failed to retrieve your domains from the High Fidelity Metaverse"); - }, - complete: function() { - // remove the spinner from the choose button - clickedButton.html("Choose from my domains") - clickedButton.removeAttr('disabled') } - }); - - } else { - bootbox.alert({ - message: "You must have an access token to query your High Fidelity domains.

    " + - "Please follow the instructions on the settings page to add an access token.", - title: "Access token required" - }) - } - } - -function createTemporaryDomain() { - swal({ - title: 'Create temporary place name', - text: "This will create a temporary place name and domain ID" - + " so other users can easily connect to your domain.

    " - + "In order to make your domain reachable, this will also enable full automatic networking.", - showCancelButton: true, - confirmButtonText: 'Create', - closeOnConfirm: false, - html: true - }, function(isConfirm){ - if (isConfirm) { - showSpinnerAlert('Creating temporary place name'); - - // make a get request to get a temporary domain - $.post(URLs.METAVERSE_URL + '/api/v1/domains/temporary', function(data){ - if (data.status == "success") { - var domain = data.data.domain; - - // we should have a new domain ID - set it on the domain ID value - $(Settings.DOMAIN_ID_SELECTOR).val(domain.id).change(); - - // we also need to make sure auto networking is set to full - $('[data-hidden-input="metaverse.automatic_networking"]').val("full").change(); - - swal({ - type: 'success', - title: 'Success!', - text: "We have created a temporary name and domain ID for you.

    " - + "Your temporary place name is " + domain.name + ".

    " - + "Press the button below to save your new settings and restart your domain-server.", - confirmButtonText: 'Save', - html: true - }, function(){ - saveSettings(); - }); - } - }); - } - }); -} - -function reloadSettings(callback) { - $.getJSON('/settings.json', function(data){ - _.extend(data, viewHelpers) - - $('.nav-stacked').html(Settings.sidebarTemplate(data)) - $('#panels').html(Settings.panelsTemplate(data)) - - Settings.data = data; - Settings.initialValues = form2js('settings-form', ".", false, cleanupFormValues, true); + return b; + })(window.location.search.substr(1).split('&')); + Settings.afterReloadActions = function() { // append the domain selection modal appendDomainIDButtons(); @@ -1291,117 +49,10 @@ function reloadSettings(callback) { } } - // setup any bootstrap switches - $('.toggle-checkbox').bootstrapSwitch(); - - $('[data-toggle="tooltip"]').tooltip(); - - // call the callback now that settings are loaded - callback(true); - }).fail(function() { - // call the failure object since settings load faild - callback(false) - }); -} - -function validateInputs() { - // check if any new values are bad - var tables = $('table'); - - var inputsValid = true; - - var tables = $('table'); - - // clear any current invalid rows - $('tr.' + Settings.INVALID_ROW_CLASS).removeClass(Settings.INVALID_ROW_CLASS); - - function markParentRowInvalid(rowChild) { - $(rowChild).closest('tr').addClass(Settings.INVALID_ROW_CLASS); + handleAction(); } - _.each(tables, function(table) { - // validate keys specificially for spaces and equality to an existing key - var newKeys = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ' td.key'); - - var keyWithSpaces = false; - var empty = false; - var duplicateKey = false; - - _.each(newKeys, function(keyCell) { - var keyVal = $(keyCell).children('input').val(); - - if (keyVal.indexOf(' ') !== -1) { - keyWithSpaces = true; - markParentRowInvalid(keyCell); - return; - } - - // make sure the key isn't empty - if (keyVal.length === 0) { - empty = true - - markParentRowInvalid(input); - return; - } - - // make sure we don't have duplicate keys in the table - var otherKeys = $(table).find('td.key').not(keyCell); - _.each(otherKeys, function(otherKeyCell) { - var keyInput = $(otherKeyCell).children('input'); - - if (keyInput.length) { - if ($(keyInput).val() == keyVal) { - duplicateKey = true; - } - } else if ($(otherKeyCell).html() == keyVal) { - duplicateKey = true; - } - - if (duplicateKey) { - markParentRowInvalid(keyCell); - return; - } - }); - - }); - - if (keyWithSpaces) { - showErrorMessage("Error", "Key contains spaces"); - inputsValid = false; - return - } - - if (empty) { - showErrorMessage("Error", "Empty field(s)"); - inputsValid = false; - return - } - - if (duplicateKey) { - showErrorMessage("Error", "Two keys cannot be identical"); - inputsValid = false; - return; - } - }); - - return inputsValid; -} - -var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!"; - -function saveSettings() { - - if (validateInputs()) { - // POST the form JSON to the domain-server settings.json endpoint so the settings are saved - var canPost = true; - - // disable any inputs not changed - $("input:not([data-changed])").each(function () { - $(this).prop('disabled', true); - }); - - // grab a JSON representation of the form via form2js - var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); + Settings.handlePostSettings = function(formJSON) { // check if we've set the basic http password if (formJSON["security"]) { @@ -1424,727 +75,945 @@ function saveSettings() { delete formJSON["security"]["verify_http_password"]; } else { bootbox.alert({ "message": "Passwords must match!", "title": "Password Error" }); - canPost = false; + return false; } } } - console.log("----- SAVING ------"); + console.log("----- handlePostSettings() called ------"); console.log(formJSON); - // re-enable all inputs - $("input").each(function () { - $(this).prop('disabled', false); - }); + if (formJSON["security"]) { + var username = formJSON["security"]["http_username"]; - // remove focus from the button - $(this).blur(); + var password = formJSON["security"]["http_password"]; - if (canPost) { - if (formJSON["security"]) { - var username = formJSON["security"]["http_username"]; + if ((password == sha256_digest("")) && (username == undefined || (username && username.length != 0))) { + swal({ + title: "Are you sure?", + text: "You have entered a blank password with a non-blank username. Are you sure you want to require a blank password?", + type: "warning", + showCancelButton: true, + confirmButtonColor: "#5cb85c", + confirmButtonText: "Yes!", + closeOnConfirm: true + }, + function () { + formJSON["security"]["http_password"] = ""; - var password = formJSON["security"]["http_password"]; + postSettings(formJSON); + }); - if ((password == sha256_digest("")) && (username == undefined || (username && username.length != 0))) { - swal({ - title: "Are you sure?", - text: "You have entered a blank password with a non-blank username. Are you sure you want to require a blank password?", - type: "warning", - showCancelButton: true, - confirmButtonColor: "#5cb85c", - confirmButtonText: "Yes!", - closeOnConfirm: true - }, - function () { - formJSON["security"]["http_password"] = ""; - postSettings(formJSON); - }); - return; - } + return; } - // POST the form JSON to the domain-server settings.json endpoint so the settings are saved - postSettings(formJSON); } - } -} -$('body').on('click', '.save-button', function(e){ - saveSettings(); - return false; -}); + postSettings(formJSON); + }; -function makeTable(setting, keypath, setting_value) { - var isArray = !_.has(setting, 'key'); - var categoryKey = setting.categorize_by_key; - var isCategorized = !!categoryKey && isArray; - - if (!isArray && setting.can_order) { - setting.can_order = false; - } - - var html = ""; - - if (setting.help) { - html += "" + setting.help + "" - } - - var nonDeletableRowKey = setting["non-deletable-row-key"]; - var nonDeletableRowValues = setting["non-deletable-row-values"]; - - html += ""; - - if (setting.caption) { - html += "" - } - - // Column groups - if (setting.groups) { - html += "" - _.each(setting.groups, function (group) { - html += "" - }) - if (!setting.read_only) { - if (setting.can_order) { - html += ""; - } - html += "" - } - html += "" - } - - // Column names - html += "" - - if (setting.numbered === true) { - html += "" // Row number - } - - if (setting.key) { - html += "" // Key - } - - var numVisibleColumns = 0; - _.each(setting.columns, function(col) { - if (!col.hidden) numVisibleColumns++; - html += "" // Data + $('#' + Settings.FORM_ID).on('click', '#' + Settings.CREATE_DOMAIN_ID_BTN_ID, function(){ + $(this).blur(); + showDomainCreationAlert(false); }) - if (!setting.read_only) { - if (setting.can_order) { - numVisibleColumns++; - html += ""; - } - numVisibleColumns++; - html += ""; + $('#' + Settings.FORM_ID).on('click', '#' + Settings.CHOOSE_DOMAIN_ID_BTN_ID, function(){ + $(this).blur(); + chooseFromHighFidelityDomains($(this)) + }); + + $('#' + Settings.FORM_ID).on('click', '#' + Settings.GET_TEMPORARY_NAME_BTN_ID, function(){ + $(this).blur(); + createTemporaryDomain(); + }); + + $('#' + Settings.FORM_ID).on('click', '#' + Settings.DISCONNECT_ACCOUNT_BTN_ID, function(e){ + $(this).blur(); + disonnectHighFidelityAccount(); + e.preventDefault(); + }); + + $('#' + Settings.FORM_ID).on('click', '#' + Settings.CONNECT_ACCOUNT_BTN_ID, function(e){ + $(this).blur(); + prepareAccessTokenPrompt(function(accessToken) { + // we have an access token - set the access token input with this and save settings + $(Settings.ACCESS_TOKEN_SELECTOR).val(accessToken).change(); + saveSettings(); + }); + }); + + function accessTokenIsSet() { + return Settings.data.values.metaverse.access_token.length > 0; } - // populate rows in the table from existing values - var row_num = 1; - - if (keypath.length > 0 && _.size(setting_value) > 0) { - var rowIsObject = setting.columns.length > 1; - - _.each(setting_value, function(row, rowIndexOrName) { - var categoryPair = {}; - var categoryValue = ""; - if (isCategorized) { - categoryValue = rowIsObject ? row[categoryKey] : row; - categoryPair[categoryKey] = categoryValue; - if (_.findIndex(setting_value, categoryPair) === rowIndexOrName) { - html += makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns, setting.can_add_new_categories, ""); + function getShareName(callback) { + getDomainFromAPI(function(data){ + // check if we have owner_places (for a real domain) or a name (for a temporary domain) + if (data && data.status == "success") { + var shareName; + if (data.domain.default_place_name) { + shareName = data.domain.default_place_name; + } else if (data.domain.name) { + shareName = data.domain.name; + } else if (data.domain.network_address) { + shareName = data.domain.network_address; + if (data.domain.network_port !== 40102) { + shareName += ':' + data.domain.network_port; + } } + + callback(true, shareName); + } else { + callback(false); + } + }) + } + + function handleAction() { + // check if we were passed an action to handle + var action = qs["action"]; + + if (action == "share") { + // figure out if we already have a stored domain ID + if (Settings.data.values.metaverse.id.length > 0) { + // we need to ask the API what a shareable name for this domain is + getShareName(function(success, shareName){ + if (success) { + var shareLink = "hifi://" + shareName; + + console.log(shareLink); + + // show a dialog with a copiable share URL + swal({ + title: "Share", + type: "input", + inputPlaceholder: shareLink, + inputValue: shareLink, + text: "Copy this URL to invite friends to your domain.", + closeOnConfirm: true + }); + + $('.sweet-alert input').select(); + + } else { + // show an error alert + swal({ + title: '', + type: 'error', + text: "There was a problem retreiving domain information from High Fidelity API.", + confirmButtonText: 'Try again', + showCancelButton: true, + closeOnConfirm: false + }, function(isConfirm){ + if (isConfirm) { + // they want to try getting domain share info again + showSpinnerAlert("Requesting domain information...") + handleAction(); + } else { + swal.close(); + } + }); + } + }); + } else { + // no domain ID present, just show the share dialog + createTemporaryDomain(); + } + } + } + + function setupHFAccountButton() { + + var hasAccessToken = accessTokenIsSet(); + var el; + + if (hasAccessToken) { + el = "

    "; + el += ""; + el += ""; + el += "

    "; + el = $(el); + } else { + // setup an object for the settings we want our button to have + var buttonSetting = { + type: 'button', + name: 'connected_account', + label: 'Connected Account', + } + buttonSetting.help = ""; + buttonSetting.classes = "btn-primary"; + buttonSetting.button_label = "Connect High Fidelity Account"; + buttonSetting.html_id = Settings.CONNECT_ACCOUNT_BTN_ID; + + buttonSetting.href = URLs.METAVERSE_URL + "/user/tokens/new?for_domain_server=true"; + + // since we do not have an access token we change hide domain ID and auto networking settings + // without an access token niether of them can do anything + $("[data-keypath='metaverse.id']").hide(); + + // use the existing getFormGroup helper to ask for a button + el = viewHelpers.getFormGroup('', buttonSetting, Settings.data.values); + } + + // add the button group to the top of the metaverse panel + $('#metaverse .panel-body').prepend(el); + } + + function disonnectHighFidelityAccount() { + // the user clicked on the disconnect account btn - give them a sweet alert to make sure this is what they want to do + swal({ + title: "Are you sure?", + text: "This will remove your domain-server OAuth access token." + + "

    This could cause your domain to appear offline and no longer be reachable via any place names.", + type: "warning", + html: true, + showCancelButton: true + }, function(){ + // we need to post to settings to clear the access-token + $(Settings.ACCESS_TOKEN_SELECTOR).val('').change(); + // reset the domain id to get a new temporary name + $(Settings.DOMAIN_ID_SELECTOR).val('').change(); + saveSettings(); + }); + } + + function showDomainCreationAlert(justConnected) { + swal({ + title: 'Create new domain ID', + type: 'input', + text: 'Enter a label this machine.

    This will help you identify which domain ID belongs to which machine.

    ', + showCancelButton: true, + confirmButtonText: "Create", + closeOnConfirm: false, + html: true + }, function(inputValue){ + if (inputValue === false) { + swal.close(); + + // user cancelled domain ID creation - if we're supposed to save after cancel then save here + if (justConnected) { + saveSettings(); + } + } else { + // we're going to change the alert to a new one with a spinner while we create this domain + showSpinnerAlert('Creating domain ID'); + createNewDomainID(inputValue, justConnected); + } + }); + } + + function createNewDomainID(label, justConnected) { + // get the JSON object ready that we'll use to create a new domain + var domainJSON = { + "label": label + } + + $.post("/api/domains", domainJSON, function(data){ + // we successfully created a domain ID, set it on that field + var domainID = data.domain.id; + console.log("Setting domain id to ", data, domainID); + $(Settings.DOMAIN_ID_SELECTOR).val(domainID).change(); + + if (justConnected) { + var successText = Strings.CREATE_DOMAIN_SUCCESS_JUST_CONNECTED + } else { + var successText = Strings.CREATE_DOMAIN_SUCCESS; } - html += ""; + successText += "

    Click the button below to save your new settings and restart your domain-server."; - if (setting.numbered === true) { - html += "" + // show a sweet alert to say we are all finished up and that we need to save + swal({ + title: 'Success!', + type: 'success', + text: successText, + html: true, + confirmButtonText: 'Save' + }, function(){ + saveSettings(); + }); + }, 'json').fail(function(){ + + var errorText = "There was a problem creating your new domain ID. Do you want to try again or"; + + if (justConnected) { + errorText += " just save your new access token?

    You can always create a new domain ID later."; + } else { + errorText += " cancel?" } - if (setting.key) { - html += "" - } - - var isNonDeletableRow = !setting.can_add_new_rows; - - _.each(setting.columns, function(col) { - - var colValue, colName; - if (isArray) { - colValue = rowIsObject ? row[col.name] : row; - colName = keypath + "[" + rowIndexOrName + "]" + (rowIsObject ? "." + col.name : ""); + // we failed to create the new domain ID, show a sweet-alert that lets them try again or cancel + swal({ + title: '', + type: 'error', + text: errorText, + html: true, + confirmButtonText: 'Try again', + showCancelButton: true, + closeOnConfirm: false + }, function(isConfirm){ + if (isConfirm) { + // they want to try creating a domain ID again + showDomainCreationAlert(justConnected); } else { - colValue = row[col.name]; - colName = keypath + "." + rowIndexOrName + "." + col.name; + // they want to cancel + if (justConnected) { + // since they just connected we need to save the access token here + saveSettings(); + } + } + }); + }); + } + + function createDomainSpinner() { + var spinner = ''; + return spinner; + } + + function createDomainLoadingError(message) { + var errorEl = $(""); + errorEl.append(message + " "); + + var retryLink = $("Please click here to try again."); + retryLink.click(function(ev) { + ev.preventDefault(); + reloadDomainInfo(); + }); + errorEl.append(retryLink); + + return errorEl; + } + + function showOrHideLabel() { + var type = getCurrentDomainIDType(); + var shouldShow = accessTokenIsSet() && (type === DOMAIN_ID_TYPE_FULL || type === DOMAIN_ID_TYPE_UNKNOWN); + $(".panel#label").toggle(shouldShow); + $("li a[href='#label']").parent().toggle(shouldShow); + return shouldShow; + } + + function setupDomainLabelSetting() { + showOrHideLabel(); + + var html = "
    " + html += " Edit"; + html += ""; + html += "
    "; + + html = $(html); + + html.find('a').click(function(ev) { + ev.preventDefault(); + + var label = DomainInfo.label === null ? "" : DomainInfo.label; + var modal_body = "
    "; + modal_body += ""; + modal_body += ""; + modal_body += "
    "; + modal_body += "
    "; + + var dialog = bootbox.dialog({ + title: 'Edit Label', + message: modal_body, + closeButton: false, + onEscape: true, + buttons: [ + { + label: 'Cancel', + className: 'edit-label-cancel-btn', + callback: function() { + dialog.modal('hide'); + } + }, + { + label: 'Save', + className: 'edit-label-save-btn btn btn-primary', + callback: function() { + var data = { + label: $('#domain-label-input').val() + }; + + $('.edit-label-cancel-btn').attr('disabled', 'disabled'); + $('.edit-label-save-btn').attr('disabled', 'disabled'); + $('.edit-label-save-btn').html("Saving..."); + + $('.error-message').hide(); + + $.ajax({ + url: '/api/domains', + type: 'PUT', + data: data, + success: function(xhr) { + dialog.modal('hide'); + reloadDomainInfo(); + }, + error: function(xhr) { + var data = parseJSONResponse(xhr); + console.log(data, data.status, data.data); + if (data.status === "fail") { + for (var key in data.data) { + var errorMsg = data.data[key]; + var errorEl = $('.error-message[data-property="' + key + '"'); + errorEl.html(errorMsg); + errorEl.show(); + } + } + $('.edit-label-cancel-btn').removeAttr('disabled'); + $('.edit-label-save-btn').removeAttr('disabled'); + $('.edit-label-save-btn').html("Save"); + } + }); + return false; + } + } + ], + callback: function(result) { + console.log("result: ", result); + } + }); + }); + + var spinner = createDomainSpinner(); + var errorEl = createDomainLoadingError("Error loading label."); + + html.append(spinner); + html.append(errorEl); + + $('div#label .panel-body').append(html); + } + + function showOrHideAutomaticNetworking() { + var type = getCurrentDomainIDType(); + if (!accessTokenIsSet() || (type !== DOMAIN_ID_TYPE_FULL && type !== DOMAIN_ID_TYPE_UNKNOWN)) { + $("[data-keypath='metaverse.automatic_networking']").hide(); + return false; + } + $("[data-keypath='metaverse.automatic_networking']").show(); + return true; + } + + function setupDomainNetworkingSettings() { + if (!showOrHideAutomaticNetworking()) { + return; + } + + var autoNetworkingSetting = Settings.data.values.metaverse.automatic_networking; + if (autoNetworkingSetting === 'full') { + return; + } + + var includeAddress = autoNetworkingSetting === 'disabled'; + + if (includeAddress) { + var label = "Network Address:Port"; + } else { + var label = "Network Port"; + } + + var lowerName = name.toLowerCase(); + var form = '
    '; + form += ''; + form += ' Edit'; + form += ''; + form += '
    This defines how nodes will connect to your domain. You can read more about automatic networking here.
    '; + form += '
    '; + + form = $(form); + + form.find('#edit-network-address-port').click(function(ev) { + ev.preventDefault(); + + var address = DomainInfo.network_address === null ? '' : DomainInfo.network_address; + var port = DomainInfo.network_port === null ? '' : DomainInfo.network_port; + var modal_body = "
    "; + if (includeAddress) { + modal_body += ""; + modal_body += ""; + modal_body += "
    "; + } + modal_body += ""; + modal_body += ""; + modal_body += "
    "; + modal_body += "
    "; + + var dialog = bootbox.dialog({ + title: 'Edit Network', + message: modal_body, + closeButton: false, + onEscape: true, + buttons: [ + { + label: 'Cancel', + className: 'edit-network-cancel-btn', + callback: function() { + dialog.modal('hide'); + } + }, + { + label: 'Save', + className: 'edit-network-save-btn btn btn-primary', + callback: function() { + var data = { + network_port: $('#network-port-input').val() + }; + if (includeAddress) { + data.network_address = $('#network-address-input').val(); + } + + $('.edit-network-cancel-btn').attr('disabled', 'disabled'); + $('.edit-network-save-btn').attr('disabled', 'disabled'); + $('.edit-network-save-btn').html("Saving..."); + + console.log('data', data); + + $('.error-message').hide(); + + $.ajax({ + url: '/api/domains', + type: 'PUT', + data: data, + success: function(xhr) { + console.log(xhr, parseJSONResponse(xhr)); + dialog.modal('hide'); + reloadDomainInfo(); + }, + error:function(xhr) { + var data = parseJSONResponse(xhr); + console.log(data, data.status, data.data); + if (data.status === "fail") { + for (var key in data.data) { + var errorMsg = data.data[key]; + console.log(key, errorMsg); + var errorEl = $('.error-message[data-property="' + key + '"'); + console.log(errorEl); + errorEl.html(errorMsg); + errorEl.show(); + } + } + $('.edit-network-cancel-btn').removeAttr('disabled'); + $('.edit-network-save-btn').removeAttr('disabled'); + $('.edit-network-save-btn').html("Save"); + } + }); + return false; + } + } + ], + callback: function(result) { + console.log("result: ", result); + } + }); + }); + + var spinner = createDomainSpinner(); + + var errorMessage = '' + if (includeAddress) { + errorMessage = "We were unable to load the network address and port."; + } else { + errorMessage = "We were unable to load the network port." + } + var errorEl = createDomainLoadingError(errorMessage); + + var autoNetworkingEl = $('div[data-keypath="metaverse.automatic_networking"]'); + autoNetworkingEl.after(spinner); + autoNetworkingEl.after(errorEl); + autoNetworkingEl.after(form); + } + + + function setupPlacesTable() { + // create a dummy table using our view helper + var placesTableSetting = { + type: 'table', + name: 'places', + label: 'Places', + html_id: Settings.PLACES_TABLE_ID, + help: "The following places currently point to this domain.
    To point places to this domain, " + + " go to the My Places " + + "page in your High Fidelity Metaverse account.", + read_only: true, + can_add_new_rows: false, + columns: [ + { + "name": "name", + "label": "Name" + }, + { + "name": "path", + "label": "Viewpoint or Path" + }, + { + "name": "remove", + "label": "", + "class": "buttons" + } + ] + } + + // get a table for the places + var placesTableGroup = viewHelpers.getFormGroup('', placesTableSetting, Settings.data.values); + + // append the places table in the right place + $('#places_paths .panel-body').prepend(placesTableGroup); + //$('#' + Settings.PLACES_TABLE_ID).append(""); + + var spinner = createDomainSpinner(); + $('#' + Settings.PLACES_TABLE_ID).after($(spinner)); + + var errorEl = createDomainLoadingError("There was an error retreiving your places."); + $("#" + Settings.PLACES_TABLE_ID).after(errorEl); + + // do we have a domain ID? + if (Settings.data.values.metaverse.id.length == 0) { + // we don't have a domain ID - add a button to offer the user a chance to get a temporary one + var temporaryPlaceButton = dynamicButton(Settings.GET_TEMPORARY_NAME_BTN_ID, 'Get a temporary place name'); + $('#' + Settings.PLACES_TABLE_ID).after(temporaryPlaceButton); + } + if (accessTokenIsSet()) { + appendAddButtonToPlacesTable(); + } + } + + function placeTableRow(name, path, isTemporary, placeID) { + var name_link = "" + (isTemporary ? name + " (temporary)" : name) + ""; + + function placeEditClicked() { + editHighFidelityPlace(placeID, name, path); + } + + function placeDeleteClicked() { + var el = $(this); + var confirmString = Strings.REMOVE_PLACE_TITLE.replace("{{place}}", name); + var dialog = bootbox.dialog({ + message: confirmString, + closeButton: false, + onEscape: true, + buttons: [ + { + label: Strings.REMOVE_PLACE_CANCEL_BUTTON, + className: "delete-place-cancel-btn", + callback: function() { + dialog.modal('hide'); + } + }, + { + label: Strings.REMOVE_PLACE_DELETE_BUTTON, + className: "delete-place-confirm-btn btn btn-danger", + callback: function() { + $('.delete-place-cancel-btn').attr('disabled', 'disabled'); + $('.delete-place-confirm-btn').attr('disabled', 'disabled'); + $('.delete-place-confirm-btn').html(Strings.REMOVE_PLACE_DELETE_BUTTON_PENDING); + sendUpdatePlaceRequest( + placeID, + '', + null, + true, + function() { + reloadDomainInfo(); + dialog.modal('hide'); + }, function() { + $('.delete-place-cancel-btn').removeAttr('disabled'); + $('.delete-place-confirm-btn').removeAttr('disabled'); + $('.delete-place-confirm-btn').html(Strings.REMOVE_PLACE_DELETE_BUTTON); + bootbox.alert(Strings.REMOVE_PLACE_ERROR); + }); + return false; + } + }, + ] + }); + } + + if (isTemporary) { + var editLink = ""; + var deleteColumn = ""; + } else { + var editLink = " Edit"; + var deleteColumn = ""; + } + + var row = $("" + deleteColumn + ""); + row.find(".place-edit").click(placeEditClicked); + row.find(".place-delete").click(placeDeleteClicked); + + return row; + } + + function placeTableRowForPlaceObject(place) { + var placePathOrIndex = (place.path ? place.path : "/"); + return placeTableRow(place.name, placePathOrIndex, false, place.id); + } + + function reloadDomainInfo() { + $('#' + Settings.PLACES_TABLE_ID + " tbody tr").not('.headers').remove(); + + $('.domain-loading-show').show(); + $('.domain-loading-hide').hide(); + $('.domain-loading-error').hide(); + $('.loading-domain-info-spinner').show(); + $('#' + Settings.PLACES_TABLE_ID).append("Hello"); + + getDomainFromAPI(function(data){ + $('.loading-domain-info-spinner').hide(); + $('.domain-loading-show').hide(); + + // check if we have owner_places (for a real domain) or a name (for a temporary domain) + if (data.status == "success") { + $('.domain-loading-hide').show(); + if (data.domain.owner_places) { + // add a table row for each of these names + _.each(data.domain.owner_places, function(place){ + $('#' + Settings.PLACES_TABLE_ID + " tbody").append(placeTableRowForPlaceObject(place)); + }); + } else if (data.domain.name) { + // add a table row for this temporary domain name + $('#' + Settings.PLACES_TABLE_ID + " tbody").append(placeTableRow(data.domain.name, '/', true)); } - isNonDeletableRow = isNonDeletableRow - || (nonDeletableRowKey === col.name && nonDeletableRowValues.indexOf(colValue) !== -1); - - if (isArray && col.type === "checkbox" && col.editable) { - html += - ""; - } else if (isArray && col.type === "time" && col.editable) { - html += - ""; - } else { - // Use a hidden input so that the values are posted. - html += - ""; + // Update label + if (showOrHideLabel()) { + var label = data.domain.label; + label = label === null ? '' : label; + $('#network-label').val(label); } + // Update automatic networking + if (showOrHideAutomaticNetworking()) { + var autoNetworkingSetting = Settings.data.values.metaverse.automatic_networking; + var address = data.domain.network_address === null ? "" : data.domain.network_address; + var port = data.domain.network_port === null ? "" : data.domain.network_port; + if (autoNetworkingSetting === 'disabled') { + $('#network-address-port input').val(address + ":" + port); + } else if (autoNetworkingSetting === 'ip') { + $('#network-address-port input').val(port); + } + } + + if (accessTokenIsSet()) { + appendAddButtonToPlacesTable(); + } + + } else { + $('.domain-loading-error').show(); + } + }) + } + + function appendDomainIDButtons() { + var domainIDInput = $(Settings.DOMAIN_ID_SELECTOR); + + var createButton = dynamicButton(Settings.CREATE_DOMAIN_ID_BTN_ID, Strings.CREATE_DOMAIN_BUTTON); + createButton.css('margin-top', '10px'); + var chooseButton = dynamicButton(Settings.CHOOSE_DOMAIN_ID_BTN_ID, Strings.CHOOSE_DOMAIN_BUTTON); + chooseButton.css('margin', '10px 0px 0px 10px'); + + domainIDInput.after(chooseButton); + domainIDInput.after(createButton); + } + + function editHighFidelityPlace(placeID, name, path) { + var dialog; + + var modal_body = "
    "; + modal_body += ""; + modal_body += "
    "; + + var modal_buttons = [ + { + label: Strings.EDIT_PLACE_CANCEL_BUTTON, + className: "edit-place-cancel-button", + callback: function() { + dialog.modal('hide'); + } + }, + { + label: Strings.EDIT_PLACE_CONFIRM_BUTTON, + className: 'edit-place-save-button btn btn-primary', + callback: function() { + var placePath = $('#place-path-input').val(); + + if (path == placePath) { + return true; + } + + $('.edit-place-cancel-button').attr('disabled', 'disabled'); + $('.edit-place-save-button').attr('disabled', 'disabled'); + $('.edit-place-save-button').html(Strings.EDIT_PLACE_BUTTON_PENDING); + + sendUpdatePlaceRequest( + placeID, + placePath, + null, + false, + function() { + dialog.modal('hide') + reloadDomainInfo(); + }, + function() { + $('.edit-place-cancel-button').removeAttr('disabled'); + $('.edit-place-save-button').removeAttr('disabled'); + $('.edit-place-save-button').html(Strings.EDIT_PLACE_CONFIRM_BUTTON); + } + ); + + return false; + } + } + ]; + + dialog = bootbox.dialog({ + title: Strings.EDIT_PLACE_TITLE, + closeButton: false, + onEscape: true, + message: modal_body, + buttons: modal_buttons + }) + } + + function appendAddButtonToPlacesTable() { + var addRow = $(""); + addRow.find(".place-add").click(function(ev) { + ev.preventDefault(); + chooseFromHighFidelityPlaces(Settings.initialValues.metaverse.access_token, null, function(placeName, newDomainID) { + if (newDomainID) { + Settings.data.values.metaverse.id = newDomainID; + var domainIDEl = $("[data-keypath='metaverse.id']"); + domainIDEl.val(newDomainID); + Settings.initialValues.metaverse.id = newDomainID; + badgeForDifferences(domainIDEl); + } + reloadDomainInfo(); + }); + }); + $('#' + Settings.PLACES_TABLE_ID + " tbody").append(addRow); + } + + function chooseFromHighFidelityDomains(clickedButton) { + // setup the modal to help user pick their domain + if (Settings.initialValues.metaverse.access_token) { + + // add a spinner to the choose button + clickedButton.html("Loading domains..."); + clickedButton.attr('disabled', 'disabled'); + + // get a list of user domains from data-web + $.ajax({ + url: "/api/domains", + dataType: 'json', + jsonp: false, + success: function(data){ + + var modal_buttons = { + cancel: { + label: 'Cancel', + className: 'btn-default' + } + } + + if (data.data.domains.length) { + // setup a select box for the returned domains + modal_body = "

    Choose the High Fidelity domain you want this domain-server to represent.
    This will set your domain ID on the settings page.

    "; + domain_select = $(""); + _.each(data.data.domains, function(domain){ + var domainString = ""; + + if (domain.label) { + domainString += '"' + domain.label+ '" - '; + } + + domainString += domain.id; + + domain_select.append(""); + }) + modal_body += "" + domain_select[0].outerHTML + modal_buttons["success"] = { + label: 'Choose domain', + callback: function() { + domainID = $('#domain-name-select').val() + // set the domain ID on the form + $(Settings.DOMAIN_ID_SELECTOR).val(domainID).change(); + } + } + } else { + modal_buttons["success"] = { + label: 'Create new domain', + callback: function() { + window.open(URLs.METAVERSE_URL + "/user/domains", '_blank'); + } + } + modal_body = "

    You do not have any domains in your High Fidelity account." + + "

    Go to your domains page to create a new one. Once your domain is created re-open this dialog to select it.

    " + } + + bootbox.dialog({ + title: "Choose matching domain", + onEscape: true, + message: modal_body, + buttons: modal_buttons + }) + }, + error: function() { + bootbox.alert("Failed to retrieve your domains from the High Fidelity Metaverse"); + }, + complete: function() { + // remove the spinner from the choose button + clickedButton.html("Choose from my domains") + clickedButton.removeAttr('disabled') + } }); - if (!setting.read_only) { - if (setting.can_order) { - html += "" - } - if (isNonDeletableRow) { - html += ""; - } else { - html += ""; - } - } - - html += "" - - if (isCategorized && setting.can_add_new_rows && _.findLastIndex(setting_value, categoryPair) === rowIndexOrName) { - html += makeTableInputs(setting, categoryPair, categoryValue); - } - - row_num++ - }); - } - - // populate inputs in the table for new values - if (!setting.read_only) { - if (setting.can_add_new_categories) { - html += makeTableCategoryInput(setting, numVisibleColumns); - } - - if (setting.can_add_new_rows || setting.can_add_new_categories) { - html += makeTableHiddenInputs(setting, {}, ""); - } - } - html += "
    " + setting.caption + "
    " + group.label + "
    #" + setting.key.label + "" + col.label + "
    " + row_num + "" + rowIndexOrName + "
    " + name_link + "" + path + editLink + "
    " + - "" + - "" + - "" + - "" + - colValue + - "" + - "
    " - + "
    " - - return html; -} - -function makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns, canRemove, message) { - var html = - "" + - "" + - "" + - "" + categoryValue + "" + - "" + - ((canRemove) ? ( - "" + - "" + - "" - ) : ( - "" - )) + - ""; - return html; -} - -function makeTableHiddenInputs(setting, initialValues, categoryValue) { - var html = ""; - - if (setting.numbered === true) { - html += ""; - } - - if (setting.key) { - html += "\ - \ - " - } - - _.each(setting.columns, function(col) { - var defaultValue = _.has(initialValues, col.name) ? initialValues[col.name] : col.default; - if (col.type === "checkbox") { - html += - "" + - "" + - ""; - } else if (col.type === "select") { - html += "" - html += ""; - html += ""; - } else { - html += - "" + - "" + - ""; - } - }) - - if (setting.can_order) { - html += "" - } - html += "" - html += "" - - return html -} - -function makeTableCategoryInput(setting, numVisibleColumns) { - var canAddRows = setting.can_add_new_rows; - var categoryKey = setting.categorize_by_key; - var placeholder = setting.new_category_placeholder || ""; - var message = setting.new_category_message || ""; - var html = - "" + - "" + - "" + - "" + - "" + - "" + - "" + - ""; - return html; -} - -function getDescriptionForKey(key) { - for (var i in Settings.data.descriptions) { - if (Settings.data.descriptions[i].name === key) { - return Settings.data.descriptions[i]; - } - } -} - -var SAVE_BUTTON_LABEL_SAVE = "Save"; -var SAVE_BUTTON_LABEL_RESTART = "Save and restart"; -var reasonsForRestart = []; -var numChangesBySection = {}; - -function badgeSidebarForDifferences(changedElement) { - // figure out which group this input is in - var panelParentID = changedElement.closest('.panel').attr('id'); - - // if the panel contains non-grouped settings, the initial value is Settings.initialValues - var isGrouped = $('#' + panelParentID).hasClass('grouped'); - - if (isGrouped) { - var initialPanelJSON = Settings.initialValues[panelParentID] - ? Settings.initialValues[panelParentID] - : {}; - - // get a JSON representation of that section - var panelJSON = form2js(panelParentID, ".", false, cleanupFormValues, true)[panelParentID]; - } else { - var initialPanelJSON = Settings.initialValues; - - // get a JSON representation of that section - var panelJSON = form2js(panelParentID, ".", false, cleanupFormValues, true); - } - - var badgeValue = 0 - var description = getDescriptionForKey(panelParentID); - - // badge for any settings we have that are not the same or are not present in initialValues - for (var setting in panelJSON) { - if ((!_.has(initialPanelJSON, setting) && panelJSON[setting] !== "") || - (!_.isEqual(panelJSON[setting], initialPanelJSON[setting]) - && (panelJSON[setting] !== "" || _.has(initialPanelJSON, setting)))) { - badgeValue += 1; - - // add a reason to restart - if (description && description.restart != false) { - reasonsForRestart.push(setting); - } - } else { - // remove a reason to restart - if (description && description.restart != false) { - reasonsForRestart = $.grep(reasonsForRestart, function(v) { return v != setting; }); - } - } - } - - // update the list-group-item badge to have the new value - if (badgeValue == 0) { - badgeValue = "" - } - - numChangesBySection[panelParentID] = badgeValue; - - var hasChanges = badgeValue > 0; - - if (!hasChanges) { - for (var key in numChangesBySection) { - if (numChangesBySection[key] > 0) { - hasChanges = true; - break; - } - } - } - - $(".save-button").prop("disabled", !hasChanges); - $(".save-button").html(reasonsForRestart.length > 0 ? SAVE_BUTTON_LABEL_RESTART : SAVE_BUTTON_LABEL_SAVE); - $("a[href='#" + panelParentID + "'] .badge").html(badgeValue); -} - -function addTableRow(row) { - var table = row.parents('table'); - var isArray = table.data('setting-type') === 'array'; - var keepField = row.data("keep-field"); - - var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS); - - var input_clone = row.clone(); - - if (!isArray) { - // show the key input - var keyInput = row.children(".key").children("input"); - } - - // Change input row to data row - var table = row.parents("table"); - var setting_name = table.attr("name"); - row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS); - - // if this is an array, add the row index (which is the index of the last row + 1) - // as a data attribute to the row - var row_index = 0; - if (isArray) { - var previous_row = row.siblings('.' + Settings.DATA_ROW_CLASS + ':last'); - - if (previous_row.length > 0) { - row_index = parseInt(previous_row.attr(Settings.DATA_ROW_INDEX), 10) + 1; - } else { - row_index = 0; - } - - row.attr(Settings.DATA_ROW_INDEX, row_index); - } - - var focusChanged = false; - - _.each(row.children(), function(element) { - if ($(element).hasClass("numbered")) { - // Index row - var numbers = columns.children(".numbered") - if (numbers.length > 0) { - $(element).html(parseInt(numbers.last().text()) + 1) } else { - $(element).html(1) + bootbox.alert({ + message: "You must have an access token to query your High Fidelity domains.

    " + + "Please follow the instructions on the settings page to add an access token.", + title: "Access token required" + }) } - } else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) { - $(element).html("") - } else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { - // Change buttons - var anchor = $(element).children("a") - anchor.removeClass(Settings.ADD_ROW_SPAN_CLASSES) - anchor.addClass(Settings.DEL_ROW_SPAN_CLASSES) - } else if ($(element).hasClass("key")) { - var input = $(element).children("input") - input.show(); - } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) { - // show inputs - var input = $(element).find("input"); - input.show(); + } - var isCheckbox = input.hasClass("table-checkbox"); - var isDropdown = input.hasClass("table-dropdown"); + function createTemporaryDomain() { + swal({ + title: 'Create temporary place name', + text: "This will create a temporary place name and domain ID" + + " so other users can easily connect to your domain.

    " + + "In order to make your domain reachable, this will also enable full automatic networking.", + showCancelButton: true, + confirmButtonText: 'Create', + closeOnConfirm: false, + html: true + }, function(isConfirm){ + if (isConfirm) { + showSpinnerAlert('Creating temporary place name'); - if (isArray) { - var key = $(element).attr('name'); + // make a get request to get a temporary domain + $.post(URLs.METAVERSE_URL + '/api/v1/domains/temporary', function(data){ + if (data.status == "success") { + var domain = data.data.domain; - // are there multiple columns or just one? - // with multiple we have an array of Objects, with one we have an array of whatever the value type is - var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length - var newName = setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""); + // we should have a new domain ID - set it on the domain ID value + $(Settings.DOMAIN_ID_SELECTOR).val(domain.id).change(); - input.attr("name", newName); + // we also need to make sure auto networking is set to full + $('[data-hidden-input="metaverse.automatic_networking"]').val("full").change(); - if (isDropdown) { - // default values for hidden inputs inside child selects gets cleared so we need to remind it - var selectElement = $(element).children("select"); - selectElement.attr("data-hidden-input", newName); - $(element).children("input").val(selectElement.val()); - } - } else { - // because the name of the setting in question requires the key - // setup a hook to change the HTML name of the element whenever the key changes - var colName = $(element).attr("name"); - keyInput.on('change', function(){ - input.attr("name", setting_name + "." + $(this).val() + "." + colName); + swal({ + type: 'success', + title: 'Success!', + text: "We have created a temporary name and domain ID for you.

    " + + "Your temporary place name is " + domain.name + ".

    " + + "Press the button below to save your new settings and restart your domain-server.", + confirmButtonText: 'Save', + html: true + }, function(){ + saveSettings(); + }); + } }); } - - if (!focusChanged) { - input.focus(); - focusChanged = true; - } - - // if we are adding a dropdown, we should go ahead and make its select - // element is visible - if (isDropdown) { - $(element).children("select").attr("style", ""); - } - - if (isCheckbox) { - $(input).find("input").attr("data-changed", "true"); - } else { - input.attr("data-changed", "true"); - } - } else { - console.log("Unknown table element"); - } - }); - - input_clone.children('td').each(function () { - if ($(this).attr("name") !== keepField) { - $(this).find("input").val($(this).children('input').attr('data-default')); - } - }); - - if (isArray) { - updateDataChangedForSiblingRows(row, true) - - // the addition of any table row should remove the empty-array-row - row.siblings('.empty-array-row').remove() - } - - badgeSidebarForDifferences($(table)) - - row.after(input_clone) -} - -function deleteTableRow($row) { - var $table = $row.closest('table'); - var categoryName = $row.data("category"); - var isArray = $table.data('setting-type') === 'array'; - - $row.empty(); - - if (!isArray) { - if ($row.attr('name')) { - $row.html(""); - } else { - // for rows that didn't have a key, simply remove the row - $row.remove(); - } - } else { - if ($table.find('.' + Settings.DATA_ROW_CLASS + "[data-category='" + categoryName + "']").length <= 1) { - // This is the last row of the category, so delete the header - $table.find('.' + Settings.DATA_CATEGORY_CLASS + "[data-category='" + categoryName + "']").remove(); - } - - if ($table.find('.' + Settings.DATA_ROW_CLASS).length > 1) { - updateDataChangedForSiblingRows($row); - - // this isn't the last row - we can just remove it - $row.remove(); - } else { - // this is the last row, we can't remove it completely since we need to post an empty array - $row - .removeClass(Settings.DATA_ROW_CLASS) - .removeClass(Settings.NEW_ROW_CLASS) - .removeAttr("data-category") - .addClass('empty-array-row') - .html(""); - } - } - - // we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated - badgeSidebarForDifferences($table); -} - -function addTableCategory($categoryInputRow) { - var $input = $categoryInputRow.find("input").first(); - var categoryValue = $input.prop("value"); - if (!categoryValue || $categoryInputRow.closest("table").find("tr[data-category='" + categoryValue + "']").length !== 0) { - $categoryInputRow.addClass("has-warning"); - - setTimeout(function () { - $categoryInputRow.removeClass("has-warning"); - }, 400); - - return; - } - - var $rowInput = $categoryInputRow.next(".inputs").clone(); - if (!$rowInput) { - console.error("Error cloning inputs"); - } - - var canAddRows = $categoryInputRow.data("can-add-rows"); - var message = $categoryInputRow.data("message"); - var categoryKey = $categoryInputRow.data("key"); - var width = 0; - $categoryInputRow - .children("td") - .each(function () { - width += $(this).prop("colSpan") || 1; }); - - $input - .prop("value", "") - .focus(); - - $rowInput.find("td[name='" + categoryKey + "'] > input").first() - .prop("value", categoryValue); - $rowInput - .attr("data-category", categoryValue) - .addClass(Settings.NEW_ROW_CLASS); - - var $newCategoryRow = $(makeTableCategoryHeader(categoryKey, categoryValue, width, true, " - " + message)); - $newCategoryRow.addClass(Settings.NEW_ROW_CLASS); - - $categoryInputRow - .before($newCategoryRow) - .before($rowInput); - - if (canAddRows) { - $rowInput.removeAttr("hidden"); - } else { - addTableRow($rowInput); - } -} - -function deleteTableCategory($categoryHeaderRow) { - var categoryName = $categoryHeaderRow.data("category"); - - $categoryHeaderRow - .closest("table") - .find("tr[data-category='" + categoryName + "']") - .each(function () { - if ($(this).hasClass(Settings.DATA_ROW_CLASS)) { - deleteTableRow($(this)); - } else { - $(this).remove(); - } - }); -} - -function toggleTableCategory($categoryHeaderRow) { - var $icon = $categoryHeaderRow.find("." + Settings.TOGGLE_CATEGORY_SPAN_CLASS).first(); - var categoryName = $categoryHeaderRow.data("category"); - var wasExpanded = $icon.hasClass(Settings.TOGGLE_CATEGORY_EXPANDED_CLASS); - if (wasExpanded) { - $icon - .addClass(Settings.TOGGLE_CATEGORY_CONTRACTED_CLASS) - .removeClass(Settings.TOGGLE_CATEGORY_EXPANDED_CLASS); - } else { - $icon - .addClass(Settings.TOGGLE_CATEGORY_EXPANDED_CLASS) - .removeClass(Settings.TOGGLE_CATEGORY_CONTRACTED_CLASS); - } - $categoryHeaderRow - .closest("table") - .find("tr[data-category='" + categoryName + "']") - .toggleClass("contracted", wasExpanded); -} - -function moveTableRow(row, move_up) { - var table = $(row).closest('table') - var isArray = table.data('setting-type') === 'array' - if (!isArray) { - return; } - if (move_up) { - var prev_row = row.prev() - if (prev_row.hasClass(Settings.DATA_ROW_CLASS)) { - prev_row.before(row) - } - } else { - var next_row = row.next() - if (next_row.hasClass(Settings.DATA_ROW_CLASS)) { - next_row.after(row) - } - } - - // we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated - badgeSidebarForDifferences($(table)) -} - -function updateDataChangedForSiblingRows(row, forceTrue) { - // anytime a new row is added to an array we need to set data-changed for all sibling row inputs to true - // unless it matches the inital set of values - - if (!forceTrue) { - // figure out which group this row is in - var panelParentID = row.closest('.panel').attr('id') - // get the short name for the setting from the table - var tableShortName = row.closest('table').data('short-name') - - // get a JSON representation of that section - var panelSettingJSON = form2js(panelParentID, ".", false, cleanupFormValues, true)[panelParentID][tableShortName] - if (Settings.initialValues[panelParentID]) { - var initialPanelSettingJSON = Settings.initialValues[panelParentID][tableShortName] - } else { - var initialPanelSettingJSON = {}; - } - - // if they are equal, we don't need data-changed - isTrue = !_.isEqual(panelSettingJSON, initialPanelSettingJSON) - } else { - isTrue = true - } - - row.siblings('.' + Settings.DATA_ROW_CLASS).each(function(){ - var hiddenInput = $(this).find('td.' + Settings.DATA_COL_CLASS + ' input') - if (isTrue) { - hiddenInput.attr('data-changed', isTrue) - } else { - hiddenInput.removeAttr('data-changed') - } - }) -} - -function cleanupFormValues(node) { - if (node.type && node.type === 'checkbox') { - return { name: node.name, value: node.checked ? true : false }; - } else { - return false; - } -} - -function showErrorMessage(title, message) { - swal(title, message) -} +}); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 65053b7366..68a36195d9 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -501,7 +501,7 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) { // store the new domain ID and auto network setting immediately QString newSettingsJSON = QString("{\"metaverse\": { \"id\": \"%1\", \"automatic_networking\": \"full\"}}").arg(id); auto settingsDocument = QJsonDocument::fromJson(newSettingsJSON.toUtf8()); - _settingsManager.recurseJSONObjectAndOverwriteSettings(settingsDocument.object()); + _settingsManager.recurseJSONObjectAndOverwriteSettings(settingsDocument.object(), DomainSettings); // store the new ID and auto networking setting on disk _settingsManager.persistToFile(); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 05227d35b7..52754babb3 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -39,8 +39,10 @@ const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings const QString DESCRIPTION_SETTINGS_KEY = "settings"; const QString SETTING_DEFAULT_KEY = "default"; const QString DESCRIPTION_NAME_KEY = "name"; +const QString DESCRIPTION_GROUP_LABEL_KEY = "label"; const QString SETTING_DESCRIPTION_TYPE_KEY = "type"; const QString DESCRIPTION_COLUMNS_KEY = "columns"; +const QString CONTENT_SETTING_FLAG_KEY = "content_setting"; const QString SETTINGS_VIEWPOINT_KEY = "viewpoint"; @@ -63,6 +65,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() { if (descriptionObject.contains(DESCRIPTION_SETTINGS_KEY)) { _descriptionArray = descriptionDocument.object()[DESCRIPTION_SETTINGS_KEY].toArray(); + splitSettingsDescription(); + return; } } @@ -78,6 +82,91 @@ DomainServerSettingsManager::DomainServerSettingsManager() { Q_ARG(int, MISSING_SETTINGS_DESC_ERROR_CODE)); } +void DomainServerSettingsManager::splitSettingsDescription() { + // construct separate description arrays for domain settings and content settings + // since they are displayed on different pages + + // along the way we also construct one object that holds the groups separated by domain settings + // and content settings, so that the DS can setup dropdown menus below "Content" and "Settings" + // headers to jump directly to a settings group on the page of either + QJsonArray domainSettingsMenuGroups; + QJsonArray contentSettingsMenuGroups; + + foreach(const QJsonValue& group, _descriptionArray) { + QJsonObject groupObject = group.toObject(); + + static const QString HIDDEN_GROUP_KEY = "hidden"; + bool groupHidden = groupObject.contains(HIDDEN_GROUP_KEY) && groupObject[HIDDEN_GROUP_KEY].toBool(); + + QJsonArray domainSettingArray; + QJsonArray contentSettingArray; + + foreach(const QJsonValue& settingDescription, groupObject[DESCRIPTION_SETTINGS_KEY].toArray()) { + QJsonObject settingDescriptionObject = settingDescription.toObject(); + + bool isContentSetting = settingDescriptionObject.contains(CONTENT_SETTING_FLAG_KEY) + && settingDescriptionObject[CONTENT_SETTING_FLAG_KEY].toBool(); + + if (isContentSetting) { + // push the setting description to the pending content setting array + contentSettingArray.push_back(settingDescriptionObject); + } else { + // push the setting description to the pending domain setting array + domainSettingArray.push_back(settingDescriptionObject); + } + } + + if (!domainSettingArray.isEmpty() || !contentSettingArray.isEmpty()) { + + // we know for sure we'll have something to add to our settings menu groups + // so setup that object for the group now, as long as the group isn't hidden alltogether + QJsonObject settingsDropdownGroup; + + if (!groupHidden) { + settingsDropdownGroup[DESCRIPTION_NAME_KEY] = groupObject[DESCRIPTION_NAME_KEY]; + settingsDropdownGroup[DESCRIPTION_GROUP_LABEL_KEY] = groupObject[DESCRIPTION_GROUP_LABEL_KEY]; + + static const QString DESCRIPTION_GROUP_HTML_ID_KEY = "html_id"; + if (groupObject.contains(DESCRIPTION_GROUP_HTML_ID_KEY)) { + settingsDropdownGroup[DESCRIPTION_GROUP_HTML_ID_KEY] = groupObject[DESCRIPTION_GROUP_HTML_ID_KEY]; + } + } + + if (!domainSettingArray.isEmpty()) { + // we have some domain settings from this group, add the group with the filtered settings + QJsonObject filteredGroupObject = groupObject; + filteredGroupObject[DESCRIPTION_SETTINGS_KEY] = domainSettingArray; + _domainSettingsDescription.push_back(filteredGroupObject); + + // if the group isn't hidden, add its information to the domain settings menu groups + if (!groupHidden) { + domainSettingsMenuGroups.push_back(settingsDropdownGroup); + } + } + + if (!contentSettingArray.isEmpty()) { + // we have some content settings from this group, add the group with the filtered settings + QJsonObject filteredGroupObject = groupObject; + filteredGroupObject[DESCRIPTION_SETTINGS_KEY] = contentSettingArray; + _contentSettingsDescription.push_back(filteredGroupObject); + + // if the group isn't hidden, add its information to the content settings menu groups + if (!groupHidden) { + contentSettingsMenuGroups.push_back(settingsDropdownGroup); + } + } + } + } + + // populate the settings menu groups with what we've collected + + static const QString SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY = "domain_settings"; + static const QString SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY = "content_settings"; + + _settingsMenuGroups[SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY] = domainSettingsMenuGroups; + _settingsMenuGroups[SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY] = contentSettingsMenuGroups; +} + void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointer message) { Assignment::Type type; message->readPrimitive(&type); @@ -986,48 +1075,72 @@ QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QStrin } bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection *connection, const QUrl &url) { - if (connection->requestOperation() == QNetworkAccessManager::PostOperation && url.path() == SETTINGS_PATH_JSON) { - // this is a POST operation to change one or more settings - QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent()); - QJsonObject postedObject = postedDocument.object(); + if (connection->requestOperation() == QNetworkAccessManager::PostOperation) { + if (url.path() == SETTINGS_PATH_JSON || url.path() == CONTENT_SETTINGS_PATH_JSON) { + // this is a POST operation to change one or more settings + QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent()); + QJsonObject postedObject = postedDocument.object(); - // we recurse one level deep below each group for the appropriate setting - bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject); + SettingsType endpointType = url.path() == SETTINGS_PATH_JSON ? DomainSettings : ContentSettings; - // store whatever the current _settingsMap is to file - persistToFile(); + // we recurse one level deep below each group for the appropriate setting + bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject, endpointType); - // return success to the caller - QString jsonSuccess = "{\"status\": \"success\"}"; - connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json"); + // store whatever the current _settingsMap is to file + persistToFile(); - // defer a restart to the domain-server, this gives our HTTPConnection enough time to respond - if (restartRequired) { - const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000; - QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart())); - } else { - unpackPermissions(); - apiRefreshGroupInformation(); - emit updateNodePermissions(); - emit settingsUpdated(); + // return success to the caller + QString jsonSuccess = "{\"status\": \"success\"}"; + connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json"); + + // defer a restart to the domain-server, this gives our HTTPConnection enough time to respond + if (restartRequired) { + const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000; + QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart())); + } else { + unpackPermissions(); + apiRefreshGroupInformation(); + emit updateNodePermissions(); + emit settingsUpdated(); + } + + return true; } + } else if (connection->requestOperation() == QNetworkAccessManager::GetOperation) { + static const QString SETTINGS_MENU_GROUPS_PATH = "/settings-menu-groups.json"; - return true; - } else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH_JSON) { - // setup a JSON Object with descriptions and non-omitted settings - const QString SETTINGS_RESPONSE_DESCRIPTION_KEY = "descriptions"; - const QString SETTINGS_RESPONSE_VALUE_KEY = "values"; + if (url.path() == SETTINGS_PATH_JSON || url.path() == CONTENT_SETTINGS_PATH_JSON) { - QJsonObject rootObject; - rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = _descriptionArray; - rootObject[SETTINGS_RESPONSE_VALUE_KEY] = responseObjectForType("", true); - connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json"); + // setup a JSON Object with descriptions and non-omitted settings + const QString SETTINGS_RESPONSE_DESCRIPTION_KEY = "descriptions"; + const QString SETTINGS_RESPONSE_VALUE_KEY = "values"; + + QJsonObject rootObject; + + bool forDomainSettings = (url.path() == SETTINGS_PATH_JSON); + bool forContentSettings = (url.path() == CONTENT_SETTINGS_PATH_JSON);; + + rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = forDomainSettings + ? _domainSettingsDescription : _contentSettingsDescription; + + // grab a domain settings object for all types, filtered for the right class of settings + rootObject[SETTINGS_RESPONSE_VALUE_KEY] = responseObjectForType("", true, forDomainSettings, forContentSettings); + + connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json"); + + return true; + } else if (url.path() == SETTINGS_MENU_GROUPS_PATH) { + connection->respond(HTTPConnection::StatusCode200, QJsonDocument(_settingsMenuGroups).toJson(), "application/json"); + + return true; + } } return false; } -QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& typeValue, bool isAuthenticated) { +QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& typeValue, bool isAuthenticated, + bool includeDomainSettings, bool includeContentSettings) { QJsonObject responseObject; if (!typeValue.isEmpty() || isAuthenticated) { @@ -1036,8 +1149,15 @@ QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& ty const QString AFFECTED_TYPES_JSON_KEY = "assignment-types"; + QJsonArray& filteredDescriptionArray = _descriptionArray; + if (includeDomainSettings && !includeContentSettings) { + filteredDescriptionArray = _domainSettingsDescription; + } else if (includeContentSettings && !includeDomainSettings) { + filteredDescriptionArray = _contentSettingsDescription; + } + // enumerate the groups in the description object to find which settings to pass - foreach(const QJsonValue& groupValue, _descriptionArray) { + foreach(const QJsonValue& groupValue, filteredDescriptionArray) { QJsonObject groupObject = groupValue.toObject(); QString groupKey = groupObject[DESCRIPTION_NAME_KEY].toString(); QJsonArray groupSettingsArray = groupObject[DESCRIPTION_SETTINGS_KEY].toArray(); @@ -1045,10 +1165,13 @@ QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& ty QJsonObject groupResponseObject; foreach(const QJsonValue& settingValue, groupSettingsArray) { + const QString VALUE_HIDDEN_FLAG_KEY = "value-hidden"; QJsonObject settingObject = settingValue.toObject(); + // consider this setting as long as it isn't hidden + // and we've been asked to include this type (domain setting or content setting) if (!settingObject[VALUE_HIDDEN_FLAG_KEY].toBool()) { QJsonArray affectedTypesArray = settingObject[AFFECTED_TYPES_JSON_KEY].toArray(); if (affectedTypesArray.isEmpty()) { @@ -1212,7 +1335,8 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson return QJsonObject(); } -bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) { +bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, + SettingsType settingsType) { static const QString SECURITY_ROOT_KEY = "security"; static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist"; static const QString BROADCASTING_KEY = "broadcasting"; @@ -1222,6 +1346,8 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ auto& settingsVariant = _configMap.getConfig(); bool needRestart = false; + auto& filteredDescriptionArray = settingsType == DomainSettings ? _domainSettingsDescription : _contentSettingsDescription; + // Iterate on the setting groups foreach(const QString& rootKey, postedObject.keys()) { const QJsonValue& rootValue = postedObject[rootKey]; @@ -1236,7 +1362,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ QJsonObject groupDescriptionObject; // we need to check the description array to see if this is a root setting or a group setting - foreach(const QJsonValue& groupValue, _descriptionArray) { + foreach(const QJsonValue& groupValue, filteredDescriptionArray) { if (groupValue.toObject()[DESCRIPTION_NAME_KEY] == rootKey) { // we matched a group - keep this since we'll use it below to update the settings groupDescriptionObject = groupValue.toObject(); @@ -1269,6 +1395,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ if (!matchingDescriptionObject.isEmpty()) { updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject); + if (rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY && rootKey != SETTINGS_PATHS_KEY && rootKey != WIZARD_KEY) { needRestart = true; @@ -1286,6 +1413,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ if (!matchingDescriptionObject.isEmpty()) { const QJsonValue& settingValue = rootValue.toObject()[settingKey]; updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject); + if ((rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY && rootKey != DESCRIPTION_ROOT_KEY && rootKey != WIZARD_KEY) || settingKey == AC_SUBNET_WHITELIST_KEY) { diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index d2f6d1e526..5e13c9f28a 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -13,6 +13,7 @@ #define hifi_DomainServerSettingsManager_h #include +#include #include #include @@ -28,6 +29,7 @@ const QString SETTINGS_PATHS_KEY = "paths"; const QString SETTINGS_PATH = "/settings"; const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; +const QString CONTENT_SETTINGS_PATH_JSON = "/content-settings.json"; const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions"; const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions"; const QString IP_PERMISSIONS_KEYPATH = "security.ip_permissions"; @@ -38,6 +40,10 @@ const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens"; using GroupByUUIDKey = QPair; // groupID, rankID +enum SettingsType { + DomainSettings, + ContentSettings +}; class DomainServerSettingsManager : public QObject { Q_OBJECT @@ -123,8 +129,10 @@ private slots: private: QStringList _argumentList; - QJsonObject responseObjectForType(const QString& typeValue, bool isAuthenticated = false); - bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject); + QJsonArray filteredDescriptionArray(bool isContentSettings); + QJsonObject responseObjectForType(const QString& typeValue, bool isAuthenticated = false, + bool includeDomainSettings = true, bool includeContentSettings = true); + bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, SettingsType settingsType); void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap, const QJsonObject& settingDescription); @@ -132,8 +140,15 @@ private: void sortPermissions(); void persistToFile(); + void splitSettingsDescription(); + double _descriptionVersion; + QJsonArray _descriptionArray; + QJsonArray _domainSettingsDescription; + QJsonArray _contentSettingsDescription; + QJsonObject _settingsMenuGroups; + HifiConfigVariantMap _configMap; friend class DomainServer; diff --git a/libraries/embedded-webserver/src/HTTPManager.cpp b/libraries/embedded-webserver/src/HTTPManager.cpp index bd256578d8..fd127a2e92 100644 --- a/libraries/embedded-webserver/src/HTTPManager.cpp +++ b/libraries/embedded-webserver/src/HTTPManager.cpp @@ -79,7 +79,7 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, QHash redirectHeader; redirectHeader.insert(QByteArray("Location"), redirectLocation.toUtf8()); - connection->respond(HTTPConnection::StatusCode301, "", HTTPConnection::DefaultContentType, redirectHeader); + connection->respond(HTTPConnection::StatusCode302, "", HTTPConnection::DefaultContentType, redirectHeader); } // if the last thing is a trailing slash then we want to look for index file