Skip to content

Commit faf8ab9

Browse files
committed
Fix live draft key-path updates
Fix #939
1 parent 4287338 commit faf8ab9

8 files changed

Lines changed: 116 additions & 11 deletions

File tree

src/lib/components/contents/details/fields/object/object-editor.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,10 @@
166166
forEachTargetLocale(
167167
{ valueStore: $entryDraft?.[valueStoreKey], locale, i18n },
168168
(_valueMap, _locale) => {
169-
// Assign `null` before deleting each property, so the draft proxy can revalidate the field
170-
getKeysByPrefix(_valueMap, `${keyPath}.`).forEach((_keyPath) => {
169+
// Assign `null` before deleting each property, so the draft proxy can revalidate the field.
170+
// The value map is the draft’s live map, which is mutated right below, so its key paths
171+
// have to be read as they are right now
172+
getKeysByPrefix(_valueMap, `${keyPath}.`, { live: true }).forEach((_keyPath) => {
171173
/** @type {EntryDraft} */ ($entryDraft)[valueStoreKey][_locale][_keyPath] = null;
172174
delete $entryDraft?.[valueStoreKey][_locale][_keyPath];
173175
});

src/lib/services/contents/draft/update/list.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ const itemListRegexCache = new Map();
4040

4141
/**
4242
* Traverse the given object by decoding dot-notated key path.
43+
*
44+
* The object is a draft’s live content, not a snapshot of it, and the caller writes the manipulated
45+
* list straight back into it, so the key paths have to be read as they are right now.
4346
* @internal
4447
* @param {Record<string, any>} obj Original object.
4548
* @param {FieldKeyPath} keyPath Dot-notated field name.
@@ -53,7 +56,7 @@ export const getItemList = (obj, keyPath) => {
5356
);
5457

5558
return [
56-
getSubtree(obj, keyPath) ?? [],
59+
getSubtree(obj, keyPath, { live: true }) ?? [],
5760
Object.fromEntries(Object.entries(obj).filter(([k]) => !regex.test(k))),
5861
];
5962
};
@@ -106,12 +109,13 @@ export const updateListField = ({
106109

107110
/**
108111
* Read a multi-value field out of the draft as a plain list. Our internal representation of such a
109-
* field is a flattened object with one numbered key per item, e.g. `images.0`, `images.1`.
112+
* field is a flattened object with one numbered key per item, e.g. `images.0`, `images.1`. Just
113+
* like {@link getItemList}, this reads a draft’s live content, which the caller then mutates.
110114
* @param {Record<string, any>} values Flattened entry content.
111115
* @param {FieldKeyPath} keyPath Dot-notated field name.
112116
* @returns {any[]} Item values in list order.
113117
*/
114-
const getMultiValueList = (values, keyPath) => getSubtree(values, keyPath) ?? [];
118+
const getMultiValueList = (values, keyPath) => getSubtree(values, keyPath, { live: true }) ?? [];
115119

116120
/**
117121
* Move an item of a multi-value field, such as a File or Image field with the `multiple` option

src/lib/services/contents/draft/update/list.test.js

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/services/contents/entry/key-paths.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
* once instead, keyed by the map object itself: the draft hands out a fresh snapshot per update, so
1414
* an index is built at most once per keystroke and shared by every field reading from it, then
1515
* garbage-collected with the snapshot.
16+
*
17+
* An index is therefore only valid while the map it was built from is left alone. A caller reading
18+
* a map it also mutates — a draft’s live content rather than a snapshot of it — has to pass the
19+
* `live` option, or it would keep getting the key paths the map had the first time it was read.
1620
*/
1721

1822
/**
@@ -51,9 +55,16 @@ const getSortedKeys = (valueMap) => {
5155
* the “first” field under a key path.
5256
* @param {FlattenedEntryContent} valueMap Flattened entry content.
5357
* @param {string} prefix Key path prefix, e.g. `authors.0.`.
58+
* @param {object} [options] Options.
59+
* @param {boolean} [options.live] Whether the value map may be mutated after this call, in which
60+
* case the key paths are scanned instead of being read from — and memoized in — the index.
5461
* @returns {FieldKeyPath[]} Matching key paths.
5562
*/
56-
export const getKeysByPrefix = (valueMap, prefix) => {
63+
export const getKeysByPrefix = (valueMap, prefix, { live = false } = {}) => {
64+
if (live) {
65+
return Object.keys(valueMap).filter((key) => key.startsWith(prefix));
66+
}
67+
5768
const sorted = getSortedKeys(valueMap);
5869
let low = 0;
5970
let high = sorted.length;
@@ -98,9 +109,18 @@ const listItemKeyCacheMap = new WeakMap();
98109
* multiple Relation or Select field — so the same index serves all of them.
99110
* @param {FlattenedEntryContent} valueMap Flattened entry content.
100111
* @param {FieldKeyPath} keyPath Key path of the list field.
112+
* @param {object} [options] Options.
113+
* @param {boolean} [options.live] Whether the value map may be mutated after this call, in which
114+
* case the key paths are scanned instead of being read from — and memoized in — the index.
101115
* @returns {FieldKeyPath[]} Item key paths, e.g. `['authors.0', 'authors.1']`.
102116
*/
103-
export const getListItemKeys = (valueMap, keyPath) => {
117+
export const getListItemKeys = (valueMap, keyPath, { live = false } = {}) => {
118+
if (live) {
119+
return Object.keys(valueMap).filter(
120+
(key) => key.match(LIST_ITEM_KEY_REGEX)?.groups?.parent === keyPath,
121+
);
122+
}
123+
104124
let index = listItemKeyCacheMap.get(valueMap);
105125

106126
if (!index) {

src/lib/services/contents/entry/key-paths.test.js

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/services/contents/entry/subtree.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,13 @@ export const getSubtreeEntries = (keyPath, value) => ({
4343
* Assemble the non-primitive value stored under the given key path from its child key paths.
4444
* @param {FlattenedEntryContent} valueMap Flattened entry content.
4545
* @param {FieldKeyPath} keyPath Key path of the field.
46+
* @param {object} [options] Options.
47+
* @param {boolean} [options.live] Whether the value map may be mutated after this call. See
48+
* {@link getKeysByPrefix}.
4649
* @returns {any} Assembled value, or `undefined` if the key path has no children.
4750
*/
48-
export const getSubtree = (valueMap, keyPath) => {
49-
const keys = getKeysByPrefix(valueMap, `${keyPath}.`);
51+
export const getSubtree = (valueMap, keyPath, { live = false } = {}) => {
52+
const keys = getKeysByPrefix(valueMap, `${keyPath}.`, { live });
5053

5154
if (!keys.length) {
5255
return undefined;

src/lib/services/contents/entry/subtree.test.js

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/services/contents/fields/key-value/helpers.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ export const getPairs = ({ entryDraft, valueStoreKey = 'currentValues', keyPath,
2323
const prefix = `${keyPath}.`;
2424

2525
return /** @type {[string, string][]} */ (
26-
getKeysByPrefix(valueMap, prefix).map((key) => [key.slice(prefix.length), valueMap[key]])
26+
// The value map is the draft’s live map, which {@link savePairs} mutates in place, so its key
27+
// paths have to be read as they are right now
28+
getKeysByPrefix(valueMap, prefix, { live: true }).map((key) => [
29+
key.slice(prefix.length),
30+
valueMap[key],
31+
])
2732
);
2833
};
2934

@@ -74,7 +79,9 @@ export const savePairs = ({
7479
// stores no placeholder at its own key path: its keys are arbitrary strings, and
7580
// `unflatten()` would turn numeric ones into an array. `finalizeContent()` rebuilds the
7681
// object from the children instead
77-
getKeysByPrefix(content, `${keyPath}.`).forEach((_keyPath) => {
82+
// The content is the draft’s live map, which is mutated right below, so its key paths
83+
// have to be read as they are right now
84+
getKeysByPrefix(content, `${keyPath}.`, { live: true }).forEach((_keyPath) => {
7885
delete content[_keyPath];
7986
});
8087

0 commit comments

Comments
 (0)