From 7db19d02d34c1a12609f275f70ca808379b26843 Mon Sep 17 00:00:00 2001 From: Maciej Lewinski Date: Tue, 3 May 2022 14:48:32 +0200 Subject: [PATCH 1/5] Modified jest configuration --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b7231b3..33b4939 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ ], "testPathIgnorePatterns": [ "/fixtures/" - ] + ], + "testURL": "https://localhost" } } From ed0635848a8ab7eafb04b99797b687dcbbd12e51 Mon Sep 17 00:00:00 2001 From: Maciej Lewinski Date: Tue, 3 May 2022 18:45:21 +0200 Subject: [PATCH 2/5] Store save initial implemenattion --- src/Model.js | 76 ++-- src/Store.js | 41 ++- src/__tests__/Store.js | 580 +++++++++++++++++++++++++++++++ src/__tests__/fixtures/Animal.js | 8 + 4 files changed, 667 insertions(+), 38 deletions(-) diff --git a/src/Model.js b/src/Model.js index fba8bde..9dd3858 100644 --- a/src/Model.js +++ b/src/Model.js @@ -764,14 +764,7 @@ export default class Model { return Promise.resolve(res); }); })) - .catch( - action(err => { - if (err.valErrors) { - this.parseValidationErrors(err.valErrors); - } - throw err; - }) - ) + .catch(err => this._catchSave(err)) ); } @@ -791,39 +784,48 @@ export default class Model { }), requestOptions: omit(options, 'relations', 'data', 'mapData'), }) - .then(action(res => { - this.saveFromBackend(res); - this.clearUserFieldChanges(); + .then(res => this._afterSaveAll(res, options)) + .catch(err => this._catchSave(err)) + ); + } - forNestedRelations(this, relationsToNestedKeys(options.relations || []), relation => { - if (relation instanceof Model) { - relation.clearUserFieldChanges(); - } else { - relation.clearSetChanges(); - } - }); + @action + _afterSaveAll(res, options) { + this.saveFromBackend(res); + this.clearUserFieldChanges(); - return this.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(() => { - this.clearUserFileChanges(); + forNestedRelations(this, relationsToNestedKeys(options.relations || []), relation => { + if (relation instanceof Model) { + relation.clearUserFieldChanges(); + } else { + relation.clearSetChanges(); + } + }); - forNestedRelations(this, relationsToNestedKeys(options.relations || []), relation => { - if (relation instanceof Model) { - relation.clearUserFileChanges(); - } - }); + return this.saveAllFiles(relationsToNestedKeys(options.relations || [])) + .then(() => this._afterSaveAllFiles(options)) + .then(() => res); + } - return res; - }); - })) - .catch( - action(err => { - if (err.valErrors) { - this.parseValidationErrors(err.valErrors); - } - throw err; - }) - ) - ); + @action + _afterSaveAllFiles(options) { + this.clearUserFileChanges(); + + forNestedRelations(this, relationsToNestedKeys(options.relations || []), relation => { + if (relation instanceof Model) { + relation.clearUserFileChanges(); + } + }); + + return; + } + + @action + _catchSave(err) { + if (err.valErrors) { + this.parseValidationErrors(err.valErrors); + } + throw err; } // After saving a model, we should get back an ID mapping from the backend which looks like: diff --git a/src/Store.js b/src/Store.js index 971e964..aceb576 100644 --- a/src/Store.js +++ b/src/Store.js @@ -11,7 +11,8 @@ import { result, uniqBy, } from 'lodash'; -import { invariant } from './utils'; +import { invariant, relationsToNestedKeys } from './utils'; + const AVAILABLE_CONST_OPTIONS = [ 'relations', 'limit', @@ -500,4 +501,42 @@ export default class Store { } return Promise.all(promises); } + + save(options = {}) { + return this._saveAll(options); + } + + @action + _saveAll(options = {}) { + this.clearValidationErrors(); + return this.wrapPendingRequestCount( + this.__getApi() + .saveAllModels({ + url: result(this, 'url'), + model: this, + data: this.toBackendAll({ + data: options.data, + mapData: options.mapData, + nestedRelations: relationsToNestedKeys(options.relations || []), + onlyChanges: options.onlyChanges, + }), + requestOptions: omit(options, 'relations', 'data', 'mapData'), + }) + .then(action(res => { + const promises = []; + for (const model of this.models) { + promises.push(model._afterSaveAll(res, options)); + } + return Promise.all(promises); + })) + .catch( + action(err => { + if (err.valErrors) { + this.parseValidationErrors(err.valErrors); + } + throw err; + }) + ) + ); + } } diff --git a/src/__tests__/Store.js b/src/__tests__/Store.js index 37e7eef..c945794 100644 --- a/src/__tests__/Store.js +++ b/src/__tests__/Store.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import { toJS } from 'mobx'; import MockAdapter from 'axios-mock-adapter'; import { Model, Store, BinderApi } from '../'; import { @@ -10,6 +11,10 @@ import { Breed, PersonStore, PersonStoreResourceName, + File, + FileStore, + FileCabinet, + FileCabinetStore, } from './fixtures/Animal'; import { CustomerStore, @@ -29,6 +34,10 @@ import pagination1Data from './fixtures/pagination/1.json'; import pagination2Data from './fixtures/pagination/2.json'; import pagination3Data from './fixtures/pagination/3.json'; import pagination4Data from './fixtures/pagination/4.json'; +import saveFailData from './fixtures/save-fail.json'; +import saveNewFailData from './fixtures/save-new-fail.json'; +import animalMultiPutResponse from './fixtures/animals-multi-put-response.json'; +import animalMultiPutError from './fixtures/animals-multi-put-error.json'; const simpleData = [ { @@ -1007,4 +1016,575 @@ describe('Pagination', () => { expect(animalStore.map('id')).toEqual([1, 2, 3]); }); }); + + + + + + + + + + + + + + + + test('save new with basic properties', () => { + const animalStore = new AnimalStore(); + animalStore.add({ name: 'Doggo' }) + + const spy = jest.spyOn(animalStore.models[0], 'saveFromBackend'); + mock.onAny().replyOnce(config => { + expect(config.url).toBe('/api/animal/'); + expect(config.method).toBe('put'); + expect(config.data).toBe('{"data":[{"id":-208,"name":"Doggo"}],"with":{}}'); + return [201, { "idmap": { "animal": [[-208, 10]] } }]; + }); + + return animalStore.save().then(() => { + expect(animalStore.models[0].id).toBe(10); + expect(spy).toHaveBeenCalled(); + + spy.mockReset(); + spy.mockRestore(); + }); + }); + + test('save existing with basic properties', () => { + const animalStore = new AnimalStore(); + animalStore.add({ id: 12, name: 'Burhan' }) + + mock.onAny().replyOnce(config => { + expect(config.method).toBe('put'); + return [200, { id: 12, name: 'Burhan' }]; + }); + + return animalStore.save().then(() => { + expect(animalStore.models[0].id).toBe(12); + }); + }); + + test('save fail with basic properties', () => { + const animalStore = new AnimalStore(); + animalStore.add({ id: -1, name: 'Nope' }) + mock.onAny().replyOnce(400, saveFailData); + + return animalStore.save().catch(() => { + const valErrors = toJS(animalStore.models[0].backendValidationErrors); + expect(valErrors).toEqual({ + name: ['required'], + kind: ['blank'], + }); + }); + }); + + test('save new model fail with basic properties', () => { + const animalStore = new AnimalStore(); + animalStore.add({ id: -1, name: 'Nope' }) + mock.onAny().replyOnce(400, saveNewFailData); + + return animalStore.save().catch(() => { + const valErrors = toJS(animalStore.models[0].backendValidationErrors); + expect(valErrors).toEqual({ + name: ['invalid'], + }); + }); + }); + + test('save fail with 500', () => { + const animalStore = new AnimalStore(); + animalStore.add({ id: -1, name: 'Nope' }) + mock.onAny().replyOnce(500, {}); + + return animalStore.save().catch(() => { + const valErrors = toJS(animalStore.models[0].backendValidationErrors); + expect(valErrors).toEqual({}); + }); + }); + + test('save with params', () => { + const animalStore = new AnimalStore(); + animalStore.add({ id: -1, name: 'Nope' }) + mock.onAny().replyOnce(config => { + expect(config.params).toEqual({ branch_id: 1 }); + return [201, {}]; + }); + + return animalStore.save({ params: { branch_id: 1 } }); + }); + + test('save with custom data', () => { + const animalStore = new AnimalStore(); + animalStore.add({id: -1}) + mock.onAny().replyOnce(config => { + expect(JSON.parse(config.data)).toEqual({ data: [{ id: -1, name: '', extra_data: 'can be saved' }], with: {}}); + return [201, {}]; + }); + + return animalStore.save({ data: { extra_data: 'can be saved' } }); + }); + + test('save with mapped data', () => { + const animalStore = new AnimalStore(); + animalStore.add({ id: -1 }) + mock.onAny().replyOnce(config => { + expect(JSON.parse(config.data)).toEqual({ data: [{ id: 'overwritten', name: '' }], with: {} }); + return [201, {}]; + }); + + return animalStore.save({ mapData: data => ({ ...data, id: 'overwritten' }) }); + }); + + test('save with custom and mapped data', () => { + const animalStore = new AnimalStore(); + animalStore.add({ id: -1 }) + mock.onAny().replyOnce(config => { + expect(JSON.parse(config.data)).toEqual({ data: [{ id: 'overwritten', name: '', extra_data: 'can be saved' }], with: {} }); + return [201, {}]; + }); + + return animalStore.save({ data: { extra_data: 'can be saved' }, mapData: data => ({ ...data, id: 'overwritten' }) }); + }); + + test('save all with relations', () => { + const animalStore = new AnimalStore({ relations: ['kind', 'pastOwners'] }); + animalStore.add( + { + id: -1, + name: 'Doggo', + kind: { id: -2, name: 'Dog' }, + pastOwners: [{ id: -3, name: 'Henk' }], + }, + ); + const animal = animalStore.models[0]; + const spy = jest.spyOn(animal, 'saveFromBackend'); + mock.onAny().replyOnce(config => { + expect(config.url).toBe('/api/animal/'); + expect(config.method).toBe('put'); + return [201, animalMultiPutResponse]; + }); + + return animalStore.save({ relations: ['kind'] }).then(response => { + expect(spy).toHaveBeenCalled(); + expect(animal.id).toBe(10); + expect(animal.kind.id).toBe(4); + expect(animal.pastOwners.at(0).id).toBe(100); + expect(response).toEqual([animalMultiPutResponse]); + + spy.mockReset(); + spy.mockRestore(); + }); + }); + + test('save all with relations - verify ids are mapped correctly', () => { + const animalStore = new AnimalStore({ relations: ['pastOwners'] }); + animalStore.add( + { + pastOwners: [{ id: -2, name: 'Henk' }, { id: 125, name: 'Hanos' }], + }, + ); + const animal = animalStore.models[0]; + + // Sanity check unrelated to the actual test. + expect(animal.pastOwners.at(0).getInternalId()).toBe(-2); + mock.onAny().replyOnce(config => { + return [ + 201, + { idmap: { animal: [[-1, 10]], person: [[-2, 100]] } }, + ]; + }); + + return animal.save({ relations: ['pastOwners'] }).then(() => { + expect(animal.pastOwners.map('id')).toEqual([100, 125]); + }); + }); + + test('save all with validation errors', () => { + const animalStore = new AnimalStore({ relations: ['kind', 'pastOwners.town'] }); + animalStore.add( + { + id: -1, + name: 'Doggo', + kind: { id: -2, name: 'Dog' }, + pastOwners: [{ id: -3, name: 'Jo', town: { id: 5, name: '' } }], + } + ); + const animal = animalStore.models[0]; + mock.onAny().replyOnce(config => { + return [400, animalMultiPutError]; + }); + + return animalStore.save({ relations: ['kind'] }).then( + () => { }, + err => { + if (!err.response) { + throw err; + } + expect(toJS(animal.backendValidationErrors).name).toEqual([ + 'blank', + ]); + expect(toJS(animal.kind.backendValidationErrors).name).toEqual([ + 'required', + ]); + expect( + toJS(animal.pastOwners.at(0).backendValidationErrors).name + ).toEqual(['required']); + expect( + toJS(animal.pastOwners.at(0).town.backendValidationErrors) + .name + ).toEqual(['maxlength']); + } + ); + }); + + test('save all with validation errors and check if it clears them', () => { + const animalStore = new AnimalStore({ relations: ['pastOwners.town'] }); + animalStore.add( + { + id: -1, + name: 'Doggo', + pastOwners: [{id: -2, name: 'Jo', town: { id: 5, name: '' } }], + }, + ); + const animal = animalStore.models[0]; + + // We first trigger a save with validation errors from the backend, then we trigger a second save which fixes those validation errors, + // then we check if the errors get cleared. + mock.onAny().replyOnce(config => { + return [400, animalMultiPutError]; + }); + + const options = { relations: ['pastOwners.town'] }; + return animalStore.save(options).then( + () => { }, + err => { + if (!err.response) { + throw err; + } + mock.onAny().replyOnce(200, { idmap: [] }); + return animalStore.save(options).then(() => { + const valErrors1 = toJS( + animal.pastOwners.at(0).backendValidationErrors + ); + expect(valErrors1).toEqual({}); + const valErrors2 = toJS( + animal.pastOwners.at(0).town.backendValidationErrors + ); + expect(valErrors2).toEqual({}); + }); + } + ); + }); + + test('save all with existing model', () => { + const animal = new Animal( + { id: 10, name: 'Doggo', kind: { name: 'Dog' } }, + { relations: ['kind'] } + ); + const animalStore = new AnimalStore({ relations: ['kind'] }); + animalStore.add(animal.toJS()); + + mock.onAny().replyOnce(config => { + expect(config.url).toBe('/api/animal/'); + expect(config.method).toBe('put'); + const putData = JSON.parse(config.data); + + // [TODO] this does not work + // expect(putData).toMatchSnapshot(); + + return [201, animalMultiPutResponse]; + }); + + return animalStore.save({ relations: ['kind'] }); + }); + + test('save all with not defined relation error', () => { + const animal = new Animal( + { id: 10, name: 'Doggo', kind: { name: 'Dog' } } + ); + const animalStore = new AnimalStore({ relations: ['kind'] }); + animalStore.add(animal.toJS()); + + mock.onAny().replyOnce(config => { + expect(config.url).toBe('/api/animal/'); + expect(config.method).toBe('put'); + const putData = JSON.parse(config.data); + + // [TODO] this does not work + // expect(putData).toMatchSnapshot(); + + return [201, animalMultiPutResponse]; + }); + + return animalStore.save({ relations: ['kind', 'owner'] }).catch((e) => { + const error = 'Relation \'owner\' is not defined in relations' + expect(e.message).toEqual(error); + }) + }); + + + test('save all with empty response from backend', () => { + const animal = new Animal( + { name: 'Doggo', kind: { name: 'Dog' } }, + { relations: ['kind'] } + ); + const animalStore = new AnimalStore({ relations: ['kind'] }); + animalStore.add(animal.toJS()); + + mock.onAny().replyOnce(config => { + return [201, {}]; + }); + + return animalStore.save({ relations: ['kind'] }); + }); + + test('save all fail', () => { + const animal = new Animal({}, { relations: ['kind'] }); + const animalStore = new AnimalStore({ relations: ['kind'] }); + animalStore.add(animal.toJS()); + + mock.onAny().replyOnce(() => { + return [500, {}]; + }); + + const promise = animalStore.save({ relations: ['kind'] }); + expect(animalStore.isLoading).toBe(true); + return promise.catch(() => { + expect(animal.isLoading).toBe(false); + }); + }); + + + + + + + + + + + + + + + test('Save model with file', () => { + const file = new File({ id: 5 }); + const dataFile = new Blob(['foo'], { type: 'text/plain' }); + file.setInput('dataFile', dataFile); + + const fileStore = new FileStore(); + fileStore.add(file.toJS()); + + mock.onAny().replyOnce(config => { + expect(config.method).toBe('put'); + + expect(config.data).toBeInstanceOf(FormData); + + const keys = Array.from(config.data.keys()).sort(); + + // [TODO] needs to be checked + // expect(keys).toEqual(['data', 'file:data_file']); + + const data = JSON.parse(config.data.get('data')); + expect(data).toEqual({ + data: [{ + data_file: null, + id: 5, + }], with: {} + }); + return [200, { id: 5, data_file: '/api/dataFile' }]; + }); + + fileStore.save().then(() => { + expect(file.id).toBe(5); + + // [TODO] check how this should work + // expect(fileStore.models[0].dataFile).toBe('/api/dataFile'); + }); + }); + + test('Save model with relations and multiple files', () => { + const fileCabinet = new FileCabinet({ id: 5 }, { relations: ['files'] }); + fileCabinet.files.add([ + { id: -2, dataFile: new Blob(['bar'], { type: 'text/plain' }) }, + { id: -3, dataFile: new Blob(['foo'], { type: 'text/plain' }) }, + { id: -4, dataFile: new Blob(['baz'], { type: 'text/plain' }) }, + ]); + + const fileCabinetStore = new FileCabinetStore({ relations: ['files'] }); + fileCabinetStore.add(fileCabinet.toJS()); + + mock.onAny().replyOnce(config => { + expect(config.method).toBe('put'); + + expect(config.data).toBeInstanceOf(FormData); + + const keys = Array.from(config.data.keys()).sort(); + expect(keys).toEqual([ + 'data', + 'file:with.files.0.data_file', + 'file:with.files.1.data_file', + 'file:with.files.2.data_file']); + + const data = JSON.parse(config.data.get('data')); + expect(data).toEqual({ + data: [{ + id: 5, + files: [-2, -3, -4], + }], + with: { + files: [ + { id: -2, data_file: null }, + { id: -3, data_file: null }, + { id: -4, data_file: null }, + ], + }, + }); + return [200, {}]; + }); + + fileCabinetStore.save({ relations: ['files'] }); + }); + + + + + + + + + + + + + test('hasUserChanges should clear changes in current fields after save', () => { + const animalStore = new AnimalStore({ relations: ['kind.breed'] }); + animalStore.add({ id: 1 }); + const animal = animalStore.models[0]; + + animal.setInput('name', 'Felix'); + + mock.onAny().replyOnce(() => { + return [200, { id: 1, name: 'Garfield' }]; + }); + + expect(animal.hasUserChanges).toBe(true); + return animalStore.save().then(() => { + expect(animal.hasUserChanges).toBe(false); + }); + }); + + test('hasUserChanges should not clear changes in model relations when not saved', () => { + const animalStore = new AnimalStore({ relations: ['kind.breed'] }); + animalStore.add({id: 1}); + const animal = animalStore.models[0]; + + animal.kind.breed.setInput('name', 'Katachtige'); + + mock.onAny().replyOnce(() => { + return [200, {}]; + }); + + return animalStore.save().then(() => { + // Because we didn't save the relation, it should return true. + expect(animal.hasUserChanges).toBe(true); + expect(animal.kind.hasUserChanges).toBe(true); + expect(animal.kind.breed.hasUserChanges).toBe(true); + }); + }); + + test('hasUserChanges should clear changes in saved model relations', () => { + const animalStore = new AnimalStore({ relations: ['kind.breed'] }); + animalStore.add({ id: 1 }); + const animal = animalStore.models[0]; + + animal.kind.breed.setInput('name', 'Katachtige'); + + mock.onAny().replyOnce(() => { + return [200, {}]; + }); + + return animalStore.save({ relations: ['kind.breed'] }).then(() => { + expect(animal.hasUserChanges).toBe(false); + }); + }); + + test('hasUserChanges should not clear changes in non-saved models relations', () => { + const animalStore = new AnimalStore({ relations: ['pastOwners', 'kind.breed'] }); + animalStore.add( + { + id: 1, pastOwners: [ + { id: 2 }, + { id: 3 }, + ] + }, + ); + const animal = animalStore.models[0]; + animal.kind.breed.setInput('name', 'Katachtige'); + animal.pastOwners.get(2).setInput('name', 'Zaico'); + + mock.onAny().replyOnce(() => { + return [200, {}]; + }); + + return animalStore.save({ relations: ['kind.breed'] }).then(() => { + expect(animal.hasUserChanges).toBe(true); + expect(animal.pastOwners.hasUserChanges).toBe(true); + expect(animal.pastOwners.get(2).hasUserChanges).toBe(true); + expect(animal.pastOwners.get(3).hasUserChanges).toBe(false); + }); + }); + + test('hasUserChanges should clear set changes in saved relations', () => { + const animalStore = new AnimalStore({ relations: ['pastOwners', 'kind.breed'] }); + animalStore.add( + { + id: 1, pastOwners: [ + { id: 2 }, + { id: 3 }, + ] + }, + ); + const animal = animalStore.models[0]; + + animal.pastOwners.add({}); + expect(animal.hasUserChanges).toBe(true); + expect(animal.pastOwners.hasUserChanges).toBe(true); + + mock.onAny().replyOnce(() => { + return [200, {}]; + }); + + return animalStore.save({ relations: ['pastOwners'] }).then(() => { + expect(animal.pastOwners.hasUserChanges).toBe(false); + expect(animal.hasUserChanges).toBe(false); + }); + }); + + test('hasUserChanges should not clear set changes in non-saved relations', () => { + + const animalStore = new AnimalStore({ relations: ['pastOwners', 'kind.breed'] }); + animalStore.add({ + id: 1, pastOwners: [ + { id: 2 }, + { id: 3 }, + ] + }); + const animal = animalStore.models[0]; + animal.pastOwners.add({}); + expect(animal.hasUserChanges).toBe(true); + expect(animal.pastOwners.hasUserChanges).toBe(true); + + mock.onAny().replyOnce(() => { + return [200, {}]; + }); + + return animalStore.save({ relations: ['kind'] }).then(() => { + expect(animal.pastOwners.hasUserChanges).toBe(true); + expect(animal.hasUserChanges).toBe(true); + }); + }); + }); diff --git a/src/__tests__/fixtures/Animal.js b/src/__tests__/fixtures/Animal.js index d66e182..9789632 100644 --- a/src/__tests__/fixtures/Animal.js +++ b/src/__tests__/fixtures/Animal.js @@ -17,6 +17,8 @@ export class File extends Model { export class FileStore extends Store { Model = File + api = new BinderApi(); + url = '/api/file/'; } export class FileCabinet extends Model { @@ -32,6 +34,12 @@ export class FileCabinet extends Model { } } +export class FileCabinetStore extends Store { + Model = FileCabinet; + api = new BinderApi(); + url = '/api/file_cabinet/'; +} + export class Breed extends Model { @observable id = null; From 266a7f3a348b111e5b84a7a66d7f20bc86e93d16 Mon Sep 17 00:00:00 2001 From: Maciej Lewinski Date: Wed, 4 May 2022 12:06:16 +0200 Subject: [PATCH 3/5] Updated tests --- src/__tests__/Store.js | 117 +++++++++----------- src/__tests__/fixtures/store-save-fail.json | 21 ++++ 2 files changed, 75 insertions(+), 63 deletions(-) create mode 100644 src/__tests__/fixtures/store-save-fail.json diff --git a/src/__tests__/Store.js b/src/__tests__/Store.js index c945794..7a5bb02 100644 --- a/src/__tests__/Store.js +++ b/src/__tests__/Store.js @@ -35,6 +35,7 @@ import pagination2Data from './fixtures/pagination/2.json'; import pagination3Data from './fixtures/pagination/3.json'; import pagination4Data from './fixtures/pagination/4.json'; import saveFailData from './fixtures/save-fail.json'; +import storeSaveFailData from './fixtures/store-save-fail.json'; import saveNewFailData from './fixtures/save-new-fail.json'; import animalMultiPutResponse from './fixtures/animals-multi-put-response.json'; import animalMultiPutError from './fixtures/animals-multi-put-error.json'; @@ -1016,35 +1017,35 @@ describe('Pagination', () => { expect(animalStore.map('id')).toEqual([1, 2, 3]); }); }); +}); - - - - - - - - - - - - - +describe('Save', () => { + let mock; + beforeEach(() => { + mock = new MockAdapter(axios); + }); + afterEach(() => { + if (mock) { + mock.restore(); + mock = null; + } + }); test('save new with basic properties', () => { const animalStore = new AnimalStore(); - animalStore.add({ name: 'Doggo' }) + animalStore.add([{ name: 'Doggo' }, { name: 'Catty' }]); const spy = jest.spyOn(animalStore.models[0], 'saveFromBackend'); mock.onAny().replyOnce(config => { expect(config.url).toBe('/api/animal/'); expect(config.method).toBe('put'); - expect(config.data).toBe('{"data":[{"id":-208,"name":"Doggo"}],"with":{}}'); - return [201, { "idmap": { "animal": [[-208, 10]] } }]; + expect(config.data).toBe('{"data":[{"id":-208,"name":"Doggo"},{"id":-209,"name":"Catty"}],"with":{}}'); + return [201, { "idmap": { "animal": [[-208, 10], [-209, 11]] } }]; }); return animalStore.save().then(() => { expect(animalStore.models[0].id).toBe(10); + expect(animalStore.models[1].id).toBe(11); expect(spy).toHaveBeenCalled(); spy.mockReset(); @@ -1054,29 +1055,51 @@ describe('Pagination', () => { test('save existing with basic properties', () => { const animalStore = new AnimalStore(); - animalStore.add({ id: 12, name: 'Burhan' }) + animalStore.add([{ id: 12, name: 'Burhan' }, { id: 11, name: 'Maciej' }]) mock.onAny().replyOnce(config => { expect(config.method).toBe('put'); - return [200, { id: 12, name: 'Burhan' }]; + expect(config.data).toBe('{"data":[{"id":12,"name":"Burhan"},{"id":11,"name":"Maciej"}],"with":{}}'); + return [200, { "idmap": {}}]; }); return animalStore.save().then(() => { expect(animalStore.models[0].id).toBe(12); + expect(animalStore.models[1].id).toBe(11); }); }); test('save fail with basic properties', () => { const animalStore = new AnimalStore(); - animalStore.add({ id: -1, name: 'Nope' }) + animalStore.add([{ id: -1, name: 'Nope' }, { id: -2, name: 'Nope' }]) mock.onAny().replyOnce(400, saveFailData); return animalStore.save().catch(() => { - const valErrors = toJS(animalStore.models[0].backendValidationErrors); - expect(valErrors).toEqual({ + const valErrors1 = toJS(animalStore.models[0].backendValidationErrors); + const valErrors2 = toJS(animalStore.models[1].backendValidationErrors); + expect(valErrors1).toEqual({ + name: ['required'], + kind: ['blank'], + }); + expect(valErrors2).toEqual({}); + }); + }); + + test('save fail with basic properties multiple', () => { + const animalStore = new AnimalStore(); + animalStore.add([{ id: -1, name: 'Nope' }, { id: -2, name: 'Nope' }]) + mock.onAny().replyOnce(400, storeSaveFailData); + + return animalStore.save().catch(() => { + const valErrors1 = toJS(animalStore.models[0].backendValidationErrors); + const valErrors2 = toJS(animalStore.models[1].backendValidationErrors); + expect(valErrors1).toEqual({ name: ['required'], kind: ['blank'], }); + expect(valErrors2).toEqual({ + name: ['required'], + }); }); }); @@ -1117,9 +1140,9 @@ describe('Pagination', () => { test('save with custom data', () => { const animalStore = new AnimalStore(); - animalStore.add({id: -1}) + animalStore.add({ id: -1 }) mock.onAny().replyOnce(config => { - expect(JSON.parse(config.data)).toEqual({ data: [{ id: -1, name: '', extra_data: 'can be saved' }], with: {}}); + expect(JSON.parse(config.data)).toEqual({ data: [{ id: -1, name: '', extra_data: 'can be saved' }], with: {} }); return [201, {}]; }); @@ -1196,7 +1219,7 @@ describe('Pagination', () => { ]; }); - return animal.save({ relations: ['pastOwners'] }).then(() => { + return animalStore.save({ relations: ['pastOwners'] }).then(() => { expect(animal.pastOwners.map('id')).toEqual([100, 125]); }); }); @@ -1207,7 +1230,7 @@ describe('Pagination', () => { { id: -1, name: 'Doggo', - kind: { id: -2, name: 'Dog' }, + kind: { id: -2, name: 'Dog' }, pastOwners: [{ id: -3, name: 'Jo', town: { id: 5, name: '' } }], } ); @@ -1245,7 +1268,7 @@ describe('Pagination', () => { { id: -1, name: 'Doggo', - pastOwners: [{id: -2, name: 'Jo', town: { id: 5, name: '' } }], + pastOwners: [{ id: -2, name: 'Jo', town: { id: 5, name: '' } }], }, ); const animal = animalStore.models[0]; @@ -1290,10 +1313,7 @@ describe('Pagination', () => { expect(config.url).toBe('/api/animal/'); expect(config.method).toBe('put'); const putData = JSON.parse(config.data); - - // [TODO] this does not work - // expect(putData).toMatchSnapshot(); - + expect(putData).toMatchSnapshot(); return [201, animalMultiPutResponse]; }); @@ -1311,10 +1331,7 @@ describe('Pagination', () => { expect(config.url).toBe('/api/animal/'); expect(config.method).toBe('put'); const putData = JSON.parse(config.data); - - // [TODO] this does not work - // expect(putData).toMatchSnapshot(); - + expect(putData).toMatchSnapshot(); return [201, animalMultiPutResponse]; }); @@ -1324,7 +1341,6 @@ describe('Pagination', () => { }) }); - test('save all with empty response from backend', () => { const animal = new Animal( { name: 'Doggo', kind: { name: 'Dog' } }, @@ -1357,18 +1373,6 @@ describe('Pagination', () => { }); - - - - - - - - - - - - test('Save model with file', () => { const file = new File({ id: 5 }); const dataFile = new Blob(['foo'], { type: 'text/plain' }); @@ -1384,8 +1388,7 @@ describe('Pagination', () => { const keys = Array.from(config.data.keys()).sort(); - // [TODO] needs to be checked - // expect(keys).toEqual(['data', 'file:data_file']); + expect(keys).toEqual(['data', 'file:data_file']); const data = JSON.parse(config.data.get('data')); expect(data).toEqual({ @@ -1399,9 +1402,7 @@ describe('Pagination', () => { fileStore.save().then(() => { expect(file.id).toBe(5); - - // [TODO] check how this should work - // expect(fileStore.models[0].dataFile).toBe('/api/dataFile'); + expect(fileStore.models[0].dataFile).toBe('/api/dataFile'); }); }); @@ -1449,16 +1450,6 @@ describe('Pagination', () => { }); - - - - - - - - - - test('hasUserChanges should clear changes in current fields after save', () => { const animalStore = new AnimalStore({ relations: ['kind.breed'] }); animalStore.add({ id: 1 }); @@ -1478,7 +1469,7 @@ describe('Pagination', () => { test('hasUserChanges should not clear changes in model relations when not saved', () => { const animalStore = new AnimalStore({ relations: ['kind.breed'] }); - animalStore.add({id: 1}); + animalStore.add({ id: 1 }); const animal = animalStore.models[0]; animal.kind.breed.setInput('name', 'Katachtige'); diff --git a/src/__tests__/fixtures/store-save-fail.json b/src/__tests__/fixtures/store-save-fail.json new file mode 100644 index 0000000..e3a7aed --- /dev/null +++ b/src/__tests__/fixtures/store-save-fail.json @@ -0,0 +1,21 @@ +{ + "errors": { + "animal": { + "-1": { + "name": [{ + "code": "required" + }], + "kind": [{ + "code": "blank" + }] + }, + "-2": { + "name": [ + { + "code": "required" + } + ] + } + } + } +} From 92af1e8570bb03ba2e104a33db4841245517a00b Mon Sep 17 00:00:00 2001 From: Maciej Lewinski Date: Wed, 4 May 2022 14:37:11 +0200 Subject: [PATCH 4/5] updated snapshot file --- src/__tests__/__snapshots__/Store.js.snap | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/__tests__/__snapshots__/Store.js.snap b/src/__tests__/__snapshots__/Store.js.snap index d24156a..43e5273 100644 --- a/src/__tests__/__snapshots__/Store.js.snap +++ b/src/__tests__/__snapshots__/Store.js.snap @@ -1,5 +1,45 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Save save all with existing model 1`] = ` +Object { + "data": Array [ + Object { + "id": 10, + "kind": -238, + "name": "Doggo", + }, + ], + "with": Object { + "kind": Array [ + Object { + "id": -238, + "name": "Dog", + }, + ], + }, +} +`; + +exports[`Save save all with not defined relation error 1`] = ` +Object { + "data": Array [ + Object { + "id": 10, + "kind": -241, + "name": "Doggo", + }, + ], + "with": Object { + "kind": Array [ + Object { + "id": -241, + "name": "", + }, + ], + }, +} +`; + exports[`requests fetch with complex nested relations 1`] = ` Array [ Object { From 92f1db61021c43069a89ea4239ac416f206ebdc1 Mon Sep 17 00:00:00 2001 From: Maciej Lewinski Date: Wed, 4 May 2022 16:25:49 +0200 Subject: [PATCH 5/5] Added builded package --- dist/mobx-spine.cjs.js | 179 +++++++++++++++++++++++++++++------------ dist/mobx-spine.es.js | 179 +++++++++++++++++++++++++++++------------ 2 files changed, 256 insertions(+), 102 deletions(-) diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 13ac9a7..5358448 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -234,6 +234,7 @@ function _applyDecoratedDescriptor(target, property, decorators, descriptor, con return desc; } + var AVAILABLE_CONST_OPTIONS = ['relations', 'limit', 'comparator', 'params', 'repository']; var Store = (_class = (_temp = _class2 = function () { @@ -732,6 +733,66 @@ var Store = (_class = (_temp = _class2 = function () { return Promise.all(promises); } + }, { + key: 'save', + value: function save() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + return this._saveAll(options); + } + }, { + key: '_saveAll', + value: function _saveAll() { + var _this9 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + this.clearValidationErrors(); + return this.wrapPendingRequestCount(this.__getApi().saveAllModels({ + url: lodash.result(this, 'url'), + model: this, + data: this.toBackendAll({ + data: options.data, + mapData: options.mapData, + nestedRelations: relationsToNestedKeys(options.relations || []), + onlyChanges: options.onlyChanges + }), + requestOptions: lodash.omit(options, 'relations', 'data', 'mapData') + }).then(mobx.action(function (res) { + var promises = []; + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = _this9.models[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var model = _step2.value; + + promises.push(model._afterSaveAll(res, options)); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + return Promise.all(promises); + })).catch(mobx.action(function (err) { + if (err.valErrors) { + _this9.parseValidationErrors(err.valErrors); + } + throw err; + }))); + } }, { key: 'totalPages', get: function get() { @@ -802,7 +863,7 @@ var Store = (_class = (_temp = _class2 = function () { totalRecords: 0 }; } -}), _applyDecoratedDescriptor(_class.prototype, 'isLoading', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'isLoading'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'length', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'length'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fromBackend', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'fromBackend'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'sort', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'sort'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'parse', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'parse'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'add', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'add'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'remove', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'remove'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'removeById', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'removeById'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'clear', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'clear'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fetch', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'fetch'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setLimit', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'setLimit'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'totalPages', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'totalPages'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'currentPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'currentPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasNextPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasPreviousPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getNextPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'getNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getPreviousPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'getPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'setPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasUserChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasUserChanges'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasSetChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasSetChanges'), _class.prototype)), _class); +}), _applyDecoratedDescriptor(_class.prototype, 'isLoading', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'isLoading'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'length', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'length'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fromBackend', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'fromBackend'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'sort', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'sort'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'parse', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'parse'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'add', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'add'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'remove', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'remove'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'removeById', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'removeById'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'clear', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'clear'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fetch', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'fetch'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setLimit', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'setLimit'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'totalPages', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'totalPages'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'currentPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'currentPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasNextPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasPreviousPage', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getNextPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'getNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getPreviousPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'getPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setPage', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, 'setPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasUserChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasUserChanges'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasSetChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasSetChanges'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, '_saveAll', [mobx.action], Object.getOwnPropertyDescriptor(_class.prototype, '_saveAll'), _class.prototype)), _class); var Relation = function () { function Relation(toModel) { @@ -1620,12 +1681,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { _this11.clearUserFileChanges(); return Promise.resolve(res); }); - })).catch(mobx.action(function (err) { - if (err.valErrors) { - _this11.parseValidationErrors(err.valErrors); - } - throw err; - }))); + })).catch(function (err) { + return _this11._catchSave(err); + })); } }, { key: '_saveAll', @@ -1645,35 +1703,54 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { onlyChanges: options.onlyChanges }), requestOptions: lodash.omit(options, 'relations', 'data', 'mapData') - }).then(mobx.action(function (res) { - _this12.saveFromBackend(res); - _this12.clearUserFieldChanges(); - - forNestedRelations(_this12, relationsToNestedKeys(options.relations || []), function (relation) { - if (relation instanceof Model) { - relation.clearUserFieldChanges(); - } else { - relation.clearSetChanges(); - } - }); + }).then(function (res) { + return _this12._afterSaveAll(res, options); + }).catch(function (err) { + return _this12._catchSave(err); + })); + } + }, { + key: '_afterSaveAll', + value: function _afterSaveAll(res, options) { + var _this13 = this; - return _this12.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { - _this12.clearUserFileChanges(); + this.saveFromBackend(res); + this.clearUserFieldChanges(); - forNestedRelations(_this12, relationsToNestedKeys(options.relations || []), function (relation) { - if (relation instanceof Model) { - relation.clearUserFileChanges(); - } - }); + forNestedRelations(this, relationsToNestedKeys(options.relations || []), function (relation) { + if (relation instanceof Model) { + relation.clearUserFieldChanges(); + } else { + relation.clearSetChanges(); + } + }); - return res; - }); - })).catch(mobx.action(function (err) { - if (err.valErrors) { - _this12.parseValidationErrors(err.valErrors); + return this.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { + return _this13._afterSaveAllFiles(options); + }).then(function () { + return res; + }); + } + }, { + key: '_afterSaveAllFiles', + value: function _afterSaveAllFiles(options) { + this.clearUserFileChanges(); + + forNestedRelations(this, relationsToNestedKeys(options.relations || []), function (relation) { + if (relation instanceof Model) { + relation.clearUserFileChanges(); } - throw err; - }))); + }); + + return; + } + }, { + key: '_catchSave', + value: function _catchSave(err) { + if (err.valErrors) { + this.parseValidationErrors(err.valErrors); + } + throw err; } // After saving a model, we should get back an ID mapping from the backend which looks like: @@ -1682,19 +1759,19 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__parseNewIds', value: function __parseNewIds(idMaps) { - var _this13 = this; + var _this14 = this; var bName = this.constructor.backendResourceName; if (bName && idMaps[bName]) { var idMap = idMaps[bName].find(function (ids) { - return ids[0] === _this13.getInternalId(); + return ids[0] === _this14.getInternalId(); }); if (idMap) { this[this.constructor.primaryKey] = idMap[1]; } } lodash.each(this.__activeCurrentRelations, function (relName) { - var rel = _this13[relName]; + var rel = _this14[relName]; rel.__parseNewIds(idMaps); }); } @@ -1706,7 +1783,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'parseValidationErrors', value: function parseValidationErrors(valErrors) { - var _this14 = this; + var _this15 = this; var bname = this.constructor.backendResourceName; @@ -1719,24 +1796,24 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return snakeToCamel(key); }); var formattedErrors = lodash.mapValues(camelCasedErrors, function (valError) { - return valError.map(_this14.validationErrorFormatter); + return valError.map(_this15.validationErrorFormatter); }); this.__backendValidationErrors = formattedErrors; } } this.__activeCurrentRelations.forEach(function (currentRel) { - _this14[currentRel].parseValidationErrors(valErrors); + _this15[currentRel].parseValidationErrors(valErrors); }); } }, { key: 'clearValidationErrors', value: function clearValidationErrors() { - var _this15 = this; + var _this16 = this; this.__backendValidationErrors = {}; this.__activeCurrentRelations.forEach(function (currentRel) { - _this15[currentRel].clearValidationErrors(); + _this16[currentRel].clearValidationErrors(); }); } @@ -1754,12 +1831,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'delete', value: function _delete() { - var _this16 = this; + var _this17 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var removeFromStore = function removeFromStore() { - return _this16.__store ? _this16.__store.remove(_this16) : null; + return _this17.__store ? _this17.__store.remove(_this17) : null; }; if (options.immediate || this.isNew) { removeFromStore(); @@ -1785,7 +1862,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fetch', value: function fetch() { - var _this17 = this; + var _this18 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1801,7 +1878,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { data: data, requestOptions: lodash.omit(options, ['data', 'url']) }).then(mobx.action(function (res) { - _this17.fromBackend(res); + _this18.fromBackend(res); })).catch(function (e) { if (Axios.isCancel(e)) { return null; @@ -1815,14 +1892,14 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'clear', value: function clear() { - var _this18 = this; + var _this19 = this; lodash.forIn(this.__originalAttributes, function (value, key) { - _this18[key] = value; + _this19[key] = value; }); this.__activeCurrentRelations.forEach(function (currentRel) { - _this18[currentRel].clear(); + _this19[currentRel].clear(); }); } @@ -1843,13 +1920,13 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'hasUserChanges', get: function get() { - var _this19 = this; + var _this20 = this; if (this.__changes.length > 0) { return true; } return this.__activeCurrentRelations.some(function (rel) { - return _this19[rel].hasUserChanges; + return _this20[rel].hasUserChanges; }); } }, { @@ -1917,7 +1994,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { initializer: function initializer() { return {}; } -}), _applyDecoratedDescriptor$1(_class$1.prototype, 'url', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'url'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isNew', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isNew'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isLoading', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isLoading'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '__parseRelations', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, '__parseRelations'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'hasUserChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'hasUserChanges'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fieldFilter', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fieldFilter'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fromBackend', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fromBackend'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parse', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parse'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'setInput', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'setInput'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_save', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_save'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_saveAll', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_saveAll'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parseValidationErrors', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parseValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clearValidationErrors', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clearValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'backendValidationErrors', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'backendValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'delete', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'delete'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fetch', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fetch'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clear', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clear'), _class$1.prototype)), _class$1); +}), _applyDecoratedDescriptor$1(_class$1.prototype, 'url', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'url'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isNew', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isNew'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isLoading', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isLoading'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '__parseRelations', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, '__parseRelations'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'hasUserChanges', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'hasUserChanges'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fieldFilter', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fieldFilter'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fromBackend', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fromBackend'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parse', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parse'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'setInput', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'setInput'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_save', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_save'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_saveAll', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_saveAll'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_afterSaveAll', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_afterSaveAll'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_afterSaveAllFiles', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_afterSaveAllFiles'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_catchSave', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_catchSave'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parseValidationErrors', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parseValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clearValidationErrors', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clearValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'backendValidationErrors', [mobx.computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'backendValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'delete', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'delete'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fetch', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fetch'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clear', [mobx.action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clear'), _class$1.prototype)), _class$1); // Function ripped from Django docs. // See: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax @@ -2257,7 +2334,7 @@ function checkLuxonDateTime(attr, value) { } var LUXON_DATE_FORMAT = 'yyyy-LL-dd'; -var LUXON_DATETIME_FORMAT = "yyyy'-'LL'-'dd'T'HH':'mm':'ssZZ"; +var LUXON_DATETIME_FORMAT = 'yyyy\'-\'LL\'-\'dd\'T\'HH\':\'mm\':\'ssZZ'; var CASTS = { momentDate: { diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 44436ae..0037429 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -228,6 +228,7 @@ function _applyDecoratedDescriptor(target, property, decorators, descriptor, con return desc; } + var AVAILABLE_CONST_OPTIONS = ['relations', 'limit', 'comparator', 'params', 'repository']; var Store = (_class = (_temp = _class2 = function () { @@ -726,6 +727,66 @@ var Store = (_class = (_temp = _class2 = function () { return Promise.all(promises); } + }, { + key: 'save', + value: function save() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + return this._saveAll(options); + } + }, { + key: '_saveAll', + value: function _saveAll() { + var _this9 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + this.clearValidationErrors(); + return this.wrapPendingRequestCount(this.__getApi().saveAllModels({ + url: result(this, 'url'), + model: this, + data: this.toBackendAll({ + data: options.data, + mapData: options.mapData, + nestedRelations: relationsToNestedKeys(options.relations || []), + onlyChanges: options.onlyChanges + }), + requestOptions: omit(options, 'relations', 'data', 'mapData') + }).then(action(function (res) { + var promises = []; + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = _this9.models[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var model = _step2.value; + + promises.push(model._afterSaveAll(res, options)); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + return Promise.all(promises); + })).catch(action(function (err) { + if (err.valErrors) { + _this9.parseValidationErrors(err.valErrors); + } + throw err; + }))); + } }, { key: 'totalPages', get: function get() { @@ -796,7 +857,7 @@ var Store = (_class = (_temp = _class2 = function () { totalRecords: 0 }; } -}), _applyDecoratedDescriptor(_class.prototype, 'isLoading', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'isLoading'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'length', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'length'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fromBackend', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'fromBackend'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'sort', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'sort'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'parse', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'parse'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'add', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'add'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'remove', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'remove'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'removeById', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'removeById'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'clear', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'clear'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fetch', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'fetch'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setLimit', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'setLimit'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'totalPages', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'totalPages'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'currentPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'currentPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasNextPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasPreviousPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getNextPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'getNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getPreviousPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'getPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'setPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasUserChanges', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasUserChanges'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasSetChanges', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasSetChanges'), _class.prototype)), _class); +}), _applyDecoratedDescriptor(_class.prototype, 'isLoading', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'isLoading'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'length', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'length'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fromBackend', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'fromBackend'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'sort', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'sort'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'parse', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'parse'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'add', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'add'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'remove', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'remove'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'removeById', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'removeById'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'clear', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'clear'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'fetch', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'fetch'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setLimit', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'setLimit'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'totalPages', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'totalPages'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'currentPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'currentPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasNextPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasPreviousPage', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getNextPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'getNextPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'getPreviousPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'getPreviousPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'setPage', [action], Object.getOwnPropertyDescriptor(_class.prototype, 'setPage'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasUserChanges', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasUserChanges'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, 'hasSetChanges', [computed], Object.getOwnPropertyDescriptor(_class.prototype, 'hasSetChanges'), _class.prototype), _applyDecoratedDescriptor(_class.prototype, '_saveAll', [action], Object.getOwnPropertyDescriptor(_class.prototype, '_saveAll'), _class.prototype)), _class); var Relation = function () { function Relation(toModel) { @@ -1614,12 +1675,9 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { _this11.clearUserFileChanges(); return Promise.resolve(res); }); - })).catch(action(function (err) { - if (err.valErrors) { - _this11.parseValidationErrors(err.valErrors); - } - throw err; - }))); + })).catch(function (err) { + return _this11._catchSave(err); + })); } }, { key: '_saveAll', @@ -1639,35 +1697,54 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { onlyChanges: options.onlyChanges }), requestOptions: omit(options, 'relations', 'data', 'mapData') - }).then(action(function (res) { - _this12.saveFromBackend(res); - _this12.clearUserFieldChanges(); - - forNestedRelations(_this12, relationsToNestedKeys(options.relations || []), function (relation) { - if (relation instanceof Model) { - relation.clearUserFieldChanges(); - } else { - relation.clearSetChanges(); - } - }); + }).then(function (res) { + return _this12._afterSaveAll(res, options); + }).catch(function (err) { + return _this12._catchSave(err); + })); + } + }, { + key: '_afterSaveAll', + value: function _afterSaveAll(res, options) { + var _this13 = this; - return _this12.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { - _this12.clearUserFileChanges(); + this.saveFromBackend(res); + this.clearUserFieldChanges(); - forNestedRelations(_this12, relationsToNestedKeys(options.relations || []), function (relation) { - if (relation instanceof Model) { - relation.clearUserFileChanges(); - } - }); + forNestedRelations(this, relationsToNestedKeys(options.relations || []), function (relation) { + if (relation instanceof Model) { + relation.clearUserFieldChanges(); + } else { + relation.clearSetChanges(); + } + }); - return res; - }); - })).catch(action(function (err) { - if (err.valErrors) { - _this12.parseValidationErrors(err.valErrors); + return this.saveAllFiles(relationsToNestedKeys(options.relations || [])).then(function () { + return _this13._afterSaveAllFiles(options); + }).then(function () { + return res; + }); + } + }, { + key: '_afterSaveAllFiles', + value: function _afterSaveAllFiles(options) { + this.clearUserFileChanges(); + + forNestedRelations(this, relationsToNestedKeys(options.relations || []), function (relation) { + if (relation instanceof Model) { + relation.clearUserFileChanges(); } - throw err; - }))); + }); + + return; + } + }, { + key: '_catchSave', + value: function _catchSave(err) { + if (err.valErrors) { + this.parseValidationErrors(err.valErrors); + } + throw err; } // After saving a model, we should get back an ID mapping from the backend which looks like: @@ -1676,19 +1753,19 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: '__parseNewIds', value: function __parseNewIds(idMaps) { - var _this13 = this; + var _this14 = this; var bName = this.constructor.backendResourceName; if (bName && idMaps[bName]) { var idMap = idMaps[bName].find(function (ids) { - return ids[0] === _this13.getInternalId(); + return ids[0] === _this14.getInternalId(); }); if (idMap) { this[this.constructor.primaryKey] = idMap[1]; } } each(this.__activeCurrentRelations, function (relName) { - var rel = _this13[relName]; + var rel = _this14[relName]; rel.__parseNewIds(idMaps); }); } @@ -1700,7 +1777,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'parseValidationErrors', value: function parseValidationErrors(valErrors) { - var _this14 = this; + var _this15 = this; var bname = this.constructor.backendResourceName; @@ -1713,24 +1790,24 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { return snakeToCamel(key); }); var formattedErrors = mapValues(camelCasedErrors, function (valError) { - return valError.map(_this14.validationErrorFormatter); + return valError.map(_this15.validationErrorFormatter); }); this.__backendValidationErrors = formattedErrors; } } this.__activeCurrentRelations.forEach(function (currentRel) { - _this14[currentRel].parseValidationErrors(valErrors); + _this15[currentRel].parseValidationErrors(valErrors); }); } }, { key: 'clearValidationErrors', value: function clearValidationErrors() { - var _this15 = this; + var _this16 = this; this.__backendValidationErrors = {}; this.__activeCurrentRelations.forEach(function (currentRel) { - _this15[currentRel].clearValidationErrors(); + _this16[currentRel].clearValidationErrors(); }); } @@ -1748,12 +1825,12 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'delete', value: function _delete() { - var _this16 = this; + var _this17 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var removeFromStore = function removeFromStore() { - return _this16.__store ? _this16.__store.remove(_this16) : null; + return _this17.__store ? _this17.__store.remove(_this17) : null; }; if (options.immediate || this.isNew) { removeFromStore(); @@ -1779,7 +1856,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'fetch', value: function fetch() { - var _this17 = this; + var _this18 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; @@ -1795,7 +1872,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { data: data, requestOptions: omit(options, ['data', 'url']) }).then(action(function (res) { - _this17.fromBackend(res); + _this18.fromBackend(res); })).catch(function (e) { if (Axios.isCancel(e)) { return null; @@ -1809,14 +1886,14 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'clear', value: function clear() { - var _this18 = this; + var _this19 = this; forIn(this.__originalAttributes, function (value, key) { - _this18[key] = value; + _this19[key] = value; }); this.__activeCurrentRelations.forEach(function (currentRel) { - _this18[currentRel].clear(); + _this19[currentRel].clear(); }); } @@ -1837,13 +1914,13 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { }, { key: 'hasUserChanges', get: function get() { - var _this19 = this; + var _this20 = this; if (this.__changes.length > 0) { return true; } return this.__activeCurrentRelations.some(function (rel) { - return _this19[rel].hasUserChanges; + return _this20[rel].hasUserChanges; }); } }, { @@ -1911,7 +1988,7 @@ var Model = (_class$1 = (_temp$1 = _class2$1 = function () { initializer: function initializer() { return {}; } -}), _applyDecoratedDescriptor$1(_class$1.prototype, 'url', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'url'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isNew', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isNew'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isLoading', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isLoading'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '__parseRelations', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, '__parseRelations'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'hasUserChanges', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'hasUserChanges'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fieldFilter', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fieldFilter'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fromBackend', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fromBackend'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parse', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parse'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'setInput', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'setInput'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_save', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_save'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_saveAll', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_saveAll'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parseValidationErrors', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parseValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clearValidationErrors', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clearValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'backendValidationErrors', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'backendValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'delete', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'delete'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fetch', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fetch'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clear', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clear'), _class$1.prototype)), _class$1); +}), _applyDecoratedDescriptor$1(_class$1.prototype, 'url', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'url'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isNew', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isNew'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'isLoading', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'isLoading'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '__parseRelations', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, '__parseRelations'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'hasUserChanges', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'hasUserChanges'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fieldFilter', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fieldFilter'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fromBackend', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fromBackend'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parse', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parse'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'setInput', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'setInput'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_save', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_save'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_saveAll', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_saveAll'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_afterSaveAll', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_afterSaveAll'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_afterSaveAllFiles', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_afterSaveAllFiles'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, '_catchSave', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, '_catchSave'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'parseValidationErrors', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'parseValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clearValidationErrors', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clearValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'backendValidationErrors', [computed], Object.getOwnPropertyDescriptor(_class$1.prototype, 'backendValidationErrors'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'delete', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'delete'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'fetch', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'fetch'), _class$1.prototype), _applyDecoratedDescriptor$1(_class$1.prototype, 'clear', [action], Object.getOwnPropertyDescriptor(_class$1.prototype, 'clear'), _class$1.prototype)), _class$1); // Function ripped from Django docs. // See: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax @@ -2251,7 +2328,7 @@ function checkLuxonDateTime(attr, value) { } var LUXON_DATE_FORMAT = 'yyyy-LL-dd'; -var LUXON_DATETIME_FORMAT = "yyyy'-'LL'-'dd'T'HH':'mm':'ssZZ"; +var LUXON_DATETIME_FORMAT = 'yyyy\'-\'LL\'-\'dd\'T\'HH\':\'mm\':\'ssZZ'; var CASTS = { momentDate: {