45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MobX FormStore
2 |
3 |
4 |
5 | FormStore is part of a collection of loosely-coupled components for managing, rendering and validating forms in MobX-based apps.
6 |
7 | ## Detailed Documentation:
8 | https://alexhisen.gitbooks.io/mobx-forms/
9 |
10 | ## FormStore Overview
11 |
12 | Instances of FormStore \(models\) store the data entered by the user into form fields and provide observables for managing validation error messages. They also track changes and provide facilities for \(auto-\)saving the \(changed\) data.
13 |
14 | ## Features
15 |
16 | * Tracks changes in each data property and by default saves only the changes.
17 | * Will not deem a property as changed for string/number, etc datatype changes or different Date objects with the same date or different Arrays \(in v1.4+\) or Objects \(in v3.0+\) with the same content.
18 | * Optionally auto-saves incremental changes \(if there are any\) every X milliseconds \(see [autoSaveInterval](https://alexhisen.gitbooks.io/mobx-forms/formstore-constructor.html)\).
19 | * By default, will not \(auto-\)save data properties that failed validation or that are still being edited by the user \(note that FormStore only provides the facilities to track this - validation, etc is done in different components\).
20 | * In a long-running app, can prevent unnecessary server requests to refresh data in the model by limiting them to not occur more often than Y milliseconds \(see [minRefreshInterval](https://alexhisen.gitbooks.io/mobx-forms/formstore-constructor.html)\).
21 | * Will auto-save any unsaved data when attempting to refresh to prevent loss of user-entered data.
22 | * Provides observable properties to drive things like loading indicators, enabling/disabling form or save button, error and confirmation messages, etc.
23 | * Server [responses](https://alexhisen.gitbooks.io/mobx-forms/formstore-server-errors.html) to save requests can drive error / validation messaging and discard invalid values.
24 | * Can differentiate between 'create' and 'update' save operations. A model that has not yet been created on the server will not try to refresh from server \(this works right in v2.0+\).
25 | * Saves are queued automatically, never concurrent.
26 | * \(NEW in v1.3\) Auto-save can be dynamically configured and enabled or disabled
27 |
28 | ## Breaking change in Version 2.0
29 | Previously, server.create() was called (instead of server.set()) only when the property defined as the idProperty in store.data was falsy.
30 | This worked well if the idProperty was only returned by the server and was not user-enterable.
31 | Now whether server.create() is called is driven by a new store.status.mustCreate property which is true only when the idProperty has not yet been returned by the server / saved even if it already has a value in store.data.
32 | Note that MobxSchemaForm v.1.14+ supports a readOnlyCondition property that can be set to "!model.status.mustCreate" to allow an id property to be entered but not modified.
33 |
34 | ## Breaking changes in Version 3.0
35 | * FormStore now deep-clones \(and merges\) objects and arrays \(plain or observable\) when storing data coming from server and in the updates object sent to server.
36 | * It's no longer published as a webpack-compiled and minified module.
37 |
38 | ## Requirements
39 |
40 | FormStore only requires [MobX](https://mobx.js.org/) 2.2+, 3.x, 4.x or 5.x. _MobX strict mode is currently not supported._ **FormStore does not implement the actual server requests, it only calls methods that you provide with the data to be sent to the server.**
41 |
42 | ## Installation
43 |
44 | ```
45 | npm install --save mobx-form-store
46 | ```
47 |
48 | ## Minimal Usage Example
49 |
50 | myStore.js \(a Singleton\):
51 |
52 | ```js
53 | import FormStore from 'mobx-form-store';
54 |
55 | const model = new FormStore({
56 | server: {
57 | // Example uses ES5 with https://github.com/github/fetch API and Promises
58 | get: function() {
59 | return fetch('myServerRefreshUrl').then(function(result) { return result.json() });
60 | },
61 |
62 | // Example uses ES6, fetch and async await
63 | set: async (info) => {
64 | const result = await fetch('myServerSaveUrl', {
65 | method: 'POST',
66 | headers: {
67 | Accept: 'application/json',
68 | 'Content-Type': 'application/json',
69 | },
70 | body: JSON.stringify(info),
71 | });
72 | return await result.json() || {}; // MUST return an object
73 | }
74 | },
75 | });
76 |
77 | export default model;
78 | ```
79 |
80 | > IMPORTANT: Your server.get method MUST return an object with ALL properties that need to be rendered in the form. If the model does not yet exist on the server, each property should have a null value but it must exist in the object or it cannot be observed with MobX.
81 |
82 | ## Example of using FormStore in a React form
83 |
84 | myForm.jsx \(this is _without_ MobxSchemaForm \(with it there is even less code\)\).
85 |
86 | ```js
87 | import React from 'react';
88 | import { observer } from 'mobx-react';
89 | import model from './myStore.js'
90 |
91 | @observer class MyForm extends React.Component {
92 | componentDidMount() {
93 | model.refresh();
94 | }
95 |
96 | onChange = (e) => {
97 | model.data[e.target.name] = e.target.value;
98 | model.dataErrors[e.target.name] = myCustomValidationPassed ? null : "error message";
99 | }
100 |
101 | onSaveClick = () => {
102 | if (!model.status.canSave || !model.status.hasChanges) return;
103 | if (myCustomValidationPassed) model.save();
104 | }
105 |
106 | render() {
107 | return (
108 | {/* ... more fields / labels ... */}
109 |
110 |
111 |
119 |
{model.dataErrors.myProperty}
120 |
121 |
127 | );
128 | }
129 | }
130 | ```
131 |
132 |
--------------------------------------------------------------------------------
/test/FormStore.spec.js:
--------------------------------------------------------------------------------
1 | import chai from 'chai';
2 | import server from './mockServer';
3 | import { extendObservable } from "mobx";
4 |
5 | let FormStore;
6 | const npmScript = process.env.npm_lifecycle_event;
7 | if (npmScript === 'test') {
8 | console.log('Testing compiled version');
9 | FormStore = require('../lib/FormStore').default;
10 | } else {
11 | FormStore = require('../src/FormStore').default;
12 | }
13 |
14 | chai.expect();
15 |
16 | const expect = chai.expect;
17 |
18 | let store;
19 |
20 | const delay = (time = 2) => {
21 | return new Promise((resolve) => setTimeout(resolve, time));
22 | };
23 |
24 | describe('FormStore with idProperty and id', function () {
25 | before(function () {
26 | store = new FormStore({ name: 'FormStore with idProperty and id', idProperty: 'id', server, /* log: console.log.bind(console) */});
27 | });
28 |
29 | it('should return the store name', () => {
30 | expect(store.options.name).to.be.equal('FormStore with idProperty and id');
31 | });
32 |
33 | describe('after getting the mock data', function () {
34 | before(async function () {
35 | server.delete();
36 | await store.refresh();
37 | });
38 |
39 | it('should have an email property in data', () => {
40 | expect(store.data).to.haveOwnProperty('email');
41 | });
42 |
43 | it('should have a status.isReady of true', () => {
44 | expect(store.status.isReady).to.be.true;
45 | });
46 | });
47 |
48 | describe('after saving bad email', function () {
49 | before(async function () {
50 | server.delete();
51 | await store.refresh();
52 | store.dataServer.id = '1';
53 | store.data.id = '1'; // only mockServer.create validates email, .set does not.
54 | store.data.email = 'bad';
55 | await store.save();
56 | });
57 |
58 | it('should have the email in data', () => {
59 | expect(store.data.email).to.equal('bad');
60 | });
61 |
62 | it('should have an error message', () => {
63 | expect(store.dataErrors.email).to.be.ok;
64 | });
65 |
66 | it('should have a status.canSave of false', () => {
67 | expect(store.status.canSave).to.be.false;
68 | });
69 | });
70 | });
71 |
72 | describe('FormStore with idProperty', function () {
73 | before(function () {
74 | store = new FormStore({ name: 'FormStore with idProperty', idProperty: 'id', server, /* log: console.log.bind(console) */});
75 | });
76 |
77 | describe('after saving for first time', function () {
78 | before(async function () {
79 | server.delete();
80 | await store.refresh();
81 | expect(store.status.mustCreate).to.be.true;
82 | store.data.email = 'test@domain.com';
83 | await store.save({ allowCreate: true });
84 | });
85 |
86 | it('should have an id', () => {
87 | expect(store.data.id).to.be.ok;
88 | expect(store.status.mustCreate).to.be.false;
89 | });
90 | });
91 |
92 | describe('refresh with unsaved data', function () {
93 | before(async function () {
94 | server.delete();
95 | await store.refresh();
96 | store.data.firstName = 'test';
97 | await store.refresh();
98 | });
99 |
100 | it('should save it so it\'s returned in refresh', () => {
101 | expect(store.data.firstName).to.equal('test');
102 | });
103 | });
104 | });
105 |
106 | describe('FormStore without idProperty', function () {
107 | const birthdate1 = new Date('2001-01-01');
108 | const birthdate2 = new Date('2001-01-01'); // same as 1
109 | const birthdate3 = new Date('2002-01-01');
110 |
111 | before(function () {
112 | store = new FormStore({ name: 'FormStore without idProperty', server, /* log: console.log.bind(console) */});
113 | });
114 |
115 | describe('after saving a date', function () {
116 | before(async function () {
117 | server.delete();
118 | await store.refresh();
119 | store.data.birthdate = birthdate1;
120 | await store.save();
121 | });
122 |
123 | it('should have a status.hasChanges of false', () => {
124 | expect(store.status.hasChanges).to.be.false;
125 | });
126 | });
127 |
128 | describe('after setting same date', function () {
129 | before(async function () {
130 | store.data.birthdate = birthdate2;
131 | });
132 |
133 | it('should have a status.hasChanges of false', () => {
134 | expect(store.status.hasChanges).to.be.false;
135 | });
136 | });
137 |
138 | describe('after changing multiple keys and saving', function () {
139 | before(async function () {
140 | store.data.firstName = 'test';
141 | store.data.birthdate = birthdate3;
142 | await store.save();
143 | });
144 |
145 | it('should have a status.hasChanges of false', () => {
146 | expect(store.status.hasChanges).to.be.false;
147 | });
148 | });
149 | });
150 |
151 | describe('AutoSaving FormStore', function () {
152 | before(function () {
153 | store = new FormStore({ name: 'AutoSaving FormStore', idProperty: 'id', autoSaveInterval: 1, server, /* log: console.log.bind(console) */});
154 | });
155 |
156 | describe('after auto-saving bad email for first time', function () {
157 | before(async function () {
158 | server.delete();
159 | await store.refresh();
160 | store.dataServer.id = '1';
161 | store.data.id = '1'; // only mockServer.create validates email, .set does not.
162 | store.data.email = 'bad';
163 | await delay(); // may not be necessary
164 | });
165 |
166 | it('should have an error message', () => {
167 | expect(store.dataErrors.email).to.be.ok;
168 | });
169 | });
170 |
171 | // relies on id set by test above
172 | describe('edits in progress', function () {
173 | it('should not save while editing', async () => {
174 | store.startEditing('firstName');
175 | store.data.firstName = 'first';
176 | await delay();
177 | // await store.save({ skipPropertyBeingEdited: true, keepServerError: true }); // same as auto-save
178 | const serverData = await server.get();
179 | expect(serverData.firstName).to.be.null;
180 | });
181 |
182 | it('should save when done editing', async () => {
183 | store.stopEditing();
184 | await delay();
185 | // await store.save({ skipPropertyBeingEdited: true, keepServerError: true }); // same as auto-save
186 | const serverData = await server.get();
187 | expect(serverData.firstName).to.equal('first');
188 | });
189 |
190 | it('field in error should remain unsaved', async () => {
191 | const serverData = await server.get();
192 | expect(serverData.email).to.be.null;
193 | });
194 |
195 | it('should save updated field when no longer in error', async () => {
196 | store.dataErrors.email = null;
197 | store.data.email = 'test@domain.com';
198 | await delay();
199 | // await store.save({ skipPropertyBeingEdited: true, keepServerError: true }); // same as auto-save
200 | const serverData = await server.get();
201 | expect(serverData.email).to.equal('test@domain.com');
202 | });
203 | });
204 | });
205 |
206 | describe('FormStore with minRefreshInterval', function () {
207 | beforeEach(function () {
208 | store = new FormStore({
209 | name: 'FormStore with minRefreshInterval',
210 | server,
211 | minRefreshInterval: 5000,
212 | /* log: console.log.bind(console) */
213 | });
214 | });
215 |
216 | it('should not perform a refresh right after prior refresh', async () => {
217 | server.delete();
218 | let result = await store.refresh();
219 | expect(result).to.be.true;
220 | result = await store.refresh();
221 | expect(result).to.be.false;
222 | });
223 |
224 | it('should not perform a refresh during another refresh', async () => {
225 | server.delete();
226 | store.refresh();
227 | const result = await store.refresh();
228 | expect(result).to.be.false;
229 | });
230 | });
231 |
232 | describe('FormStore with a computed and nested data', function () {
233 | beforeEach(function () {
234 | store = new FormStore({
235 | name: 'FormStore with a computed and nested data',
236 | server,
237 | afterRefresh: async (store) => {
238 | delete store.data.name;
239 | extendObservable(store.data, {
240 | get name() {
241 | return (store.data.firstName || store.data.lastName) && `${store.data.firstName || ''} ${store.data.lastName || ''}`.trim();
242 | },
243 | });
244 | },
245 | /* log: console.log.bind(console) */
246 | });
247 | });
248 |
249 | it('should update and save computed', async () => {
250 | server.delete();
251 | await store.refresh();
252 | store.data.firstName = 'first';
253 | store.data.lastName = 'last';
254 | expect(store.data.name).to.equal('first last');
255 | await store.save();
256 | expect(store.dataServer.name).to.equal('first last');
257 | });
258 |
259 | it('should save computed in saveAll', async () => {
260 | server.delete();
261 | await store.refresh();
262 | store.data.firstName = 'first';
263 | store.data.lastName = 'lastname';
264 | await store.save({ saveAll: true });
265 | expect(store.dataServer.name).to.equal('first lastname');
266 | });
267 |
268 | it('should save arrays and objects in saveAll', async () => {
269 | server.delete();
270 | await store.refresh();
271 | store.data.hobbies = ['chess'];
272 | store.data.attributes = {
273 | weight: 100,
274 | };
275 | await store.save({ saveAll: true });
276 | expect(store.dataServer.hobbies[0]).to.equal('chess');
277 | expect(store.dataServer.attributes.weight).to.equal(100);
278 | });
279 |
280 | it('should detect and save whole array changes', async () => {
281 | server.delete();
282 | await store.refresh();
283 | store.data.hobbies = ['chess'];
284 | expect(store.status.hasChanges).to.be.true;
285 | await store.save();
286 | expect(store.dataServer.hobbies[0]).to.equal('chess');
287 | });
288 |
289 | it('should detect and save whole object changes if contents differs', async () => {
290 | server.delete();
291 | await store.refresh();
292 | store.data.attributes = {
293 | weight: 100,
294 | height: 150,
295 | };
296 | expect(store.status.hasChanges).to.be.true;
297 | await store.save();
298 | expect(store.dataServer.attributes.weight).to.equal(100);
299 | expect(store.dataServer.attributes.height).to.equal(null); // server blocks this
300 | expect(store.data.attributes.height).to.equal(150); // confirms that removal from updates does not affect data
301 | expect(store.status.hasChanges).to.be.true;
302 | store.data.attributes = {
303 | height: null,
304 | weight: 100,
305 | };
306 | expect(store.status.hasChanges).to.be.false;
307 | });
308 | });
309 |
--------------------------------------------------------------------------------
/.idea/dbnavigator.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
--------------------------------------------------------------------------------
/src/FormStore.js:
--------------------------------------------------------------------------------
1 | import { observable, observe, autorun, autorunAsync, action, computed, asMap, isComputedProp, isObservableArray } from 'mobx';
2 | import extend from 'just-extend';
3 |
4 | const DEFAULT_SERVER_ERROR_MESSAGE = 'Lost connection to server';
5 |
6 | function isObject(obj) {
7 | return {}.toString.call(obj) === '[object Object]';
8 | }
9 |
10 | function isSame(val1, val2) {
11 | /* eslint-disable eqeqeq */
12 | return (
13 | val1 == val2 ||
14 | (val1 instanceof Date && val2 instanceof Date && val1.valueOf() == val2.valueOf()) ||
15 | ((Array.isArray(val1) || isObservableArray(val1)) &&
16 | (Array.isArray(val2) || isObservableArray(val2)) &&
17 | val1.toString() === val2.toString()
18 | ) ||
19 | (isObject(val1) && isObject(val2) && compareObjects(val1, val2))
20 | );
21 | /* eslint-enable eqeqeq */
22 | }
23 |
24 | // Based on https://github.com/angus-c/just/blob/master/packages/collection-compare/index.js
25 | function compareObjects(val1, val2) {
26 | const keys1 = Object.getOwnPropertyNames(val1).filter((key) => key[0] !== '$').sort();
27 | const keys2 = Object.getOwnPropertyNames(val2).filter((key) => key[0] !== '$').sort();
28 | const len = keys1.length;
29 | if (len !== keys2.length) {
30 | return false;
31 | }
32 | for (let i = 0; i < len; i++) {
33 | const key1 = keys1[i];
34 | const key2 = keys2[i];
35 | if (!(key1 === key2 && isSame(val1[key1], val2[key2]))) {
36 | return false;
37 | }
38 | }
39 | return true;
40 | }
41 |
42 | /**
43 | * Observes data and if changes come, add them to dataChanges,
44 | * unless it resets back to dataServer value, then clear that change
45 | * @this {FormStore}
46 | * @param {Object} change
47 | * @param {String} change.name - name of property that changed
48 | * @param {*} change.newValue
49 | */
50 | function observableChanged(change) {
51 | const store = this;
52 | action(() => {
53 | store.dataChanges.set(change.name, change.newValue);
54 |
55 | if (store.isSame(store.dataChanges.get(change.name), store.dataServer[change.name])) {
56 | store.dataChanges.delete(change.name);
57 | }
58 | })();
59 | }
60 |
61 | /**
62 | * Sets up observation on all computed data properties, if any
63 | * @param {FormStore} store
64 | */
65 | function observeComputedProperties(store) {
66 | store.observeComputedPropertiesDisposers.forEach((f) => f());
67 | store.observeComputedPropertiesDisposers = [];
68 | action(() => {
69 | Object.getOwnPropertyNames(store.data).forEach((key) => {
70 | if (isComputedProp(store.data, key)) {
71 | store.options.log(`[${store.options.name}] Observing computed property: ${key}`);
72 | const disposer = observe(store.data, key, ({ newValue }) => store.storeDataChanged({ name: key, newValue }));
73 | store.observeComputedPropertiesDisposers.push(disposer);
74 | // add or delete from dataChanges depending on whether value is same as in dataServer:
75 | store.storeDataChanged({ name: key, newValue: store.data[key] });
76 | }
77 | });
78 | })();
79 | }
80 |
81 | /**
82 | * Records successfully saved data as saved
83 | * and reverts fields server indicates to be in error
84 | * @param {FormStore} store
85 | * @param {Object} updates - what we sent to the server
86 | * @param {Object} response
87 | * @param {String} [response.data] - optional updated data to merge into the store (server.create can return id here)
88 | * @param {String} [response.status] - 'error' indicates one or more fields were invalid and not saved.
89 | * @param {String|Object} [response.error] - either a single error message to show to user if string or field-specific error messages if object
90 | * @param {String|Array} [response.error_field] - name of the field (or array of field names) in error
91 | * If autoSave is enabled, any field in error_field for which there is no error message in response.error will be reverted
92 | * to prevent autoSave from endlessly trying to save the changed field.
93 | * @returns response.status
94 | */
95 | async function processSaveResponse(store, updates, response) {
96 | store.options.log(`[${store.options.name}] Response received from server.`);
97 |
98 | if (response.status === 'error') {
99 | action(() => {
100 | let errorFields = [];
101 | if (response.error) {
102 | if (typeof response.error === 'string') {
103 | store.serverError = response.error;
104 | } else {
105 | Object.assign(store.dataErrors, response.error);
106 | errorFields = Object.keys(response.error);
107 | }
108 | }
109 |
110 | // Supports an array of field names in error_field or a string
111 | errorFields = errorFields.concat(response.error_field);
112 | errorFields.forEach((field) => {
113 | if (store.options.autoSaveInterval && !store.dataErrors[field] && store.isSame(updates[field], store.data[field])) {
114 | store.data[field] = store.dataServer[field]; // revert or it'll keep trying to autosave it
115 | }
116 | delete updates[field]; // don't save it as the new dataServer value
117 | });
118 | })();
119 | } else {
120 | store.serverError = null;
121 | }
122 |
123 | const deep = true;
124 | extend(deep, store.dataServer, updates);
125 |
126 | action(() => {
127 | if (response.data) {
128 | extend(deep, store.dataServer, response.data);
129 | extend(deep, store.data, response.data);
130 | }
131 |
132 | for (const [key, value] of Array.from(store.dataChanges)) {
133 | if (store.isSame(value, store.dataServer[key])) {
134 | store.dataChanges.delete(key);
135 | }
136 | }
137 | })();
138 |
139 | if (typeof store.options.afterSave === 'function') {
140 | await store.options.afterSave(store, updates, response);
141 | }
142 |
143 | return response.status;
144 | }
145 |
146 | /**
147 | * @param {FormStore} store
148 | * @param {Error} err
149 | */
150 | function handleError(store, err) {
151 | if (typeof store.options.server.errorMessage === 'function') {
152 | store.serverError = store.options.server.errorMessage(err);
153 | } else {
154 | store.serverError = store.options.server.errorMessage;
155 | }
156 |
157 | store.options.logError(err);
158 | }
159 |
160 | class FormStore {
161 | /** @private */
162 | options = {
163 | name: 'FormStore', // used in log statements
164 | idProperty: null,
165 | autoSaveOptions: { skipPropertyBeingEdited: true, keepServerError: true },
166 | autoSaveInterval: 0, // in ms
167 | minRefreshInterval: 0, // in ms
168 | saveNotificationStatusOnError: null,
169 | log: function noop() {},
170 | logError: console.error.bind(console), // eslint-disable-line
171 | /** @type {Boolean|function(object): Boolean} passed status object */
172 | isReadOnly: (status) => !status.isReady,
173 | server: {
174 | /** @type {undefined|function: Promise|Object} - MUST resolve to an object with all data properties present even if all have null values */
175 | get: undefined,
176 | /** @type {undefined|function(object): Promise|Object} passed updates object - see processSaveResponse for expected error response properties */
177 | set: undefined,
178 | /** @type {undefined|function(object}: Promise|Object} passed updates object - see processSaveResponse for expected error response properties */
179 | create: undefined,
180 | /** @type {String|function(error): String} passed error object */
181 | errorMessage: DEFAULT_SERVER_ERROR_MESSAGE,
182 | },
183 | /** @type {undefined|function(FormStore): Promise|Boolean} passed store instance - if it returns false, no refresh will be performed */
184 | beforeRefresh: undefined,
185 | /** @type {undefined|function(FormStore): Promise} passed store instance */
186 | afterRefresh: undefined,
187 | /** @type {undefined|function(FormStore, object, object): Promise|Boolean} passed store instance, updates object and saveOptions object,
188 | * (i.e. with skipPropertyBeingEdited, etc booleans) - if it returns false, no save will be performed */
189 | beforeSave: undefined,
190 | /** @type {undefined|function(FormStore, object, object): Promise} passed store instance, updates object and response object
191 | * - updates object will already have fields removed from it that response indicates are in error */
192 | afterSave: undefined,
193 | };
194 |
195 | /**
196 | * @private
197 | * @type {null|Date}
198 | */
199 | lastSync = null;
200 | /** @private */
201 | saveQueue = Promise.resolve();
202 | /** @private */
203 | observeDataObjectDisposer;
204 | /** @private */
205 | observeDataPropertiesDisposer;
206 | /**
207 | * @private
208 | * @type {Array}
209 | */
210 | observeComputedPropertiesDisposers = [];
211 | /** @private */
212 | autorunDisposer;
213 |
214 | /** @private */
215 | @observable isReady = false; // true after initial data load (refresh) has completed
216 | /** @private */
217 | @observable isLoading = false;
218 | /** @private */
219 | @observable isSaving = false;
220 | /** @private */
221 | @observable serverError = null; // stores both communication error and any explicit response.error returned to save
222 |
223 | /** @private */
224 | // To support both Mobx 2.2+ and 3+, this is now done in constructor:
225 | // @observable dataChanges = asMap(); // changes that will be sent to server
226 |
227 | /** @private */
228 | dataServer = {}; // data returned by the server (kept for checking old values)
229 |
230 | @observable data = {};
231 | // stores validation error message if any for each field (data structure is identical to data)
232 | @observable dataErrors = {};
233 | // active is set to true right after a save is completed and status is set to response.status
234 | // this allows a confirmation message to be shown to user and to drive its dismissal,
235 | // UI can set this observable's active property back to false.
236 | @observable saveNotification = { active: false, status: null };
237 | @observable propertyBeingEdited = null; // property currently being edited as set by startEditing()
238 |
239 | isSame = isSame;
240 |
241 | constructor(options, data) {
242 | const store = this;
243 | Object.assign(store.options, options);
244 | if (!data && typeof store.options.server.get !== 'function') {
245 | throw new Error('options must specify server get function or supply initial data object to constructor');
246 | }
247 | if (!typeof store.options.server.create !== 'function' && typeof store.options.server.set !== 'function') {
248 | throw new Error('options must specify server set and/or create function(s)');
249 | }
250 | store.options.server.errorMessage = store.options.server.errorMessage || DEFAULT_SERVER_ERROR_MESSAGE;
251 |
252 | // Supports both Mobx 3+ (observable.map) and 2.x (asMap) without deprecation warnings:
253 | store.dataChanges = observable.map ? observable.map() : asMap(); // changes that will be sent to server
254 |
255 | // register observe for changes to properties in store.data as well as to complete replacement of store.data object
256 | store.storeDataChanged = observableChanged.bind(store);
257 | store.observeDataPropertiesDisposer = observe(store.data, store.storeDataChanged);
258 | store.observeDataObjectDisposer = observe(store, 'data', () => {
259 | store.observeDataPropertiesDisposer && store.observeDataPropertiesDisposer();
260 | store.observeDataPropertiesDisposer = observe(store.data, store.storeDataChanged);
261 |
262 | store.dataChanges.clear();
263 | action(() => {
264 | Object.keys(store.data).forEach((key) => {
265 | const value = store.data[key];
266 | if (!store.isSame(value, store.dataServer[key])) {
267 | store.dataChanges.set(key, value);
268 | }
269 | });
270 | observeComputedProperties(store);
271 | })();
272 | });
273 |
274 | store.configAutoSave(store.options.autoSaveInterval, store.options.autoSaveOptions);
275 |
276 | if (data) {
277 | store.reset(data);
278 | }
279 | }
280 |
281 | /**
282 | * disposes of all internal observation/autoruns so this instance can be garbage-collected.
283 | */
284 | dispose() {
285 | const store = this;
286 | store.autorunDisposer && store.autorunDisposer();
287 | store.observeDataObjectDisposer && store.observeDataObjectDisposer();
288 | store.observeDataPropertiesDisposer && store.observeDataPropertiesDisposer();
289 | store.observeComputedPropertiesDisposers.forEach((f) => f());
290 | store.autorunDisposer = undefined;
291 | store.observeDataObjectDisposer = undefined;
292 | store.observeDataPropertiesDisposer = undefined;
293 | store.observeComputedPropertiesDisposers = [];
294 | }
295 |
296 | /**
297 | * Configures and enables or disables auto-save
298 | * @param {Number} autoSaveInterval - (in ms) - if non-zero autosave will be enabled, otherwise disabled
299 | * @param {Object} [autoSaveOptions] - overrides the default autoSaveOptions if provided
300 | */
301 | configAutoSave(autoSaveInterval, autoSaveOptions) {
302 | const store = this;
303 | store.autorunDisposer && store.autorunDisposer();
304 | store.options.autoSaveInterval = autoSaveInterval;
305 | store.options.autoSaveOptions = autoSaveOptions || store.options.autoSaveOptions;
306 |
307 | // auto-save by observing dataChanges keys
308 | if (store.options.autoSaveInterval) {
309 | // Supports both Mobx <=3 (autorunAsync) and Mobx 4+
310 | // (ObservableMap keys no longer returning an Array is used to detect Mobx 4+,
311 | // because in non-production build autorunAsync exists in 4.x to issue deprecation error)
312 | const asyncAutorun = Array.isArray(store.dataChanges.keys()) ? autorunAsync : (fn, delay) => autorun(fn, { delay });
313 |
314 | store.autorunDisposer = asyncAutorun(() => {
315 | if (!store.status.mustCreate && Array.from(store.dataChanges).length) {
316 | store.options.log(`[${store.options.name}] Auto-save started...`);
317 | store.save(store.options.autoSaveOptions);
318 | }
319 | }, store.options.autoSaveInterval);
320 | } else {
321 | store.autorunDisposer = undefined;
322 | }
323 | }
324 |
325 | /**
326 | * Marks data property as edit-in-progress and therefore it should not be autosaved - to be called on field focus
327 | * @param {String|Array} name - field/property name (Array format supports json schema forms)
328 | */
329 | startEditing(name) {
330 | const store = this;
331 | store.propertyBeingEdited = Array.isArray(name) ? name[0] : name;
332 | }
333 |
334 | // to be called on field blur, any field name parameter is ignored
335 | stopEditing() {
336 | const store = this;
337 | store.propertyBeingEdited = null;
338 | if (store.status.hasChanges) {
339 | // This will trigger autorun in case it already ran while we were editing:
340 | action(() => {
341 | // In MobX 4+, ObservableMap.keys() returns an Iterable, not an array
342 | const key = Array.from(store.dataChanges)[0][0];
343 | const value = store.dataChanges.get(key);
344 | store.dataChanges.delete(key);
345 | store.dataChanges.set(key, value);
346 | })();
347 | }
348 | }
349 |
350 | /**
351 | * Returns the value of a field/property, optionally returning the last saved value for not validated/in progress fields
352 | * Without validated:true, using this function is not necessary, can just access store.data[name].
353 | * @param {String|Array} name - field/property name (Array format supports json schema forms)
354 | * @param {Boolean} [validated] - only return validated value, i.e. if it's in error, fallback to dataServer
355 | * @param {Boolean} [skipPropertyBeingEdited] - used only when validated is true to again fallback to dataServer
356 | * @returns {*}
357 | */
358 | getValue(name, validated, skipPropertyBeingEdited) {
359 | const store = this;
360 | const prop = Array.isArray(name) ? name[0] : name;
361 | if (validated) {
362 | // check if property is being edited or invalid
363 | if ((skipPropertyBeingEdited && prop === store.propertyBeingEdited) || store.dataErrors[prop]) {
364 | return store.dataServer[prop];
365 | }
366 | }
367 | return store.data[prop];
368 | }
369 |
370 | // Returns the last saved (or server-provided) set of data
371 | // - in an afterSave callback it already includes merged updates that were not in error
372 | getSavedData() {
373 | const store = this;
374 | return store.dataServer;
375 | }
376 |
377 | /**
378 | * @returns {{errors: Array, isReady: Boolean, isInProgress: Boolean, canSave: Boolean, hasChanges: Boolean, isReadOnly: Boolean}}
379 | * errors is an array of any serverError plus all the error messages from all fields (in no particular order)
380 | * (serverError is either the string returned in response.error or a communication error and is cleared on every refresh and save)
381 | * isReady indicates initial data load (refresh) has been completed and user can start entering data
382 | * isInProgress indicates either a refresh or a save is in progress
383 | * canSave is true when no refresh or save is in progress and there are no validation errors
384 | * hasChanges is true when one or more data properties has a value that's different from last-saved/server-loaded data.
385 | * isReadOnly by default is true when isReady is false but can be set to the return value of an
386 | * optional callback to which this status object (without isReadOnly) is passed
387 | */
388 | @computed get status() {
389 | const store = this;
390 | let errors = [];
391 |
392 | if (store.serverError) {
393 | errors = [store.serverError];
394 | }
395 |
396 | Object.keys(store.dataErrors).forEach((key) => {
397 | if (store.dataErrors[key]) {
398 | errors.push(store.dataErrors[key]);
399 | }
400 | });
401 |
402 | const status = {
403 | errors,
404 | isReady: store.isReady,
405 | isInProgress: store.isLoading || store.isSaving,
406 | canSave: !store.isLoading && !store.isSaving && (store.serverError ? errors.length === 1 : errors.length === 0),
407 | hasChanges: !!store.dataChanges.size,
408 | mustCreate: !!(store.options.idProperty && !store.dataServer[store.options.idProperty]),
409 | };
410 | if (typeof store.options.isReadOnly === 'function') {
411 | status.isReadOnly = store.options.isReadOnly(status);
412 | } else {
413 | status.isReadOnly = store.options.isReadOnly;
414 | }
415 | return status;
416 | }
417 |
418 | /**
419 | * Copies dataServer into data and resets the error observable and lastSync.
420 | * Mostly for internal use by constructor and refresh().
421 | * @param {Object} [data] If provided, dataServer will be set to it and store.isReady will be set to true
422 | */
423 | reset(data) {
424 | const store = this;
425 |
426 | action(() => {
427 | if (data) {
428 | store.dataServer = data;
429 | }
430 | const deep = true;
431 | store.data = extend(deep, {}, store.dataServer);
432 |
433 | // setup error observable
434 | const temp = {};
435 | Object.keys(store.data).forEach((key) => {
436 | temp[key] = null;
437 | });
438 | store.dataErrors = temp;
439 |
440 | store.lastSync = null;
441 | observeComputedProperties(store);
442 | if (data && !store.isReady) store.isReady = true;
443 | })();
444 | }
445 |
446 | /**
447 | * Loads data from server unless a refresh was performed within the last minRefreshInterval (i.e. 15 minutes).
448 | * If there are pending (and ready to save) changes, triggers save instead and 'resets the clock' on minRefreshInterval.
449 | * For a store with idProperty defined, if that data property is falsy in data received from server,
450 | * loads from server only the very first time refresh() is called unless called with allowIfMustCreate=true option.
451 | * @param {Object} [refreshOptions]
452 | * @param {Boolean} [refreshOptions.allowIfMustCreate=false]
453 | * @param {Boolean} [refreshOptions.ignoreMinRefreshInterval=false]
454 | * @returns {Promise|Boolean} resolves to true if refresh actually performed, false if skipped
455 | */
456 | async refresh(refreshOptions = {}) {
457 | // for some reason this syntax is erroring in tests:
458 | // const { allowIfMustCreate = false, ignoreMinRefreshInterval = false } = refreshOptions;
459 | const allowIfMustCreate = refreshOptions.allowIfMustCreate || false;
460 | const ignoreMinRefreshInterval = refreshOptions.ignoreMinRefreshInterval || false;
461 | const store = this;
462 | if (!store.options.server.get || (store.isReady && store.status.mustCreate && !allowIfMustCreate)) {
463 | return false;
464 | }
465 | store.options.log(`[${store.options.name}] Starting data refresh...`);
466 |
467 | if (store.isLoading) {
468 | store.options.log(`[${store.options.name}] Data is already being refreshed.`);
469 | return false;
470 | }
471 |
472 | const now = new Date();
473 | const past = new Date(Date.now() - store.options.minRefreshInterval);
474 |
475 | // check if lastSync is between now and 15 minutes ago
476 | if (!ignoreMinRefreshInterval && past < store.lastSync && store.lastSync <= now) {
477 | store.options.log(`[${store.options.name}] Data refreshed within last ${store.options.minRefreshInterval / 1000} seconds.`);
478 | return false;
479 | }
480 |
481 | if (store.status.hasChanges && !store.status.mustCreate) {
482 | store.options.log(`[${store.options.name}] Unsaved changes detected...`);
483 |
484 | if (await store.save()) {
485 | store.options.log(`[${store.options.name}] Postponing refresh for ${store.options.minRefreshInterval / 1000} seconds.`);
486 | store.lastSync = new Date();
487 | return false;
488 | }
489 | }
490 |
491 | if (typeof store.options.beforeRefresh === 'function') {
492 | if (await store.options.beforeRefresh(store) === false) {
493 | return false;
494 | }
495 | }
496 |
497 | store.options.log(`[${store.options.name}] Refreshing data...`);
498 | store.isLoading = true;
499 |
500 | try {
501 | const result = await store.options.server.get();
502 | store.options.log(`[${store.options.name}] Data received from server.`);
503 |
504 | action(() => {
505 | store.dataServer = result;
506 | store.serverError = null;
507 | store.reset();
508 | store.lastSync = new Date();
509 | })();
510 |
511 | if (typeof store.options.afterRefresh === 'function') {
512 | await store.options.afterRefresh(store);
513 | observeComputedProperties(store); // again, in case afterRefresh added some
514 | }
515 |
516 | store.options.log(`[${store.options.name}] Refresh finished.`);
517 | if (!store.isReady) store.isReady = true;
518 | } catch (err) {
519 | handleError(store, err);
520 | }
521 |
522 | store.isLoading = false;
523 | return true;
524 | }
525 |
526 | /**
527 | * Sends ready-to-save data changes to the server (normally using server.set unless it's undefined, then with server.create)
528 | * For a store with idProperty defined when that property is falsy in the data received from server
529 | * and allowCreate=true, uses server.create instead.
530 | * Calls to save() while one is in progress are queued.
531 | * @param {Object} saveOptions - the object as a whole is also passed to the beforeSave callback
532 | * @param {Boolean} [saveOptions.allowCreate=false] - for a store with idProperty defined, this must be true
533 | * for the save to actually be performed when that property is falsy.
534 | * @param {Boolean} [saveOptions.saveAll=false] - normally save only sends changes and if no changes, no save is done.
535 | * if saveAll=true, sends the full data object regardless of changes.
536 | * @param {Boolean} [saveOptions.skipPropertyBeingEdited=false] - true in an auto-save
537 | * @param {Boolean} [saveOptions.keepServerError=false] - true in an auto-save, otherwise will also deactivate saveNotification prior to save
538 | * @returns {Promise|Boolean} resolves to true if save actually performed, false if skipped
539 | */
540 | save(saveOptions = {}) {
541 | const { allowCreate = false, saveAll = false, skipPropertyBeingEdited = false, keepServerError = false } = saveOptions;
542 | const store = this;
543 |
544 | store.saveQueue = store.saveQueue.then(
545 | async () => {
546 | if (store.status.mustCreate && !allowCreate) {
547 | return false;
548 | }
549 | store.options.log(`[${store.options.name}] Starting data save...`);
550 |
551 | const deep = true;
552 | let updates;
553 | if (saveAll) {
554 | updates = {};
555 | Object.getOwnPropertyNames(store.data).forEach((property) => {
556 | if (property[0] === '$') {
557 | return;
558 | }
559 | if (isObservableArray(store.data[property])) {
560 | updates[property] = store.data[property].slice();
561 | return;
562 | }
563 | updates[property] = store.data[property];
564 | });
565 | updates = extend(deep, {}, updates);
566 | } else {
567 | // Mobx 4+ toJS() exports a Map, not an Object and toJSON is the 'legacy' method to export an Object
568 | updates = store.dataChanges.toJSON ? store.dataChanges.toJSON() : store.dataChanges.toJS();
569 |
570 | if (Object.keys(updates).length === 0) {
571 | store.options.log(`[${store.options.name}] No changes to save.`);
572 | return false;
573 | }
574 |
575 | // check if we have property currently being edited in changes
576 | // or if a property has an error and clone (observable or regular)
577 | // Arrays and Objects to plain ones
578 | Object.keys(updates).forEach((property) => {
579 | if (skipPropertyBeingEdited && property === store.propertyBeingEdited) {
580 | store.options.log(`[${store.options.name}] Property "${property}" is being edited.`);
581 | delete updates[property];
582 | return;
583 | }
584 |
585 | if (store.dataErrors[property]) {
586 | store.options.log(`[${store.options.name}] Property "${property}" is not validated.`);
587 | delete updates[property];
588 | return;
589 | }
590 |
591 | if (store.isSame(updates[property], store.dataServer[property])) {
592 | store.options.log(`[${store.options.name}] Property "${property}" is same as on the server.`);
593 | delete updates[property];
594 | store.dataChanges.delete(property);
595 | return;
596 | }
597 |
598 | if (Array.isArray(updates[property]) || isObservableArray(updates[property])) {
599 | updates[property] = updates[property].slice();
600 | } else if (isObject(updates[property])) {
601 | updates[property] = extend(deep, {}, updates[property]);
602 | }
603 | });
604 |
605 | if (Object.keys(updates).length === 0) {
606 | store.options.log(`[${store.options.name}] No changes ready to save.`);
607 | return false;
608 | }
609 | }
610 |
611 | if (typeof store.options.beforeSave === 'function') {
612 | if (await store.options.beforeSave(store, updates, saveOptions) === false) {
613 | return false;
614 | }
615 | }
616 |
617 | store.options.log(`[${store.options.name}] Saving data...`);
618 | store.options.log(updates);
619 | store.isSaving = true;
620 |
621 | try {
622 | if (!keepServerError) {
623 | store.saveNotification.active = false;
624 | store.serverError = null;
625 | }
626 |
627 | let response;
628 | if (store.options.server.set && (!store.options.server.create || !store.status.mustCreate)) {
629 | response = await store.options.server.set(updates);
630 | } else {
631 | response = await store.options.server.create(updates);
632 | }
633 |
634 | store.saveNotification.status = await processSaveResponse(store, updates, response);
635 | store.saveNotification.active = true;
636 |
637 | store.options.log(`[${store.options.name}] Save finished.`);
638 | } catch (err) {
639 | handleError(store, err);
640 | if (store.options.saveNotificationStatusOnError) {
641 | store.saveNotification.status = store.options.saveNotificationStatusOnError;
642 | store.saveNotification.active = true;
643 | }
644 | }
645 |
646 | store.isSaving = false;
647 | return true;
648 | }
649 | );
650 |
651 | return store.saveQueue;
652 | }
653 | }
654 |
655 | export default FormStore;
656 |
--------------------------------------------------------------------------------
/lib/FormStore.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | Object.defineProperty(exports, "__esModule", {
4 | value: true
5 | });
6 | exports["default"] = void 0;
7 |
8 | var _mobx = require("mobx");
9 |
10 | var _justExtend = _interopRequireDefault(require("just-extend"));
11 |
12 | var _class, _descriptor, _descriptor2, _descriptor3, _descriptor4, _descriptor5, _descriptor6, _descriptor7, _descriptor8;
13 |
14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
15 |
16 | function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
17 |
18 | function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
19 |
20 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
21 |
22 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
23 |
24 | function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
25 |
26 | function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
27 |
28 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
29 |
30 | function _initializerDefineProperty(target, property, descriptor, context) { if (!descriptor) return; Object.defineProperty(target, property, { enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: descriptor.writable, value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 }); }
31 |
32 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
33 |
34 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
35 |
36 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
37 |
38 | function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; Object.keys(descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if ('value' in desc || desc.initializer) { desc.writable = true; } desc = decorators.slice().reverse().reduce(function (desc, decorator) { return decorator(target, property, desc) || desc; }, desc); if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object.defineProperty(target, property, desc); desc = null; } return desc; }
39 |
40 | function _initializerWarningHelper(descriptor, context) { throw new Error('Decorating class property failed. Please ensure that ' + 'proposal-class-properties is enabled and runs after the decorators transform.'); }
41 |
42 | function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
43 |
44 | function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
45 |
46 | var DEFAULT_SERVER_ERROR_MESSAGE = 'Lost connection to server';
47 |
48 | function isObject(obj) {
49 | return {}.toString.call(obj) === '[object Object]';
50 | }
51 |
52 | function isSame(val1, val2) {
53 | /* eslint-disable eqeqeq */
54 | return val1 == val2 || val1 instanceof Date && val2 instanceof Date && val1.valueOf() == val2.valueOf() || (Array.isArray(val1) || (0, _mobx.isObservableArray)(val1)) && (Array.isArray(val2) || (0, _mobx.isObservableArray)(val2)) && val1.toString() === val2.toString() || isObject(val1) && isObject(val2) && compareObjects(val1, val2);
55 | /* eslint-enable eqeqeq */
56 | } // Based on https://github.com/angus-c/just/blob/master/packages/collection-compare/index.js
57 |
58 |
59 | function compareObjects(val1, val2) {
60 | var keys1 = Object.getOwnPropertyNames(val1).filter(function (key) {
61 | return key[0] !== '$';
62 | }).sort();
63 | var keys2 = Object.getOwnPropertyNames(val2).filter(function (key) {
64 | return key[0] !== '$';
65 | }).sort();
66 | var len = keys1.length;
67 |
68 | if (len !== keys2.length) {
69 | return false;
70 | }
71 |
72 | for (var i = 0; i < len; i++) {
73 | var key1 = keys1[i];
74 | var key2 = keys2[i];
75 |
76 | if (!(key1 === key2 && isSame(val1[key1], val2[key2]))) {
77 | return false;
78 | }
79 | }
80 |
81 | return true;
82 | }
83 | /**
84 | * Observes data and if changes come, add them to dataChanges,
85 | * unless it resets back to dataServer value, then clear that change
86 | * @this {FormStore}
87 | * @param {Object} change
88 | * @param {String} change.name - name of property that changed
89 | * @param {*} change.newValue
90 | */
91 |
92 |
93 | function observableChanged(change) {
94 | var store = this;
95 | (0, _mobx.action)(function () {
96 | store.dataChanges.set(change.name, change.newValue);
97 |
98 | if (store.isSame(store.dataChanges.get(change.name), store.dataServer[change.name])) {
99 | store.dataChanges["delete"](change.name);
100 | }
101 | })();
102 | }
103 | /**
104 | * Sets up observation on all computed data properties, if any
105 | * @param {FormStore} store
106 | */
107 |
108 |
109 | function observeComputedProperties(store) {
110 | store.observeComputedPropertiesDisposers.forEach(function (f) {
111 | return f();
112 | });
113 | store.observeComputedPropertiesDisposers = [];
114 | (0, _mobx.action)(function () {
115 | Object.getOwnPropertyNames(store.data).forEach(function (key) {
116 | if ((0, _mobx.isComputedProp)(store.data, key)) {
117 | store.options.log("[".concat(store.options.name, "] Observing computed property: ").concat(key));
118 | var disposer = (0, _mobx.observe)(store.data, key, function (_ref) {
119 | var newValue = _ref.newValue;
120 | return store.storeDataChanged({
121 | name: key,
122 | newValue: newValue
123 | });
124 | });
125 | store.observeComputedPropertiesDisposers.push(disposer); // add or delete from dataChanges depending on whether value is same as in dataServer:
126 |
127 | store.storeDataChanged({
128 | name: key,
129 | newValue: store.data[key]
130 | });
131 | }
132 | });
133 | })();
134 | }
135 | /**
136 | * Records successfully saved data as saved
137 | * and reverts fields server indicates to be in error
138 | * @param {FormStore} store
139 | * @param {Object} updates - what we sent to the server
140 | * @param {Object} response
141 | * @param {String} [response.data] - optional updated data to merge into the store (server.create can return id here)
142 | * @param {String} [response.status] - 'error' indicates one or more fields were invalid and not saved.
143 | * @param {String|Object} [response.error] - either a single error message to show to user if string or field-specific error messages if object
144 | * @param {String|Array} [response.error_field] - name of the field (or array of field names) in error
145 | * If autoSave is enabled, any field in error_field for which there is no error message in response.error will be reverted
146 | * to prevent autoSave from endlessly trying to save the changed field.
147 | * @returns response.status
148 | */
149 |
150 |
151 | function processSaveResponse(_x, _x2, _x3) {
152 | return _processSaveResponse.apply(this, arguments);
153 | }
154 | /**
155 | * @param {FormStore} store
156 | * @param {Error} err
157 | */
158 |
159 |
160 | function _processSaveResponse() {
161 | _processSaveResponse = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee3(store, updates, response) {
162 | var deep;
163 | return regeneratorRuntime.wrap(function _callee3$(_context3) {
164 | while (1) {
165 | switch (_context3.prev = _context3.next) {
166 | case 0:
167 | store.options.log("[".concat(store.options.name, "] Response received from server."));
168 |
169 | if (response.status === 'error') {
170 | (0, _mobx.action)(function () {
171 | var errorFields = [];
172 |
173 | if (response.error) {
174 | if (typeof response.error === 'string') {
175 | store.serverError = response.error;
176 | } else {
177 | Object.assign(store.dataErrors, response.error);
178 | errorFields = Object.keys(response.error);
179 | }
180 | } // Supports an array of field names in error_field or a string
181 |
182 |
183 | errorFields = errorFields.concat(response.error_field);
184 | errorFields.forEach(function (field) {
185 | if (store.options.autoSaveInterval && !store.dataErrors[field] && store.isSame(updates[field], store.data[field])) {
186 | store.data[field] = store.dataServer[field]; // revert or it'll keep trying to autosave it
187 | }
188 |
189 | delete updates[field]; // don't save it as the new dataServer value
190 | });
191 | })();
192 | } else {
193 | store.serverError = null;
194 | }
195 |
196 | deep = true;
197 | (0, _justExtend["default"])(deep, store.dataServer, updates);
198 | (0, _mobx.action)(function () {
199 | if (response.data) {
200 | (0, _justExtend["default"])(deep, store.dataServer, response.data);
201 | (0, _justExtend["default"])(deep, store.data, response.data);
202 | }
203 |
204 | for (var _i = 0, _Array$from = Array.from(store.dataChanges); _i < _Array$from.length; _i++) {
205 | var _Array$from$_i = _slicedToArray(_Array$from[_i], 2),
206 | key = _Array$from$_i[0],
207 | value = _Array$from$_i[1];
208 |
209 | if (store.isSame(value, store.dataServer[key])) {
210 | store.dataChanges["delete"](key);
211 | }
212 | }
213 | })();
214 |
215 | if (!(typeof store.options.afterSave === 'function')) {
216 | _context3.next = 8;
217 | break;
218 | }
219 |
220 | _context3.next = 8;
221 | return store.options.afterSave(store, updates, response);
222 |
223 | case 8:
224 | return _context3.abrupt("return", response.status);
225 |
226 | case 9:
227 | case "end":
228 | return _context3.stop();
229 | }
230 | }
231 | }, _callee3);
232 | }));
233 | return _processSaveResponse.apply(this, arguments);
234 | }
235 |
236 | function handleError(store, err) {
237 | if (typeof store.options.server.errorMessage === 'function') {
238 | store.serverError = store.options.server.errorMessage(err);
239 | } else {
240 | store.serverError = store.options.server.errorMessage;
241 | }
242 |
243 | store.options.logError(err);
244 | }
245 |
246 | var FormStore = (_class = /*#__PURE__*/function () {
247 | /** @private */
248 |
249 | /**
250 | * @private
251 | * @type {null|Date}
252 | */
253 |
254 | /** @private */
255 |
256 | /** @private */
257 |
258 | /** @private */
259 |
260 | /**
261 | * @private
262 | * @type {Array}
263 | */
264 |
265 | /** @private */
266 |
267 | /** @private */
268 | // true after initial data load (refresh) has completed
269 |
270 | /** @private */
271 |
272 | /** @private */
273 |
274 | /** @private */
275 | // stores both communication error and any explicit response.error returned to save
276 |
277 | /** @private */
278 | // To support both Mobx 2.2+ and 3+, this is now done in constructor:
279 | // @observable dataChanges = asMap(); // changes that will be sent to server
280 |
281 | /** @private */
282 | // data returned by the server (kept for checking old values)
283 | // stores validation error message if any for each field (data structure is identical to data)
284 | // active is set to true right after a save is completed and status is set to response.status
285 | // this allows a confirmation message to be shown to user and to drive its dismissal,
286 | // UI can set this observable's active property back to false.
287 | // property currently being edited as set by startEditing()
288 | function FormStore(options, data) {
289 | _classCallCheck(this, FormStore);
290 |
291 | this.options = {
292 | name: 'FormStore',
293 | // used in log statements
294 | idProperty: null,
295 | autoSaveOptions: {
296 | skipPropertyBeingEdited: true,
297 | keepServerError: true
298 | },
299 | autoSaveInterval: 0,
300 | // in ms
301 | minRefreshInterval: 0,
302 | // in ms
303 | saveNotificationStatusOnError: null,
304 | log: function noop() {},
305 | logError: console.error.bind(console),
306 | // eslint-disable-line
307 |
308 | /** @type {Boolean|function(object): Boolean} passed status object */
309 | isReadOnly: function isReadOnly(status) {
310 | return !status.isReady;
311 | },
312 | server: {
313 | /** @type {undefined|function: Promise|Object} - MUST resolve to an object with all data properties present even if all have null values */
314 | get: undefined,
315 |
316 | /** @type {undefined|function(object): Promise|Object} passed updates object - see processSaveResponse for expected error response properties */
317 | set: undefined,
318 |
319 | /** @type {undefined|function(object}: Promise|Object} passed updates object - see processSaveResponse for expected error response properties */
320 | create: undefined,
321 |
322 | /** @type {String|function(error): String} passed error object */
323 | errorMessage: DEFAULT_SERVER_ERROR_MESSAGE
324 | },
325 |
326 | /** @type {undefined|function(FormStore): Promise|Boolean} passed store instance - if it returns false, no refresh will be performed */
327 | beforeRefresh: undefined,
328 |
329 | /** @type {undefined|function(FormStore): Promise} passed store instance */
330 | afterRefresh: undefined,
331 |
332 | /** @type {undefined|function(FormStore, object, object): Promise|Boolean} passed store instance, updates object and saveOptions object,
333 | * (i.e. with skipPropertyBeingEdited, etc booleans) - if it returns false, no save will be performed */
334 | beforeSave: undefined,
335 |
336 | /** @type {undefined|function(FormStore, object, object): Promise} passed store instance, updates object and response object
337 | * - updates object will already have fields removed from it that response indicates are in error */
338 | afterSave: undefined
339 | };
340 | this.lastSync = null;
341 | this.saveQueue = Promise.resolve();
342 | this.observeDataObjectDisposer = void 0;
343 | this.observeDataPropertiesDisposer = void 0;
344 | this.observeComputedPropertiesDisposers = [];
345 | this.autorunDisposer = void 0;
346 |
347 | _initializerDefineProperty(this, "isReady", _descriptor, this);
348 |
349 | _initializerDefineProperty(this, "isLoading", _descriptor2, this);
350 |
351 | _initializerDefineProperty(this, "isSaving", _descriptor3, this);
352 |
353 | _initializerDefineProperty(this, "serverError", _descriptor4, this);
354 |
355 | this.dataServer = {};
356 |
357 | _initializerDefineProperty(this, "data", _descriptor5, this);
358 |
359 | _initializerDefineProperty(this, "dataErrors", _descriptor6, this);
360 |
361 | _initializerDefineProperty(this, "saveNotification", _descriptor7, this);
362 |
363 | _initializerDefineProperty(this, "propertyBeingEdited", _descriptor8, this);
364 |
365 | this.isSame = isSame;
366 | var store = this;
367 | Object.assign(store.options, options);
368 |
369 | if (!data && typeof store.options.server.get !== 'function') {
370 | throw new Error('options must specify server get function or supply initial data object to constructor');
371 | }
372 |
373 | if (!_typeof(store.options.server.create) !== 'function' && typeof store.options.server.set !== 'function') {
374 | throw new Error('options must specify server set and/or create function(s)');
375 | }
376 |
377 | store.options.server.errorMessage = store.options.server.errorMessage || DEFAULT_SERVER_ERROR_MESSAGE; // Supports both Mobx 3+ (observable.map) and 2.x (asMap) without deprecation warnings:
378 |
379 | store.dataChanges = _mobx.observable.map ? _mobx.observable.map() : (0, _mobx.asMap)(); // changes that will be sent to server
380 | // register observe for changes to properties in store.data as well as to complete replacement of store.data object
381 |
382 | store.storeDataChanged = observableChanged.bind(store);
383 | store.observeDataPropertiesDisposer = (0, _mobx.observe)(store.data, store.storeDataChanged);
384 | store.observeDataObjectDisposer = (0, _mobx.observe)(store, 'data', function () {
385 | store.observeDataPropertiesDisposer && store.observeDataPropertiesDisposer();
386 | store.observeDataPropertiesDisposer = (0, _mobx.observe)(store.data, store.storeDataChanged);
387 | store.dataChanges.clear();
388 | (0, _mobx.action)(function () {
389 | Object.keys(store.data).forEach(function (key) {
390 | var value = store.data[key];
391 |
392 | if (!store.isSame(value, store.dataServer[key])) {
393 | store.dataChanges.set(key, value);
394 | }
395 | });
396 | observeComputedProperties(store);
397 | })();
398 | });
399 | store.configAutoSave(store.options.autoSaveInterval, store.options.autoSaveOptions);
400 |
401 | if (data) {
402 | store.reset(data);
403 | }
404 | }
405 | /**
406 | * disposes of all internal observation/autoruns so this instance can be garbage-collected.
407 | */
408 |
409 |
410 | _createClass(FormStore, [{
411 | key: "dispose",
412 | value: function dispose() {
413 | var store = this;
414 | store.autorunDisposer && store.autorunDisposer();
415 | store.observeDataObjectDisposer && store.observeDataObjectDisposer();
416 | store.observeDataPropertiesDisposer && store.observeDataPropertiesDisposer();
417 | store.observeComputedPropertiesDisposers.forEach(function (f) {
418 | return f();
419 | });
420 | store.autorunDisposer = undefined;
421 | store.observeDataObjectDisposer = undefined;
422 | store.observeDataPropertiesDisposer = undefined;
423 | store.observeComputedPropertiesDisposers = [];
424 | }
425 | /**
426 | * Configures and enables or disables auto-save
427 | * @param {Number} autoSaveInterval - (in ms) - if non-zero autosave will be enabled, otherwise disabled
428 | * @param {Object} [autoSaveOptions] - overrides the default autoSaveOptions if provided
429 | */
430 |
431 | }, {
432 | key: "configAutoSave",
433 | value: function configAutoSave(autoSaveInterval, autoSaveOptions) {
434 | var store = this;
435 | store.autorunDisposer && store.autorunDisposer();
436 | store.options.autoSaveInterval = autoSaveInterval;
437 | store.options.autoSaveOptions = autoSaveOptions || store.options.autoSaveOptions; // auto-save by observing dataChanges keys
438 |
439 | if (store.options.autoSaveInterval) {
440 | // Supports both Mobx <=3 (autorunAsync) and Mobx 4+
441 | // (ObservableMap keys no longer returning an Array is used to detect Mobx 4+,
442 | // because in non-production build autorunAsync exists in 4.x to issue deprecation error)
443 | var asyncAutorun = Array.isArray(store.dataChanges.keys()) ? _mobx.autorunAsync : function (fn, delay) {
444 | return (0, _mobx.autorun)(fn, {
445 | delay: delay
446 | });
447 | };
448 | store.autorunDisposer = asyncAutorun(function () {
449 | if (!store.status.mustCreate && Array.from(store.dataChanges).length) {
450 | store.options.log("[".concat(store.options.name, "] Auto-save started..."));
451 | store.save(store.options.autoSaveOptions);
452 | }
453 | }, store.options.autoSaveInterval);
454 | } else {
455 | store.autorunDisposer = undefined;
456 | }
457 | }
458 | /**
459 | * Marks data property as edit-in-progress and therefore it should not be autosaved - to be called on field focus
460 | * @param {String|Array} name - field/property name (Array format supports json schema forms)
461 | */
462 |
463 | }, {
464 | key: "startEditing",
465 | value: function startEditing(name) {
466 | var store = this;
467 | store.propertyBeingEdited = Array.isArray(name) ? name[0] : name;
468 | } // to be called on field blur, any field name parameter is ignored
469 |
470 | }, {
471 | key: "stopEditing",
472 | value: function stopEditing() {
473 | var store = this;
474 | store.propertyBeingEdited = null;
475 |
476 | if (store.status.hasChanges) {
477 | // This will trigger autorun in case it already ran while we were editing:
478 | (0, _mobx.action)(function () {
479 | // In MobX 4+, ObservableMap.keys() returns an Iterable, not an array
480 | var key = Array.from(store.dataChanges)[0][0];
481 | var value = store.dataChanges.get(key);
482 | store.dataChanges["delete"](key);
483 | store.dataChanges.set(key, value);
484 | })();
485 | }
486 | }
487 | /**
488 | * Returns the value of a field/property, optionally returning the last saved value for not validated/in progress fields
489 | * Without validated:true, using this function is not necessary, can just access store.data[name].
490 | * @param {String|Array} name - field/property name (Array format supports json schema forms)
491 | * @param {Boolean} [validated] - only return validated value, i.e. if it's in error, fallback to dataServer
492 | * @param {Boolean} [skipPropertyBeingEdited] - used only when validated is true to again fallback to dataServer
493 | * @returns {*}
494 | */
495 |
496 | }, {
497 | key: "getValue",
498 | value: function getValue(name, validated, skipPropertyBeingEdited) {
499 | var store = this;
500 | var prop = Array.isArray(name) ? name[0] : name;
501 |
502 | if (validated) {
503 | // check if property is being edited or invalid
504 | if (skipPropertyBeingEdited && prop === store.propertyBeingEdited || store.dataErrors[prop]) {
505 | return store.dataServer[prop];
506 | }
507 | }
508 |
509 | return store.data[prop];
510 | } // Returns the last saved (or server-provided) set of data
511 | // - in an afterSave callback it already includes merged updates that were not in error
512 |
513 | }, {
514 | key: "getSavedData",
515 | value: function getSavedData() {
516 | var store = this;
517 | return store.dataServer;
518 | }
519 | /**
520 | * @returns {{errors: Array, isReady: Boolean, isInProgress: Boolean, canSave: Boolean, hasChanges: Boolean, isReadOnly: Boolean}}
521 | * errors is an array of any serverError plus all the error messages from all fields (in no particular order)
522 | * (serverError is either the string returned in response.error or a communication error and is cleared on every refresh and save)
523 | * isReady indicates initial data load (refresh) has been completed and user can start entering data
524 | * isInProgress indicates either a refresh or a save is in progress
525 | * canSave is true when no refresh or save is in progress and there are no validation errors
526 | * hasChanges is true when one or more data properties has a value that's different from last-saved/server-loaded data.
527 | * isReadOnly by default is true when isReady is false but can be set to the return value of an
528 | * optional callback to which this status object (without isReadOnly) is passed
529 | */
530 |
531 | }, {
532 | key: "status",
533 | get: function get() {
534 | var store = this;
535 | var errors = [];
536 |
537 | if (store.serverError) {
538 | errors = [store.serverError];
539 | }
540 |
541 | Object.keys(store.dataErrors).forEach(function (key) {
542 | if (store.dataErrors[key]) {
543 | errors.push(store.dataErrors[key]);
544 | }
545 | });
546 | var status = {
547 | errors: errors,
548 | isReady: store.isReady,
549 | isInProgress: store.isLoading || store.isSaving,
550 | canSave: !store.isLoading && !store.isSaving && (store.serverError ? errors.length === 1 : errors.length === 0),
551 | hasChanges: !!store.dataChanges.size,
552 | mustCreate: !!(store.options.idProperty && !store.dataServer[store.options.idProperty])
553 | };
554 |
555 | if (typeof store.options.isReadOnly === 'function') {
556 | status.isReadOnly = store.options.isReadOnly(status);
557 | } else {
558 | status.isReadOnly = store.options.isReadOnly;
559 | }
560 |
561 | return status;
562 | }
563 | /**
564 | * Copies dataServer into data and resets the error observable and lastSync.
565 | * Mostly for internal use by constructor and refresh().
566 | * @param {Object} [data] If provided, dataServer will be set to it and store.isReady will be set to true
567 | */
568 |
569 | }, {
570 | key: "reset",
571 | value: function reset(data) {
572 | var store = this;
573 | (0, _mobx.action)(function () {
574 | if (data) {
575 | store.dataServer = data;
576 | }
577 |
578 | var deep = true;
579 | store.data = (0, _justExtend["default"])(deep, {}, store.dataServer); // setup error observable
580 |
581 | var temp = {};
582 | Object.keys(store.data).forEach(function (key) {
583 | temp[key] = null;
584 | });
585 | store.dataErrors = temp;
586 | store.lastSync = null;
587 | observeComputedProperties(store);
588 | if (data && !store.isReady) store.isReady = true;
589 | })();
590 | }
591 | /**
592 | * Loads data from server unless a refresh was performed within the last minRefreshInterval (i.e. 15 minutes).
593 | * If there are pending (and ready to save) changes, triggers save instead and 'resets the clock' on minRefreshInterval.
594 | * For a store with idProperty defined, if that data property is falsy in data received from server,
595 | * loads from server only the very first time refresh() is called unless called with allowIfMustCreate=true option.
596 | * @param {Object} [refreshOptions]
597 | * @param {Boolean} [refreshOptions.allowIfMustCreate=false]
598 | * @param {Boolean} [refreshOptions.ignoreMinRefreshInterval=false]
599 | * @returns {Promise|Boolean} resolves to true if refresh actually performed, false if skipped
600 | */
601 |
602 | }, {
603 | key: "refresh",
604 | value: function () {
605 | var _refresh = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
606 | var refreshOptions,
607 | allowIfMustCreate,
608 | ignoreMinRefreshInterval,
609 | store,
610 | now,
611 | past,
612 | result,
613 | _args = arguments;
614 | return regeneratorRuntime.wrap(function _callee$(_context) {
615 | while (1) {
616 | switch (_context.prev = _context.next) {
617 | case 0:
618 | refreshOptions = _args.length > 0 && _args[0] !== undefined ? _args[0] : {};
619 | // for some reason this syntax is erroring in tests:
620 | // const { allowIfMustCreate = false, ignoreMinRefreshInterval = false } = refreshOptions;
621 | allowIfMustCreate = refreshOptions.allowIfMustCreate || false;
622 | ignoreMinRefreshInterval = refreshOptions.ignoreMinRefreshInterval || false;
623 | store = this;
624 |
625 | if (!(!store.options.server.get || store.isReady && store.status.mustCreate && !allowIfMustCreate)) {
626 | _context.next = 6;
627 | break;
628 | }
629 |
630 | return _context.abrupt("return", false);
631 |
632 | case 6:
633 | store.options.log("[".concat(store.options.name, "] Starting data refresh..."));
634 |
635 | if (!store.isLoading) {
636 | _context.next = 10;
637 | break;
638 | }
639 |
640 | store.options.log("[".concat(store.options.name, "] Data is already being refreshed."));
641 | return _context.abrupt("return", false);
642 |
643 | case 10:
644 | now = new Date();
645 | past = new Date(Date.now() - store.options.minRefreshInterval); // check if lastSync is between now and 15 minutes ago
646 |
647 | if (!(!ignoreMinRefreshInterval && past < store.lastSync && store.lastSync <= now)) {
648 | _context.next = 15;
649 | break;
650 | }
651 |
652 | store.options.log("[".concat(store.options.name, "] Data refreshed within last ").concat(store.options.minRefreshInterval / 1000, " seconds."));
653 | return _context.abrupt("return", false);
654 |
655 | case 15:
656 | if (!(store.status.hasChanges && !store.status.mustCreate)) {
657 | _context.next = 23;
658 | break;
659 | }
660 |
661 | store.options.log("[".concat(store.options.name, "] Unsaved changes detected..."));
662 | _context.next = 19;
663 | return store.save();
664 |
665 | case 19:
666 | if (!_context.sent) {
667 | _context.next = 23;
668 | break;
669 | }
670 |
671 | store.options.log("[".concat(store.options.name, "] Postponing refresh for ").concat(store.options.minRefreshInterval / 1000, " seconds."));
672 | store.lastSync = new Date();
673 | return _context.abrupt("return", false);
674 |
675 | case 23:
676 | if (!(typeof store.options.beforeRefresh === 'function')) {
677 | _context.next = 29;
678 | break;
679 | }
680 |
681 | _context.next = 26;
682 | return store.options.beforeRefresh(store);
683 |
684 | case 26:
685 | _context.t0 = _context.sent;
686 |
687 | if (!(_context.t0 === false)) {
688 | _context.next = 29;
689 | break;
690 | }
691 |
692 | return _context.abrupt("return", false);
693 |
694 | case 29:
695 | store.options.log("[".concat(store.options.name, "] Refreshing data..."));
696 | store.isLoading = true;
697 | _context.prev = 31;
698 | _context.next = 34;
699 | return store.options.server.get();
700 |
701 | case 34:
702 | result = _context.sent;
703 | store.options.log("[".concat(store.options.name, "] Data received from server."));
704 | (0, _mobx.action)(function () {
705 | store.dataServer = result;
706 | store.serverError = null;
707 | store.reset();
708 | store.lastSync = new Date();
709 | })();
710 |
711 | if (!(typeof store.options.afterRefresh === 'function')) {
712 | _context.next = 41;
713 | break;
714 | }
715 |
716 | _context.next = 40;
717 | return store.options.afterRefresh(store);
718 |
719 | case 40:
720 | observeComputedProperties(store); // again, in case afterRefresh added some
721 |
722 | case 41:
723 | store.options.log("[".concat(store.options.name, "] Refresh finished."));
724 | if (!store.isReady) store.isReady = true;
725 | _context.next = 48;
726 | break;
727 |
728 | case 45:
729 | _context.prev = 45;
730 | _context.t1 = _context["catch"](31);
731 | handleError(store, _context.t1);
732 |
733 | case 48:
734 | store.isLoading = false;
735 | return _context.abrupt("return", true);
736 |
737 | case 50:
738 | case "end":
739 | return _context.stop();
740 | }
741 | }
742 | }, _callee, this, [[31, 45]]);
743 | }));
744 |
745 | function refresh() {
746 | return _refresh.apply(this, arguments);
747 | }
748 |
749 | return refresh;
750 | }()
751 | /**
752 | * Sends ready-to-save data changes to the server (normally using server.set unless it's undefined, then with server.create)
753 | * For a store with idProperty defined when that property is falsy in the data received from server
754 | * and allowCreate=true, uses server.create instead.
755 | * Calls to save() while one is in progress are queued.
756 | * @param {Object} saveOptions - the object as a whole is also passed to the beforeSave callback
757 | * @param {Boolean} [saveOptions.allowCreate=false] - for a store with idProperty defined, this must be true
758 | * for the save to actually be performed when that property is falsy.
759 | * @param {Boolean} [saveOptions.saveAll=false] - normally save only sends changes and if no changes, no save is done.
760 | * if saveAll=true, sends the full data object regardless of changes.
761 | * @param {Boolean} [saveOptions.skipPropertyBeingEdited=false] - true in an auto-save
762 | * @param {Boolean} [saveOptions.keepServerError=false] - true in an auto-save, otherwise will also deactivate saveNotification prior to save
763 | * @returns {Promise|Boolean} resolves to true if save actually performed, false if skipped
764 | */
765 |
766 | }, {
767 | key: "save",
768 | value: function save() {
769 | var saveOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
770 | var _saveOptions$allowCre = saveOptions.allowCreate,
771 | allowCreate = _saveOptions$allowCre === void 0 ? false : _saveOptions$allowCre,
772 | _saveOptions$saveAll = saveOptions.saveAll,
773 | saveAll = _saveOptions$saveAll === void 0 ? false : _saveOptions$saveAll,
774 | _saveOptions$skipProp = saveOptions.skipPropertyBeingEdited,
775 | skipPropertyBeingEdited = _saveOptions$skipProp === void 0 ? false : _saveOptions$skipProp,
776 | _saveOptions$keepServ = saveOptions.keepServerError,
777 | keepServerError = _saveOptions$keepServ === void 0 ? false : _saveOptions$keepServ;
778 | var store = this;
779 | store.saveQueue = store.saveQueue.then( /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() {
780 | var deep, updates, response;
781 | return regeneratorRuntime.wrap(function _callee2$(_context2) {
782 | while (1) {
783 | switch (_context2.prev = _context2.next) {
784 | case 0:
785 | if (!(store.status.mustCreate && !allowCreate)) {
786 | _context2.next = 2;
787 | break;
788 | }
789 |
790 | return _context2.abrupt("return", false);
791 |
792 | case 2:
793 | store.options.log("[".concat(store.options.name, "] Starting data save..."));
794 | deep = true;
795 |
796 | if (!saveAll) {
797 | _context2.next = 10;
798 | break;
799 | }
800 |
801 | updates = {};
802 | Object.getOwnPropertyNames(store.data).forEach(function (property) {
803 | if (property[0] === '$') {
804 | return;
805 | }
806 |
807 | if ((0, _mobx.isObservableArray)(store.data[property])) {
808 | updates[property] = store.data[property].slice();
809 | return;
810 | }
811 |
812 | updates[property] = store.data[property];
813 | });
814 | updates = (0, _justExtend["default"])(deep, {}, updates);
815 | _context2.next = 18;
816 | break;
817 |
818 | case 10:
819 | // Mobx 4+ toJS() exports a Map, not an Object and toJSON is the 'legacy' method to export an Object
820 | updates = store.dataChanges.toJSON ? store.dataChanges.toJSON() : store.dataChanges.toJS();
821 |
822 | if (!(Object.keys(updates).length === 0)) {
823 | _context2.next = 14;
824 | break;
825 | }
826 |
827 | store.options.log("[".concat(store.options.name, "] No changes to save."));
828 | return _context2.abrupt("return", false);
829 |
830 | case 14:
831 | // check if we have property currently being edited in changes
832 | // or if a property has an error and clone (observable or regular)
833 | // Arrays and Objects to plain ones
834 | Object.keys(updates).forEach(function (property) {
835 | if (skipPropertyBeingEdited && property === store.propertyBeingEdited) {
836 | store.options.log("[".concat(store.options.name, "] Property \"").concat(property, "\" is being edited."));
837 | delete updates[property];
838 | return;
839 | }
840 |
841 | if (store.dataErrors[property]) {
842 | store.options.log("[".concat(store.options.name, "] Property \"").concat(property, "\" is not validated."));
843 | delete updates[property];
844 | return;
845 | }
846 |
847 | if (store.isSame(updates[property], store.dataServer[property])) {
848 | store.options.log("[".concat(store.options.name, "] Property \"").concat(property, "\" is same as on the server."));
849 | delete updates[property];
850 | store.dataChanges["delete"](property);
851 | return;
852 | }
853 |
854 | if (Array.isArray(updates[property]) || (0, _mobx.isObservableArray)(updates[property])) {
855 | updates[property] = updates[property].slice();
856 | } else if (isObject(updates[property])) {
857 | updates[property] = (0, _justExtend["default"])(deep, {}, updates[property]);
858 | }
859 | });
860 |
861 | if (!(Object.keys(updates).length === 0)) {
862 | _context2.next = 18;
863 | break;
864 | }
865 |
866 | store.options.log("[".concat(store.options.name, "] No changes ready to save."));
867 | return _context2.abrupt("return", false);
868 |
869 | case 18:
870 | if (!(typeof store.options.beforeSave === 'function')) {
871 | _context2.next = 24;
872 | break;
873 | }
874 |
875 | _context2.next = 21;
876 | return store.options.beforeSave(store, updates, saveOptions);
877 |
878 | case 21:
879 | _context2.t0 = _context2.sent;
880 |
881 | if (!(_context2.t0 === false)) {
882 | _context2.next = 24;
883 | break;
884 | }
885 |
886 | return _context2.abrupt("return", false);
887 |
888 | case 24:
889 | store.options.log("[".concat(store.options.name, "] Saving data..."));
890 | store.options.log(updates);
891 | store.isSaving = true;
892 | _context2.prev = 27;
893 |
894 | if (!keepServerError) {
895 | store.saveNotification.active = false;
896 | store.serverError = null;
897 | }
898 |
899 | if (!(store.options.server.set && (!store.options.server.create || !store.status.mustCreate))) {
900 | _context2.next = 35;
901 | break;
902 | }
903 |
904 | _context2.next = 32;
905 | return store.options.server.set(updates);
906 |
907 | case 32:
908 | response = _context2.sent;
909 | _context2.next = 38;
910 | break;
911 |
912 | case 35:
913 | _context2.next = 37;
914 | return store.options.server.create(updates);
915 |
916 | case 37:
917 | response = _context2.sent;
918 |
919 | case 38:
920 | _context2.next = 40;
921 | return processSaveResponse(store, updates, response);
922 |
923 | case 40:
924 | store.saveNotification.status = _context2.sent;
925 | store.saveNotification.active = true;
926 | store.options.log("[".concat(store.options.name, "] Save finished."));
927 | _context2.next = 49;
928 | break;
929 |
930 | case 45:
931 | _context2.prev = 45;
932 | _context2.t1 = _context2["catch"](27);
933 | handleError(store, _context2.t1);
934 |
935 | if (store.options.saveNotificationStatusOnError) {
936 | store.saveNotification.status = store.options.saveNotificationStatusOnError;
937 | store.saveNotification.active = true;
938 | }
939 |
940 | case 49:
941 | store.isSaving = false;
942 | return _context2.abrupt("return", true);
943 |
944 | case 51:
945 | case "end":
946 | return _context2.stop();
947 | }
948 | }
949 | }, _callee2, null, [[27, 45]]);
950 | })));
951 | return store.saveQueue;
952 | }
953 | }]);
954 |
955 | return FormStore;
956 | }(), (_descriptor = _applyDecoratedDescriptor(_class.prototype, "isReady", [_mobx.observable], {
957 | configurable: true,
958 | enumerable: true,
959 | writable: true,
960 | initializer: function initializer() {
961 | return false;
962 | }
963 | }), _descriptor2 = _applyDecoratedDescriptor(_class.prototype, "isLoading", [_mobx.observable], {
964 | configurable: true,
965 | enumerable: true,
966 | writable: true,
967 | initializer: function initializer() {
968 | return false;
969 | }
970 | }), _descriptor3 = _applyDecoratedDescriptor(_class.prototype, "isSaving", [_mobx.observable], {
971 | configurable: true,
972 | enumerable: true,
973 | writable: true,
974 | initializer: function initializer() {
975 | return false;
976 | }
977 | }), _descriptor4 = _applyDecoratedDescriptor(_class.prototype, "serverError", [_mobx.observable], {
978 | configurable: true,
979 | enumerable: true,
980 | writable: true,
981 | initializer: function initializer() {
982 | return null;
983 | }
984 | }), _descriptor5 = _applyDecoratedDescriptor(_class.prototype, "data", [_mobx.observable], {
985 | configurable: true,
986 | enumerable: true,
987 | writable: true,
988 | initializer: function initializer() {
989 | return {};
990 | }
991 | }), _descriptor6 = _applyDecoratedDescriptor(_class.prototype, "dataErrors", [_mobx.observable], {
992 | configurable: true,
993 | enumerable: true,
994 | writable: true,
995 | initializer: function initializer() {
996 | return {};
997 | }
998 | }), _descriptor7 = _applyDecoratedDescriptor(_class.prototype, "saveNotification", [_mobx.observable], {
999 | configurable: true,
1000 | enumerable: true,
1001 | writable: true,
1002 | initializer: function initializer() {
1003 | return {
1004 | active: false,
1005 | status: null
1006 | };
1007 | }
1008 | }), _descriptor8 = _applyDecoratedDescriptor(_class.prototype, "propertyBeingEdited", [_mobx.observable], {
1009 | configurable: true,
1010 | enumerable: true,
1011 | writable: true,
1012 | initializer: function initializer() {
1013 | return null;
1014 | }
1015 | }), _applyDecoratedDescriptor(_class.prototype, "status", [_mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, "status"), _class.prototype)), _class);
1016 | var _default = FormStore;
1017 | exports["default"] = _default;
--------------------------------------------------------------------------------