* fix: eslint operator-linebreak issue * fix: eslint quotes issue * fix: react jsx indent and props issues * fix: eslint trailing spaces issues * fix: eslint line around directives issue * fix: eslint semi rule * fix: eslint newline per chain rule * fix: eslint space infix ops rule * fix: eslint space-in-parens issue * fix: eslint space before function paren issue * fix: eslint space before blocks issue * fix: eslint arrow body style issue * fix: eslint dot-location issue * fix: eslint quotes issue * fix: eslint quote props issue * fix: eslint operator assignment issue * fix: eslint new line after import issue * fix: indent issues * fix: operator assignment issue * fix: all autofixable eslint issues * fix: all react related fixable issues * fix: autofixable eslint issues * chore: remove all template literals * fix: remaining autofixable issues * chore: apply amnesty on all existing issues * fix: failing xss-lint issues * refactor: apply amnesty on remaining issues * refactor: apply amnesty on new issues * fix: remove file level suppressions * refactor: apply amnesty on new issues
205 lines
8.7 KiB
JavaScript
205 lines
8.7 KiB
JavaScript
define(['js/views/validation',
|
|
'jquery',
|
|
'underscore',
|
|
'gettext',
|
|
'codemirror',
|
|
'js/views/modals/validation_error_modal',
|
|
'edx-ui-toolkit/js/utils/html-utils'],
|
|
function(ValidatingView, $, _, gettext, CodeMirror, ValidationErrorModal, HtmlUtils) {
|
|
var AdvancedView = ValidatingView.extend({
|
|
error_saving: 'error_saving',
|
|
successful_changes: 'successful_changes',
|
|
render_deprecated: false,
|
|
|
|
// Model class is CMS.Models.Settings.Advanced
|
|
events: {
|
|
'focus :input': 'focusInput',
|
|
'blur :input': 'blurInput'
|
|
// TODO enable/disable save based on validation (currently enabled whenever there are changes)
|
|
},
|
|
initialize: function() {
|
|
this.template = HtmlUtils.template(
|
|
$('#advanced_entry-tpl').text()
|
|
);
|
|
this.listenTo(this.model, 'invalid', this.handleValidationError);
|
|
this.render();
|
|
},
|
|
render: function() {
|
|
// catch potential outside call before template loaded
|
|
if (!this.template) { return this; }
|
|
|
|
var listEle$ = this.$el.find('.course-advanced-policy-list');
|
|
listEle$.empty();
|
|
|
|
// b/c we've deleted all old fields, clear the map and repopulate
|
|
this.fieldToSelectorMap = {};
|
|
this.selectorToField = {};
|
|
|
|
// iterate through model and produce key : value editors for each property in model.get
|
|
var self = this;
|
|
_.each(_.sortBy(_.keys(this.model.attributes), function(key) { return self.model.get(key).display_name; }),
|
|
function(key) {
|
|
if (self.render_deprecated || !self.model.get(key).deprecated) {
|
|
HtmlUtils.append(listEle$, self.renderTemplate(key, self.model.get(key)));
|
|
}
|
|
});
|
|
|
|
this.codeMirrors = [];
|
|
var policyValues = listEle$.find('.json');
|
|
_.each(policyValues, this.attachJSONEditor, this);
|
|
return this;
|
|
},
|
|
attachJSONEditor: function(textarea) {
|
|
// Since we are allowing duplicate keys at the moment, it is possible that we will try to attach
|
|
// JSON Editor to a value that already has one. Therefore only attach if no CodeMirror peer exists.
|
|
if ($(textarea).siblings().hasClass('CodeMirror')) {
|
|
return;
|
|
}
|
|
|
|
var self = this;
|
|
var oldValue = $(textarea).val();
|
|
var cm = CodeMirror.fromTextArea(textarea, {
|
|
mode: 'application/json',
|
|
lineNumbers: false,
|
|
lineWrapping: false
|
|
});
|
|
cm.on('change', function(instance, changeobj) {
|
|
instance.save();
|
|
// this event's being called even when there's no change :-(
|
|
if (instance.getValue() !== oldValue) {
|
|
var message = gettext('Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.');
|
|
self.showNotificationBar(message,
|
|
_.bind(self.saveView, self),
|
|
_.bind(self.revertView, self));
|
|
}
|
|
});
|
|
cm.on('focus', function(mirror) {
|
|
$(textarea).parent().children('label').addClass('is-focused');
|
|
});
|
|
cm.on('blur', function(mirror) {
|
|
$(textarea).parent().children('label').removeClass('is-focused');
|
|
var key = $(mirror.getWrapperElement()).closest('.field-group').children('.key').attr('id');
|
|
var stringValue = $.trim(mirror.getValue());
|
|
// update CodeMirror to show the trimmed value.
|
|
mirror.setValue(stringValue);
|
|
// eslint-disable-next-line no-undef-init
|
|
var JSONValue = undefined;
|
|
try {
|
|
JSONValue = JSON.parse(stringValue);
|
|
} catch (e) {
|
|
// If it didn't parse, try converting non-arrays/non-objects to a String.
|
|
// But don't convert single-quote strings, which are most likely errors.
|
|
var firstNonWhite = stringValue.substring(0, 1);
|
|
if (firstNonWhite !== '{' && firstNonWhite !== '[' && firstNonWhite !== "'") {
|
|
try {
|
|
stringValue = '"' + stringValue + '"';
|
|
JSONValue = JSON.parse(stringValue);
|
|
mirror.setValue(stringValue);
|
|
} catch (quotedE) {
|
|
// TODO: validation error
|
|
// console.log("Error with JSON, even after converting to String.");
|
|
// console.log(quotedE);
|
|
JSONValue = undefined;
|
|
}
|
|
}
|
|
}
|
|
if (JSONValue !== undefined) {
|
|
var modelVal = self.model.get(key);
|
|
modelVal.value = JSONValue;
|
|
self.model.set(key, modelVal);
|
|
}
|
|
});
|
|
this.codeMirrors.push(cm);
|
|
},
|
|
validateJSON: function() {
|
|
var jsonValidationErrors = [];
|
|
_.each(this.codeMirrors, function(mirror) {
|
|
var keyDiv = $(mirror.getWrapperElement()).closest('.field-group').children('.key');
|
|
var key = keyDiv.attr('id');
|
|
var displayName = keyDiv.children('.title').text();
|
|
var stringValue = mirror.getValue();
|
|
try {
|
|
JSON.parse(stringValue);
|
|
} catch (e) {
|
|
jsonValidationErrors.push({
|
|
key: key,
|
|
message: 'Incorrectly formatted JSON',
|
|
model: {display_name: displayName}
|
|
});
|
|
}
|
|
});
|
|
return jsonValidationErrors;
|
|
},
|
|
saveView: function() {
|
|
var self = this;
|
|
var jsonValidationErrors = self.validateJSON();
|
|
if (jsonValidationErrors.length) {
|
|
self.showErrorModal(jsonValidationErrors);
|
|
return;
|
|
}
|
|
this.model.save({}, {
|
|
success: function() {
|
|
var title = gettext('Your policy changes have been saved.');
|
|
var message = gettext('No validation is performed on policy keys or value pairs. If you are having difficulties, check your formatting.'); // eslint-disable-line max-len
|
|
self.render();
|
|
self.showSavedBar(title, message);
|
|
analytics.track('Saved Advanced Settings', {
|
|
course: course_location_analytics
|
|
});
|
|
},
|
|
silent: true,
|
|
error: function(model, response, options) {
|
|
var jsonResponse;
|
|
|
|
/* Check that the server came back with a bad request error */
|
|
if (response.status === 400) {
|
|
jsonResponse = $.parseJSON(response.responseText);
|
|
self.showErrorModal(jsonResponse);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
showErrorModal: function(content) {
|
|
/* initialize and show validation error modal */
|
|
var self, errModal;
|
|
self = this;
|
|
errModal = new ValidationErrorModal();
|
|
errModal.setContent(content);
|
|
errModal.setResetCallback(function() { self.revertView(); });
|
|
errModal.show();
|
|
},
|
|
revertView: function() {
|
|
var self = this;
|
|
this.model.fetch({
|
|
success: function() { self.render(); },
|
|
reset: true
|
|
});
|
|
},
|
|
renderTemplate: function(key, model) {
|
|
var newKeyId = _.uniqueId('policy_key_'),
|
|
newEle = this.template({
|
|
key: key,
|
|
display_name: model.display_name,
|
|
help: model.help,
|
|
value: JSON.stringify(model.value, null, 4),
|
|
deprecated: model.deprecated,
|
|
keyUniqueId: newKeyId,
|
|
valueUniqueId: _.uniqueId('policy_value_'),
|
|
hidden: model.hidden
|
|
});
|
|
|
|
this.fieldToSelectorMap[key] = newKeyId;
|
|
this.selectorToField[newKeyId] = key;
|
|
return newEle;
|
|
},
|
|
focusInput: function(event) {
|
|
$(event.target).prev().addClass('is-focused');
|
|
},
|
|
blurInput: function(event) {
|
|
$(event.target).prev().removeClass('is-focused');
|
|
}
|
|
});
|
|
|
|
return AdvancedView;
|
|
});
|