@homebound/form-state 2.26.9 → 2.26.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fields/listField.js +91 -11
- package/dist/fields/objectField.d.ts +2 -1
- package/dist/fields/objectField.js +5 -0
- package/dist/formState.test.js +94 -0
- package/dist/useFormState.test.js +40 -2
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +12 -0
- package/package.json +4 -2
package/dist/fields/listField.js
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.newListFieldState = newListFieldState;
|
|
4
7
|
const mobx_1 = require("mobx");
|
|
5
8
|
const objectField_1 = require("./objectField");
|
|
6
9
|
const rules_1 = require("../rules");
|
|
7
10
|
const utils_1 = require("../utils");
|
|
8
|
-
|
|
11
|
+
const object_hash_1 = __importDefault(require("object-hash"));
|
|
12
|
+
function newListFieldState(
|
|
13
|
+
// parentCopy is objectField's deepClone-d `originalCopy` that it/we use for `dirty` checking
|
|
14
|
+
parentCopy,
|
|
15
|
+
// parentInstance is objectField's `instance`, that we're currently mutating
|
|
16
|
+
parentInstance, parentState, key, rules, listConfig, config, strictOrder, maybeAutoSave) {
|
|
9
17
|
var _a;
|
|
10
|
-
// Keep a map of "item in the
|
|
18
|
+
// Keep a map of "item in the parentInstance list" -> "that item's ObjectState"
|
|
11
19
|
const rowMap = new Map();
|
|
20
|
+
const addedRows = new Set();
|
|
12
21
|
const _tick = (0, mobx_1.observable)({ value: 1 });
|
|
13
22
|
const _originalValueTick = (0, mobx_1.observable)({ value: 1 });
|
|
14
23
|
const _childTick = (0, mobx_1.observable)({ value: 1 });
|
|
@@ -165,6 +174,7 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
165
174
|
maybeAutoSave();
|
|
166
175
|
},
|
|
167
176
|
set(values, opts = {}) {
|
|
177
|
+
var _a, _b, _c, _d, _e;
|
|
168
178
|
if (this.readOnly && !opts.resetting && !opts.refreshing) {
|
|
169
179
|
throw new Error(`${String(key)} is currently readOnly`);
|
|
170
180
|
}
|
|
@@ -173,19 +183,89 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
173
183
|
// passing us cloned rows from the parentCopy, but `getOrCreateChildState` will
|
|
174
184
|
// use the `copyMap` to recover the non-cloned rows, to avoid promoting the clone
|
|
175
185
|
// into a real row.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
186
|
+
if (this.dirty && opts.refreshing) {
|
|
187
|
+
// When refreshing a dirty list, we need to preserve WIP values
|
|
188
|
+
// Start with current list, then merge in incoming changes
|
|
189
|
+
//
|
|
190
|
+
// These will all be POJOs, where `currentItems` existing in our `parentInstance`, and
|
|
191
|
+
// `incomingItems` could really be any of:
|
|
192
|
+
// - an instance from `parentInstance[key]` that user code is passing back in
|
|
193
|
+
// - an instance from `parentCopy[key]` that a cache refresh is passing in
|
|
194
|
+
// - a new instance, either from a cache refresh or user code, that we haven't seen yet
|
|
195
|
+
const currentItems = this.value || [];
|
|
196
|
+
const incomingItems = values || [];
|
|
197
|
+
const mergedItems = [];
|
|
198
|
+
// Index by idKey or a hash of the object, so we don't have to n^2 merging
|
|
199
|
+
const idKey = this.rows.length > 0 && this.rows[0].idKey;
|
|
200
|
+
const hashItem = (item) => (idKey && item[idKey]) || (0, object_hash_1.default)(item);
|
|
201
|
+
const hashWithoutId = (item) => (0, object_hash_1.default)(Object.fromEntries(Object.entries(item).filter(([key]) => key !== idKey)));
|
|
202
|
+
const currentById = (0, utils_1.groupBy)(currentItems, hashItem);
|
|
203
|
+
const incomingById = (0, utils_1.groupBy)(incomingItems, hashItem);
|
|
204
|
+
// If our local instance doesn't assign in id, when we get results back from the server, strip
|
|
205
|
+
// their id and then see if we're the same based on data. Note that this is a hueristic, and will
|
|
206
|
+
// fail if the client-side has made additional changes to the child after submitting it to the server,
|
|
207
|
+
// or if the server acks back more/different data than the client sent.
|
|
208
|
+
//
|
|
209
|
+
// Only do this if one of our client-side objects is missing an id
|
|
210
|
+
const incomingByHash = idKey && currentItems.some((item) => !item[idKey]) && (0, utils_1.groupBy)(incomingItems, hashWithoutId);
|
|
211
|
+
// Watch for deletions
|
|
212
|
+
const hasOpKey = Object.keys(listConfig.config).includes("op");
|
|
213
|
+
// First, process all current items
|
|
214
|
+
for (const currentItem of currentItems) {
|
|
215
|
+
const childState = rowMap.get(currentItem); // We'll always have a child state for `currentItems`
|
|
216
|
+
// Try to find a matching item in the incoming data
|
|
217
|
+
const hash = hashItem(currentItem);
|
|
218
|
+
const match = ((_b = (_a = incomingById.get(hash)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : (!!incomingByHash && ((_c = incomingByHash === null || incomingByHash === void 0 ? void 0 : incomingByHash.get(hash)) === null || _c === void 0 ? void 0 : _c[0])));
|
|
219
|
+
if (match) {
|
|
220
|
+
// Defer to set to handle not nuking WIP changes
|
|
221
|
+
childState.set(match, opts);
|
|
222
|
+
mergedItems.push(childState.value);
|
|
223
|
+
// Once matched, we don't this to be an addedRow anymore
|
|
224
|
+
addedRows.delete(currentItem);
|
|
225
|
+
}
|
|
226
|
+
else if (childState.dirty || addedRows.has(currentItem)) {
|
|
227
|
+
// If no incoming, but we're dirty (...and not deleted), keep the WIP change
|
|
228
|
+
if (hasOpKey && currentItem.op === "delete") {
|
|
229
|
+
// delete
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
mergedItems.push(currentItem);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// Local is not dirty, and it's not upstream, so remove it
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Then, add any incoming items that don't exist in current
|
|
240
|
+
for (const incomingItem of incomingItems) {
|
|
241
|
+
// Look for both `incoming.id` and the incoming-sans-id for new entities
|
|
242
|
+
const match = ((_d = currentById.get(hashItem(incomingItem))) === null || _d === void 0 ? void 0 : _d[0]) || ((_e = currentById.get(hashWithoutId(incomingItem))) === null || _e === void 0 ? void 0 : _e[0]);
|
|
243
|
+
if (!match) {
|
|
244
|
+
// New item from server, add it
|
|
245
|
+
const childState = getOrCreateChildState(incomingItem, opts);
|
|
246
|
+
mergedItems.push(childState.value);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
parentInstance[key] = mergedItems;
|
|
250
|
+
_tick.value++;
|
|
251
|
+
// Set original to not merged...
|
|
252
|
+
this.setOriginalValue(incomingItems);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
parentInstance[key] = (values !== null && values !== void 0 ? values : []).map((child) => getOrCreateChildState(child, opts).value);
|
|
256
|
+
_tick.value++;
|
|
257
|
+
// Reset originalCopy so that our dirty checks have the right # of rows.
|
|
258
|
+
if (opts.refreshing) {
|
|
259
|
+
this.setOriginalValue();
|
|
260
|
+
}
|
|
182
261
|
}
|
|
183
262
|
},
|
|
184
263
|
add(value, spliceIndex) {
|
|
185
264
|
// This is called by the user, so value should be a non-proxy value we should keep
|
|
186
265
|
const childState = getOrCreateChildState(value);
|
|
187
|
-
// const childState = createObjectState(config, value, { maybeAutoSave }) as ObjectStateInternal<U>;
|
|
188
266
|
rowMap.set(value, childState);
|
|
267
|
+
// Let `.set` know this is a new row
|
|
268
|
+
addedRows.add(value);
|
|
189
269
|
this.ensureSet();
|
|
190
270
|
this.value.splice(typeof spliceIndex === "number" ? spliceIndex : this.value.length, 0, childState.value);
|
|
191
271
|
_tick.value++;
|
|
@@ -223,9 +303,9 @@ function newListFieldState(parentCopy, parentInstance, parentState, key, rules,
|
|
|
223
303
|
return value !== null && value !== void 0 ? value : [];
|
|
224
304
|
},
|
|
225
305
|
// This should only be called when value === originalValue
|
|
226
|
-
setOriginalValue() {
|
|
306
|
+
setOriginalValue(incomingItems) {
|
|
227
307
|
// Use the rows' originalValues to update the parentCopy
|
|
228
|
-
parentCopy[key] = this.rows.map((r) => r.originalValue);
|
|
308
|
+
parentCopy[key] = incomingItems !== null && incomingItems !== void 0 ? incomingItems : this.rows.map((r) => r.originalValue);
|
|
229
309
|
parentCopy[key].forEach((copy, i) => {
|
|
230
310
|
copyMap.set(copy, parentInstance[key][i]);
|
|
231
311
|
});
|
|
@@ -34,9 +34,10 @@ export type ObjectState<T> = FieldStates<T> & FieldState<T> & {
|
|
|
34
34
|
/** Returns whether the object can be saved, i.e. is valid, but also as a side-effect marks touched. */
|
|
35
35
|
canSave(): boolean;
|
|
36
36
|
};
|
|
37
|
-
export type ObjectStateInternal<T> = ObjectState<T> & {
|
|
37
|
+
export type ObjectStateInternal<T = unknown> = ObjectState<T> & {
|
|
38
38
|
set(value: T, opts?: InternalSetOpts): void;
|
|
39
39
|
isSameEntity(other: T): boolean;
|
|
40
|
+
idKey: string | undefined;
|
|
40
41
|
};
|
|
41
42
|
declare const fragmentSym: unique symbol;
|
|
42
43
|
export type Fragment<V> = V & {
|
|
@@ -49,6 +49,7 @@ function newObjectState(config, parentState, parentInstance, parentListState, in
|
|
|
49
49
|
field = (0, valueField_1.newValueFieldState)(originalCopy, instance, getObjectState, key, config.rules || [], config.isIdKey ||
|
|
50
50
|
// Default the id key to "id" unless some other field has isIdKey set
|
|
51
51
|
(key === "id" &&
|
|
52
|
+
!("isIdKey" in config) && // Allow setting `isIdKey=false`
|
|
52
53
|
!Object.entries(objectConfig).some(([other, c]) => other !== key && c.isIdKey)), config.isDeleteKey || false, config.isReadOnlyKey || false, (_a = config.computed) !== null && _a !== void 0 ? _a :
|
|
53
54
|
// If instance is a mobx class, we can detect computeds as they won't be enumerable
|
|
54
55
|
((0, mobx_1.isObservable)(instance) ? !(key in originalCopy) : false), (_b = config.readOnly) !== null && _b !== void 0 ? _b : false, (_c = config.strictOrder) !== null && _c !== void 0 ? _c : true, maybeAutoSave);
|
|
@@ -250,6 +251,10 @@ function newObjectState(config, parentState, parentInstance, parentListState, in
|
|
|
250
251
|
isUnset() {
|
|
251
252
|
return !!parentInstance && parentInstance[key] === undefined;
|
|
252
253
|
},
|
|
254
|
+
get idKey() {
|
|
255
|
+
var _a;
|
|
256
|
+
return (_a = getFields(this).find((f) => f._isIdKey)) === null || _a === void 0 ? void 0 : _a.key;
|
|
257
|
+
},
|
|
253
258
|
};
|
|
254
259
|
proxy = (0, mobx_1.makeAutoObservable)(obj, {
|
|
255
260
|
// Use custom equality on `value` that is _never_ equals. This sounds weird, but
|
package/dist/formState.test.js
CHANGED
|
@@ -165,6 +165,99 @@ describe("formState", () => {
|
|
|
165
165
|
expect(state.books.dirty).toBe(false);
|
|
166
166
|
expect(state.books.originalValue).toEqual([]);
|
|
167
167
|
});
|
|
168
|
+
it("list value can refresh and not drop new children w/id", () => {
|
|
169
|
+
// Given a list field that is currently []
|
|
170
|
+
const a1 = { firstName: "a1", books: [] };
|
|
171
|
+
const state = createAuthorInputState(a1);
|
|
172
|
+
// And we add a new child on the client-side
|
|
173
|
+
state.books.add({ id: "b:1", title: "b1" });
|
|
174
|
+
// When we refresh it with the original list undefined
|
|
175
|
+
state.set({ books: [] }, { refreshing: true });
|
|
176
|
+
// Then it keeps the new child (and it somehow already has the id set)
|
|
177
|
+
expect(state.books.value).toEqual([{ id: "b:1", title: "b1" }]);
|
|
178
|
+
// And it's still considered changed
|
|
179
|
+
expect(state.books.dirty).toBe(true);
|
|
180
|
+
expect(state.books.originalValue).toEqual([]);
|
|
181
|
+
// And later when we refresh with the value acked from the server
|
|
182
|
+
state.set({ books: [{ id: "b:1", title: "b1", isPublished: true }] }, { refreshing: true });
|
|
183
|
+
// Then we have only 1 value
|
|
184
|
+
expect(state.books.value).toEqual([{ id: "b:1", title: "b1", isPublished: true }]);
|
|
185
|
+
// And we're not longer dirty
|
|
186
|
+
expect(state.books.dirty).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
it("list value can refresh and not drop new children w/o an id", () => {
|
|
189
|
+
// Given a list field that is currently []
|
|
190
|
+
const a1 = { firstName: "a1", books: [] };
|
|
191
|
+
const state = createAuthorInputState(a1);
|
|
192
|
+
// And we add a new child on the client-side (and it doesn't have an id)
|
|
193
|
+
state.books.add({ title: "b1" });
|
|
194
|
+
// When we refresh it with the original list undefined
|
|
195
|
+
state.set({ books: [] }, { refreshing: true });
|
|
196
|
+
// Then it keeps the new child
|
|
197
|
+
expect(state.books.value).toEqual([{ title: "b1" }]);
|
|
198
|
+
// And it's still considered changed
|
|
199
|
+
expect(state.books.dirty).toBe(true);
|
|
200
|
+
expect(state.books.originalValue).toEqual([]);
|
|
201
|
+
// And later when we refresh with the value acked from the server
|
|
202
|
+
state.set({ books: [{ id: "b:1", title: "b1" }] }, { refreshing: true });
|
|
203
|
+
// Then we have only 1 value
|
|
204
|
+
expect(state.books.value).toEqual([{ id: "b:1", title: "b1" }]);
|
|
205
|
+
// And we're not longer dirty
|
|
206
|
+
expect(state.books.dirty).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
it("list value can refresh and not drop new children that are just data", () => {
|
|
209
|
+
const config = configBuilders_1.f.config({
|
|
210
|
+
firstName: configBuilders_1.f.value(),
|
|
211
|
+
tags: configBuilders_1.f.list({ combo: configBuilders_1.f.value() }),
|
|
212
|
+
});
|
|
213
|
+
const state = (0, objectField_1.createObjectState)(config, { firstName: "a1", tags: [] });
|
|
214
|
+
// And we add a new child on the client-side (and it doesn't have an id)
|
|
215
|
+
state.tags.add({ combo: ["b1", "b2"] });
|
|
216
|
+
// When we refresh it with the original list undefined
|
|
217
|
+
state.set({ tags: [] }, { refreshing: true });
|
|
218
|
+
// Then it keeps the new child
|
|
219
|
+
expect(state.tags.value).toEqual([{ combo: ["b1", "b2"] }]);
|
|
220
|
+
// And it's still considered changed
|
|
221
|
+
expect(state.tags.dirty).toBe(true);
|
|
222
|
+
expect(state.tags.originalValue).toEqual([]);
|
|
223
|
+
// And we add an additional child
|
|
224
|
+
state.tags.add({ combo: ["c1", "c2"] });
|
|
225
|
+
// And a refresh brings back the 1st child acked from the server
|
|
226
|
+
state.set({ tags: [{ combo: ["b1", "b2"] }] }, { refreshing: true });
|
|
227
|
+
// Then we still have 2 children
|
|
228
|
+
expect(state.tags.value).toEqual([{ combo: ["b1", "b2"] }, { combo: ["c1", "c2"] }]);
|
|
229
|
+
// And we're still dirty
|
|
230
|
+
expect(state.tags.dirty).toBe(true);
|
|
231
|
+
// When the server finally acks both children
|
|
232
|
+
state.set({ tags: [{ combo: ["b1", "b2"] }, { combo: ["c1", "c2"] }] }, { refreshing: true });
|
|
233
|
+
// Then we're finally clean
|
|
234
|
+
expect(state.tags.dirty).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
it("list value can refresh and drop children that were deleted", () => {
|
|
237
|
+
// Given a list field that is currently populated
|
|
238
|
+
const a1 = {
|
|
239
|
+
id: "a:1",
|
|
240
|
+
firstName: "a1",
|
|
241
|
+
books: [
|
|
242
|
+
{ id: "b:1", title: "b1", op: "include" },
|
|
243
|
+
{ id: "b:2", title: "b2", op: "include" },
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
// And incremental deletion is enabled
|
|
247
|
+
const config = configBuilders_1.f.config({
|
|
248
|
+
id: configBuilders_1.f.value(),
|
|
249
|
+
books: configBuilders_1.f.list({ id: configBuilders_1.f.value(), title: configBuilders_1.f.value(), op: configBuilders_1.f.value() }),
|
|
250
|
+
});
|
|
251
|
+
const state = (0, objectField_1.createObjectState)(config, a1);
|
|
252
|
+
// When we delete the 1st book
|
|
253
|
+
state.books.rows[0].op.set("delete");
|
|
254
|
+
// When we refresh it only the 2nd book
|
|
255
|
+
state.set({ books: [{ id: "b:2", title: "b2" }] }, { refreshing: true });
|
|
256
|
+
// Then we only have 1 book
|
|
257
|
+
expect(state.books.value).toEqual([{ id: "b:2", title: "b2", op: "include" }]);
|
|
258
|
+
// And we're not dirty
|
|
259
|
+
expect(state.books.dirty).toBe(false);
|
|
260
|
+
});
|
|
168
261
|
it("list value can observe changes", () => {
|
|
169
262
|
const b1 = { title: "t1" };
|
|
170
263
|
const a1 = { firstName: "a1", books: [b1] };
|
|
@@ -1743,6 +1836,7 @@ const authorWithBooksConfig = configBuilders_1.f.config({
|
|
|
1743
1836
|
id: configBuilders_1.f.value(),
|
|
1744
1837
|
title: configBuilders_1.f.value().req(),
|
|
1745
1838
|
classification: configBuilders_1.f.value(),
|
|
1839
|
+
isPublished: configBuilders_1.f.value(),
|
|
1746
1840
|
}),
|
|
1747
1841
|
});
|
|
1748
1842
|
function createAuthorInputState(input, maybeAutoSave) {
|
|
@@ -218,7 +218,7 @@ describe("useFormState", () => {
|
|
|
218
218
|
id: "a:1",
|
|
219
219
|
});
|
|
220
220
|
});
|
|
221
|
-
it("
|
|
221
|
+
it("can accept new data while read only", async () => {
|
|
222
222
|
// Given a component
|
|
223
223
|
function TestComponent() {
|
|
224
224
|
const config = authorWithAddressAndBooksConfig;
|
|
@@ -255,7 +255,7 @@ describe("useFormState", () => {
|
|
|
255
255
|
expect(r.street.textContent).toEqual("s2");
|
|
256
256
|
expect(r.title1.textContent).toEqual("a2");
|
|
257
257
|
});
|
|
258
|
-
it("
|
|
258
|
+
it("can accept new data with computed fields", async () => {
|
|
259
259
|
// Given a component
|
|
260
260
|
// And it's using a class/mobx proxy as the basis for the data
|
|
261
261
|
class AuthorRow {
|
|
@@ -356,6 +356,44 @@ describe("useFormState", () => {
|
|
|
356
356
|
// Then we auto save again
|
|
357
357
|
expect(autoSave).toBeCalledTimes(2);
|
|
358
358
|
});
|
|
359
|
+
it("can revert to last-saved-state after a failed auto save", async () => {
|
|
360
|
+
// Given a component
|
|
361
|
+
function TestComponent() {
|
|
362
|
+
// And the firstName is initially "f1"
|
|
363
|
+
const [data, setData] = (0, react_2.useState)({ firstName: "f1" });
|
|
364
|
+
const query = (0, react_2.useMemo)(() => {
|
|
365
|
+
return { data, loading: false, error: undefined };
|
|
366
|
+
}, [data]);
|
|
367
|
+
const form = (0, useFormState_1.useFormState)({
|
|
368
|
+
config: authorConfig,
|
|
369
|
+
init: { query, map: (d) => d },
|
|
370
|
+
async autoSave(fs) {
|
|
371
|
+
// If the user types "f2", we'll pretend the backend accepted the update, and acks it in the mutation response
|
|
372
|
+
if (fs.firstName.value === "f2") {
|
|
373
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
374
|
+
setData(fs.value);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// If the user types anything else, pretend the backend rejected it, and revert
|
|
378
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
379
|
+
fs.revertChanges();
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
return ((0, jsx_runtime_1.jsx)(mobx_react_1.Observer, { children: () => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { "data-testid": "firstNameOriginal", children: form.firstName.originalValue }), (0, jsx_runtime_1.jsx)(FormStateApp_1.TextField, { field: form.firstName }), ";"] })) }));
|
|
384
|
+
}
|
|
385
|
+
// So we start out with f1
|
|
386
|
+
const r = await (0, rtl_utils_1.render)((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
387
|
+
expect(r.firstName).toHaveValue("f1");
|
|
388
|
+
// When we type f2, it should be accepted & stay
|
|
389
|
+
await (0, rtl_utils_1.typeAndWait)(r.firstName, "f2");
|
|
390
|
+
expect(r.firstName).toHaveValue("f2");
|
|
391
|
+
expect(r.firstNameOriginal).toHaveTextContent("f2");
|
|
392
|
+
// But if we type f3, we want it reverted, back to f2 (the last good value)
|
|
393
|
+
await (0, rtl_utils_1.typeAndWait)(r.firstName, "f3");
|
|
394
|
+
expect(r.firstName).toHaveValue("f2");
|
|
395
|
+
expect(r.firstNameOriginal).toHaveTextContent("f2");
|
|
396
|
+
});
|
|
359
397
|
it("returns empty lists even when inputs are undefined", async () => {
|
|
360
398
|
const autoSave = jest.fn();
|
|
361
399
|
// Given a component
|
package/dist/utils.d.ts
CHANGED
|
@@ -46,3 +46,4 @@ export declare function hasToJSON(o?: unknown): o is {
|
|
|
46
46
|
};
|
|
47
47
|
/** Make a clone of `obj`, but only recurse into POJOs and Arrays...and stores. */
|
|
48
48
|
export declare function deepClone<T>(obj: T, map?: WeakMap<WeakKey, any>): T;
|
|
49
|
+
export declare function groupBy<T, Y = T, K = string>(list: readonly T[], fn: (x: T) => K, valueFn?: (x: T) => Y): Map<K, Y[]>;
|
package/dist/utils.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.isEmpty = isEmpty;
|
|
|
11
11
|
exports.areEqual = areEqual;
|
|
12
12
|
exports.hasToJSON = hasToJSON;
|
|
13
13
|
exports.deepClone = deepClone;
|
|
14
|
+
exports.groupBy = groupBy;
|
|
14
15
|
const is_plain_object_1 = require("is-plain-object");
|
|
15
16
|
const mobx_1 = require("mobx");
|
|
16
17
|
const deepEquals_1 = require("./fields/deepEquals");
|
|
@@ -144,6 +145,17 @@ function deepClone(obj, map = new WeakMap()) {
|
|
|
144
145
|
return obj;
|
|
145
146
|
}
|
|
146
147
|
}
|
|
148
|
+
function groupBy(list, fn, valueFn) {
|
|
149
|
+
const result = new Map();
|
|
150
|
+
list.forEach((o) => {
|
|
151
|
+
const group = fn(o);
|
|
152
|
+
if (!result.has(group)) {
|
|
153
|
+
result.set(group, []);
|
|
154
|
+
}
|
|
155
|
+
result.get(group).push(valueFn === undefined ? o : valueFn(o));
|
|
156
|
+
});
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
147
159
|
/** Returns all property names, including mobx computeds (non-enumerable) & inherited. */
|
|
148
160
|
function getAllPropertyNames(obj) {
|
|
149
161
|
const proto = Object.getPrototypeOf(obj);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homebound/form-state",
|
|
3
|
-
"version": "2.26.
|
|
3
|
+
"version": "2.26.11",
|
|
4
4
|
"author": "Homebound",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
"src/**/*.{ts,tsx,css,md}": "prettier --write"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"
|
|
33
|
+
"@types/object-hash": "^3.0.6",
|
|
34
|
+
"is-plain-object": "^5.0.0",
|
|
35
|
+
"object-hash": "^3.0.0"
|
|
34
36
|
},
|
|
35
37
|
"peerDependencies": {
|
|
36
38
|
"mobx": ">=6",
|