diff --git a/js/languages/en-US.json b/js/languages/en-US.json
index ba909574a..fca80175d 100644
--- a/js/languages/en-US.json
+++ b/js/languages/en-US.json
@@ -317,6 +317,10 @@
},
"settings": {
"settingsLabel": "Settings",
+ "unrecognizedModelErrsWarning": {
+ "title": "Unable to save your settings",
+ "body": "We are unable to save the settings for this tab because there are unrecognized errors. They are probably coming from invalid values in another tab. Please save out the the other tabs in order to identify the field with an error. The error will be displayed above the field. After fixing the error, make sure to click Save on the tab the error was in. Afterwards, you may return to this tab to complete the save of this tab."
+ },
"generalTab": {
"helperLanguage": "Display interface elements in",
"helperCountry": "The country you primarily reside in",
diff --git a/js/models/BaseModel.js b/js/models/BaseModel.js
index 85937d575..0097cdfb8 100644
--- a/js/models/BaseModel.js
+++ b/js/models/BaseModel.js
@@ -108,6 +108,9 @@ export default class extends Model {
(attrs = {})[key] = val;
}
+ // take a snapshot of the attrs provided to this method
+ const setAttrs = JSON.parse(JSON.stringify(attrs));
+
const previousAttrs = this.toJSON();
// todo: will it break things if we unset a nested attribute?
@@ -148,7 +151,7 @@ export default class extends Model {
// account nested models, we'll fire our own event if any part of the
// model (including nested parts) change.
if (!_.isEqual(this.toJSON(), previousAttrs)) {
- this.trigger('someChange', this, {});
+ this.trigger('someChange', this, { setAttrs });
}
return superSet;
diff --git a/js/views/modals/Settings/Addresses.js b/js/views/modals/Settings/Addresses.js
index ff0997afb..bfeb0ce68 100644
--- a/js/views/modals/Settings/Addresses.js
+++ b/js/views/modals/Settings/Addresses.js
@@ -14,12 +14,14 @@ export default class extends baseVw {
});
this.settings = app.settings.clone();
- this.listenTo(this.settings, 'sync', (md, resp, syncOpts) => {
- // Since different tabs are working off different parts of
- // the settings model, to not overwrite each other, we'll only
- // update fields that our tab has changed.
- app.settings.set(syncOpts.attrs);
- });
+
+ // Sync our clone with any changes made to the global settings model.
+ this.listenTo(app.settings, 'someChange', (md, opts) =>
+ this.settings.set(opts.setAttrs));
+
+ // Sync the global settings model with any changes we save via our clone.
+ this.listenTo(this.settings, 'sync', (md, resp, opts) =>
+ app.settings.set(this.settings.toJSON(opts.attrs)));
this.addressForm = this.createChild(AddressesForm, { model: new ShippingAddress() });
@@ -38,7 +40,14 @@ export default class extends baseVw {
const shippingAddresses = this.settings.get('shippingAddresses');
const removeIndex = shippingAddresses.indexOf(address);
- shippingAddresses.remove(address);
+ this.settings.set({}, { validate: true });
+
+ if (!this.settings.validationError) {
+ shippingAddresses.remove(address);
+ } else {
+ this.trigger('unrecognizedModelError', this, [this.settings]);
+ return;
+ }
const save = this.settings.save({ shippingAddresses: shippingAddresses.toJSON() }, {
attrs: { shippingAddresses: shippingAddresses.toJSON() },
@@ -95,8 +104,9 @@ export default class extends baseVw {
model.set(formData);
model.set(formData, { validate: true });
+ this.settings.set({}, { validate: true });
- if (!model.validationError) {
+ if (!this.settings.validationError) {
const shippingAddresses = this.settings.get('shippingAddresses');
shippingAddresses.push(model);
@@ -107,6 +117,7 @@ export default class extends baseVw {
});
if (save) {
+ this.$btnAddAddress.addClass('processing');
const truncatedName = model.get('name').slice(0, 30);
const msg = {
@@ -158,10 +169,16 @@ export default class extends baseVw {
// render so errors are shown / cleared
this.addressForm.render();
- if (!model.validationError) this.$btnAddAddress.addClass('processing');
- const $firstFormErr = this.$('.js-formContainer .errorList:first');
- if ($firstFormErr.length) $firstFormErr[0].scrollIntoViewIfNeeded();
+ if (this.settings.validationError) {
+ const $firstFormErr = this.$('.js-formContainer .errorList:first');
+
+ if ($firstFormErr.length) {
+ $firstFormErr[0].scrollIntoViewIfNeeded();
+ } else {
+ this.trigger('unrecognizedModelError', this, [this.settings]);
+ }
+ }
}
get $btnAddAddress() {
diff --git a/js/views/modals/Settings/General.js b/js/views/modals/Settings/General.js
index 6aa720149..45ed7d76a 100644
--- a/js/views/modals/Settings/General.js
+++ b/js/views/modals/Settings/General.js
@@ -17,17 +17,22 @@ export default class extends baseVw {
this.settings = app.settings.clone();
- this.listenTo(this.settings, 'sync', (md, resp, syncOpts) => {
- // Since different tabs are working off different parts of
- // the settings model, to not overwrite each other, we'll only
- // update fields that our tab has changed.
- app.settings.set(syncOpts.attrs);
- });
+ // Sync our clone with any changes made to the global settings model.
+ this.listenTo(app.settings, 'someChange',
+ (md, opts) => this.settings.set(opts.setAttrs));
+
+ // Sync the global settings model with any changes we save via our clone.
+ this.listenTo(this.settings, 'sync', (md, resp, opts) => app.settings.set(opts.attrs));
this.localSettings = app.localSettings.clone();
+ // Sync our clone with any changes made to the global local settings model.
+ this.listenTo(this.localSettings, 'sync',
+ (md, resp, opts) => app.localSettings.set(this.localSettings.toJSON(opts.attrs)));
+
+ // Sync the global local settings model with any changes we save via our clone.
this.listenTo(this.localSettings, 'sync',
- () => app.localSettings.set(this.localSettings.toJSON()));
+ (md, resp, opts) => app.localSettings.set(opts.attrs));
this.countryList = getTranslatedCountries(app.settings.get('language'));
this.currencyList = getTranslatedCurrencies(app.settings.get('language'));
@@ -107,12 +112,21 @@ export default class extends baseVw {
}
this.render();
+
if (!this.localSettings.validationError && !this.settings.validationError) {
this.$btnSave.addClass('processing');
+ } else {
+ const $firstErr = this.$('.errorList:first');
+
+ if ($firstErr.length) {
+ $firstErr[0].scrollIntoViewIfNeeded();
+ } else {
+ const models = [];
+ if (this.localSettings.validationError) models.push(this.localSettings);
+ if (this.settings.validationError) models.push(this.settings);
+ this.trigger('unrecognizedModelError', this, models);
+ }
}
-
- const $firstErr = this.$('.errorList:first');
- if ($firstErr.length) $firstErr[0].scrollIntoViewIfNeeded();
}
get $btnSave() {
diff --git a/js/views/modals/Settings/Moderation.js b/js/views/modals/Settings/Moderation.js
index f6a455fd5..55b12e84b 100644
--- a/js/views/modals/Settings/Moderation.js
+++ b/js/views/modals/Settings/Moderation.js
@@ -19,6 +19,14 @@ export default class extends baseVw {
this.profile = app.profile.clone();
+ // Sync our clone with any changes made to the global profile.
+ this.listenTo(app.profile, 'someChange',
+ (md, opts) => this.profile.set(opts.setAttrs));
+
+ // Sync the global profile with any changes we save via our clone.
+ this.listenTo(this.profile, 'sync',
+ (md, resp, opts) => app.profile.set(this.profile.toJSON(opts.attrs)));
+
if (this.profile.get('moderatorInfo')) {
this.moderator = this.profile.get('moderatorInfo');
} else {
@@ -117,10 +125,17 @@ export default class extends baseVw {
// render so errrors are shown / cleared
this.render();
- if (save) this.$btnSave.addClass('processing');
+ if (save) {
+ this.$btnSave.addClass('processing');
+ } else {
+ const $firstErr = this.$('.errorList:first');
- const $firstErr = this.$('.errorList:first');
- if ($firstErr.length) $firstErr[0].scrollIntoViewIfNeeded();
+ if ($firstErr.length) {
+ $firstErr[0].scrollIntoViewIfNeeded();
+ } else {
+ this.trigger('unrecognizedModelError', this, [this.profile]);
+ }
+ }
}
changeFeeType(e) {
diff --git a/js/views/modals/Settings/Page.js b/js/views/modals/Settings/Page.js
index cd46d950e..f06b15c40 100644
--- a/js/views/modals/Settings/Page.js
+++ b/js/views/modals/Settings/Page.js
@@ -21,7 +21,14 @@ export default class extends baseVw {
this.headerMinHeight = 700;
this.profile = app.profile.clone();
- this.listenTo(this.profile, 'sync', () => app.profile.set(this.profile.toJSON()));
+
+ // Sync our clone with any changes made to the global profile.
+ this.listenTo(app.profile, 'someChange',
+ (md, opts) => this.profile.set(opts.setAttrs));
+
+ // Sync the global profile with any changes we save via our clone.
+ this.listenTo(this.profile, 'sync',
+ (md, resp, syncOpts) => app.profile.set(this.profile.toJSON(syncOpts.attrs)));
this.socialAccounts = this.createChild(SocialAccounts, {
collection: this.profile.get('contactInfo').get('social'),
@@ -223,10 +230,18 @@ export default class extends baseVw {
}
this.render();
- if (save) this.$btnSave.addClass('processing');
- const $firstErr = this.$('.errorList:first');
- if ($firstErr.length) $firstErr[0].scrollIntoViewIfNeeded();
+ if (save) {
+ this.$btnSave.addClass('processing');
+ } else {
+ const $firstErr = this.$('.errorList:first');
+
+ if ($firstErr.length) {
+ $firstErr[0].scrollIntoViewIfNeeded();
+ } else {
+ this.trigger('unrecognizedModelError', this, [this.profile]);
+ }
+ }
}
get $btnSave() {
diff --git a/js/views/modals/Settings/Settings.js b/js/views/modals/Settings/Settings.js
index 4d1d8dfe2..954dc7244 100644
--- a/js/views/modals/Settings/Settings.js
+++ b/js/views/modals/Settings/Settings.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import app from '../../../app';
+import { openSimpleMessage } from '../../modals/SimpleMessage';
import loadTemplate from '../../../utils/loadTemplate';
import BaseModal from '../BaseModal';
import General from './General';
@@ -55,6 +56,18 @@ export default class extends BaseModal {
this.selectTab(targ);
}
+ onUnrecognizedModelError(tabView, models = []) {
+ const errors = models.map(md => {
+ const errObj = md.validationError || {};
+ return Object.keys(errObj).map(key => `${key}: ${errObj[key]}`);
+ });
+
+ const body = app.polyglot.t('settings.unrecognizedModelErrsWarning.body') +
+ (errors.length ? `
${errors.join('
')}` : '');
+
+ openSimpleMessage(app.polyglot.t('settings.unrecognizedModelErrsWarning.title'), body);
+ }
+
selectTab(targ, options = {}) {
const tabViewName = targ.data('tab');
let tabView = this.tabViewCache[tabViewName];
@@ -68,6 +81,7 @@ export default class extends BaseModal {
tabView = this.createChild(this.tabViews[tabViewName]);
this.tabViewCache[tabViewName] = tabView;
tabView.render();
+ this.listenTo(tabView, 'unrecognizedModelError', this.onUnrecognizedModelError);
}
this.$tabContent.append(tabView.$el);
diff --git a/js/views/modals/Settings/Store.js b/js/views/modals/Settings/Store.js
index 2cc72fc25..c848ac5f9 100644
--- a/js/views/modals/Settings/Store.js
+++ b/js/views/modals/Settings/Store.js
@@ -18,7 +18,25 @@ export default class extends baseVw {
});
this.profile = app.profile.clone();
+
+ // Sync our clone with any changes made to the global profile.
+ this.listenTo(app.profile, 'someChange',
+ (md, opts) => this.profile.set(opts.setAttrs));
+
+ // Sync the global profile with any changes we save via our clone.
+ this.listenTo(this.profile, 'sync',
+ (md, resp, opts) => app.profile.set(this.profile.toJSON(opts.attrs)));
+
this.settings = app.settings.clone();
+
+ // Sync our clone with any changes made to the global settings model.
+ this.listenTo(app.settings, 'someChange',
+ (md, opts) => this.settings.set(opts.setAttrs));
+
+ // Sync the global settings model with any changes we save via our clone.
+ this.listenTo(this.settings, 'sync',
+ (md, resp, opts) => app.settings.set(this.settings.toJSON(opts.attrs)));
+
this.currentMods = this.settings.get('storeModerators');
this.modsSelected = new Moderators(null, {
@@ -104,9 +122,6 @@ export default class extends baseVw {
this.listenTo(this.modsAvailable, 'doneLoading', () => {
this.doneLoading(this.$modListAvailable);
});
-
- this.listenTo(this.profile, 'sync', () => app.profile.set(this.profile.toJSON()));
- this.listenTo(this.settings, 'sync', () => app.settings.set(this.settings.toJSON()));
}
events() {
@@ -292,9 +307,13 @@ export default class extends baseVw {
const settingsFormData = this.getSettingsData();
this.profile.set(profileFormData);
+ this.profile.set(profileFormData, { validate: true });
this.settings.set(settingsFormData);
+ this.settings.set(settingsFormData, { validate: true });
if (!this.profile.validationError && !this.settings.validationError) {
+ this.$btnSave.addClass('processing');
+
const msg = {
msg: app.polyglot.t('settings.storeTab.status.saving'),
type: 'message',
@@ -366,13 +385,18 @@ export default class extends baseVw {
setTimeout(() => statusMessage.remove(), 3000);
this.render();
});
- }
- if (!this.profile.validationError && !this.settings.validationError) {
- this.$btnSave.addClass('processing');
- }
+ } else {
+ const $firstErr = this.$('.errorList:first:not(.hide)');
- const $firstErr = this.$('.errorList:first');
- if ($firstErr.length) $firstErr[0].scrollIntoViewIfNeeded();
+ if ($firstErr.length) {
+ $firstErr[0].scrollIntoViewIfNeeded();
+ } else {
+ const models = [];
+ if (this.profile.validationError) models.push(this.profile);
+ if (this.settings.validationError) models.push(this.settings);
+ this.trigger('unrecognizedModelError', this, models);
+ }
+ }
}
get $btnSave() {
diff --git a/js/views/modals/Settings/advanced/Advanced.js b/js/views/modals/Settings/advanced/Advanced.js
index 293347843..902ecb02c 100644
--- a/js/views/modals/Settings/advanced/Advanced.js
+++ b/js/views/modals/Settings/advanced/Advanced.js
@@ -16,17 +16,24 @@ export default class extends baseVw {
});
this.settings = app.settings.clone();
+
+ // Sync our clone with any changes made to the global settings model.
+ this.listenTo(app.settings, 'someChange',
+ (md, opts) => this.settings.set(opts.setAttrs));
+
+ // Sync the global settings model with any changes we save via our clone.
+ this.listenTo(this.settings, 'sync',
+ (md, resp, opts) => app.settings.set(this.settings.toJSON(opts.attrs)));
+
this.localSettings = app.localSettings.clone();
- this.listenTo(this.localSettings, 'sync',
- () => app.localSettings.set(this.localSettings.toJSON()));
+ // Sync our clone with any changes made to the global local settings model.
+ this.listenTo(app.localSettings, 'someChange',
+ (md, opts) => this.localSettings.set(opts.setAttrs));
- this.listenTo(this.settings, 'sync', (md, resp, syncOpts) => {
- // Since different tabs are working off different parts of
- // the settings model, to not overwrite each other, we'll only
- // update fields that our tab has changed.
- app.settings.set(syncOpts.attrs);
- });
+ // Sync the global local settings model with any changes we save via our clone.
+ this.listenTo(this.localSettings, 'sync',
+ (md, resp, opts) => app.localSettings.set(this.localSettings.toJSON(opts.attrs)));
}
get events() {
@@ -223,12 +230,21 @@ export default class extends baseVw {
}
this.render();
+
if (!this.localSettings.validationError && !this.settings.validationError) {
this.getCachedEl('.js-save').addClass('processing');
+ } else {
+ const $firstErr = this.$('.errorList:first');
+
+ if ($firstErr.length) {
+ $firstErr[0].scrollIntoViewIfNeeded();
+ } else {
+ const models = [];
+ if (this.localSettings.validationError) models.push(this.localSettings);
+ if (this.settings.validationError) models.push(this.settings);
+ this.trigger('unrecognizedModelError', this, models);
+ }
}
-
- const $firstErr = this.$('.errorList:first');
- if ($firstErr.length) $firstErr[0].scrollIntoViewIfNeeded();
}
get $smtpSettingsFields() {