Skip to content

Commit

Permalink
Fix i18n item duplication and expander issues on list widget
Browse files Browse the repository at this point in the history
  • Loading branch information
kyoshino committed Sep 10, 2023
1 parent 18234c6 commit afe0a49
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 54 deletions.
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ words:
- Typecheck
- uncategorized
- unflatten
- unproxify
- weba
- webformat
- xslx
Expand Down
52 changes: 24 additions & 28 deletions src/lib/components/contents/details/widgets/list/list-editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,14 @@
let mounted = false;
let widgetId = '';
let inputValue = '';
let itemExpandedList = [];
onMount(() => {
mounted = true;
widgetId = generateUUID().split('-').pop();
itemExpandedList = items.map(() => !collapsed);
items.forEach((__, index) => {
$entryDraft.viewStates[locale][`${keyPath}.${index}.expanded`] = !collapsed;
});
});
/**
Expand Down Expand Up @@ -116,8 +118,7 @@
/**
* Update the value for the List widget with subfield(s).
* @param {(list: any[]) => void} manipulate A function to manipulate the list, which takes one
* argument of the list itself. The typical usage is `list.splice()`.
* @param {({ valueList, viewList }) => void} manipulate See {@link updateListField}.
*/
const updateComplexList = (manipulate) => {
Object.keys($entryDraft.currentValues).forEach((_locale) => {
Expand All @@ -133,7 +134,7 @@
* @see https://decapcms.org/docs/beta-features/#list-widget-variable-types
*/
const addItem = (subFieldName) => {
updateComplexList((list) => {
updateComplexList(({ valueList, viewList }) => {
const newItem = {};
const subFields = subFieldName
Expand All @@ -153,10 +154,10 @@
newItem[typeKey] = subFieldName;
}
const index = addToTop ? 0 : list.length;
const index = addToTop ? 0 : valueList.length;
list.splice(index, 0, newItem);
itemExpandedList.splice(index, 0, true);
valueList.splice(index, 0, newItem);
viewList.splice(index, 0, { expanded: true });
});
};
Expand All @@ -165,9 +166,9 @@
* @param {number} index Target index.
*/
const deleteItem = (index) => {
updateComplexList((list) => {
list.splice(index, 1);
itemExpandedList.splice(index, 1);
updateComplexList(({ valueList, viewList }) => {
valueList.splice(index, 1);
viewList.splice(index, 1);
});
};
Expand All @@ -176,12 +177,9 @@
* @param {number} index Target index.
*/
const moveUpItem = (index) => {
updateComplexList((list) => {
[list[index], list[index - 1]] = [list[index - 1], list[index]];
[itemExpandedList[index], itemExpandedList[index - 1]] = [
itemExpandedList[index - 1],
itemExpandedList[index],
];
updateComplexList(({ valueList, viewList }) => {
[valueList[index], valueList[index - 1]] = [valueList[index - 1], valueList[index]];
[viewList[index], viewList[index - 1]] = [viewList[index - 1], viewList[index]];
});
};
Expand All @@ -190,12 +188,9 @@
* @param {number} index Target index.
*/
const moveDownItem = (index) => {
updateComplexList((list) => {
[list[index], list[index + 1]] = [list[index + 1], list[index]];
[itemExpandedList[index], itemExpandedList[index + 1]] = [
itemExpandedList[index + 1],
itemExpandedList[index],
];
updateComplexList(({ valueList, viewList }) => {
[valueList[index], valueList[index + 1]] = [valueList[index + 1], valueList[index]];
[viewList[index], viewList[index + 1]] = [viewList[index + 1], viewList[index]];
});
};
Expand Down Expand Up @@ -234,21 +229,22 @@
</div>
<div class="item-list" id="list-{widgetId}-item-list" class:collapsed={!parentExpanded}>
{#each items as item, index}
{@const expanded = !!$entryDraft.viewStates[locale][`${keyPath}.${index}.expanded`]}
<!-- @todo Support drag sorting. -->
<div class="item">
<div class="header">
<div>
<Button
aria-expanded={itemExpandedList[index]}
aria-expanded={expanded}
aria-controls="list-{widgetId}-item-{index}-body"
on:click={() => {
itemExpandedList[index] = !itemExpandedList[index];
$entryDraft.viewStates[locale][`${keyPath}.${index}.expanded`] = !expanded;
}}
>
<Icon
slot="start-icon"
name={itemExpandedList[index] ? 'expand_more' : 'chevron_right'}
label={itemExpandedList[index] ? $_('collapse') : $_('expand')}
name={expanded ? 'expand_more' : 'chevron_right'}
label={expanded ? $_('collapse') : $_('expand')}
/>
{#if types}
<span class="type">
Expand Down Expand Up @@ -287,7 +283,7 @@
</div>
</div>
<div class="item-body" id="list-{widgetId}-item-{index}-body">
{#if itemExpandedList[index]}
{#if expanded}
{@const subFieldName = Array.isArray(types)
? $entryDraft.currentValues[locale][`${keyPath}.${index}.${typeKey}`]
: undefined}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@
/**
* Update the value for the list.
* @param {(list: any[]) => void} manipulate A function to manipulate the list, which takes one
* argument of the list itself. The typical usage is `list.splice()`.
* @param {({ valueList, viewList }) => void} manipulate See {@link updateListField}.
*/
const updateList = (manipulate) => {
Object.keys($entryDraft.currentValues).forEach((_locale) => {
Expand All @@ -59,8 +58,8 @@
* @param {string} value New value.
*/
const addValue = (value) => {
updateList((list) => {
list.push(value);
updateList(({ valueList }) => {
valueList.push(value);
});
};
Expand All @@ -69,8 +68,8 @@
* @param {number} index Target index.
*/
const removeValue = (index) => {
updateList((list) => {
list.splice(index, 1);
updateList(({ valueList }) => {
valueList.splice(index, 1);
});
};
</script>
Expand Down
51 changes: 37 additions & 14 deletions src/lib/services/contents/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,30 +265,49 @@ export const createDraft = (entry, defaultValues) => {
]),
),
validities: Object.fromEntries(allLocales.map((locale) => [locale, {}])),
viewStates: Object.fromEntries(allLocales.map((locale) => [locale, {}])),
});
};

/**
* Update the value in a list field.
* @param {LocaleCode} locale Target locale.
* @param {string} keyPath Dot-connected field name.
* @param {(list: any[]) => void} manipulate A function to manipulate the list, which takes one
* argument of the list itself. The typical usage is `list.splice()`.
* Unproxify & unflatten the given object.
* @param {Proxy | object} obj Original proxy or object.
* @returns {object} Processed object.
*/
export const updateListField = (locale, keyPath, manipulate) => {
const unflattenObj = unflatten(get(entryDraft).currentValues[locale]);
const unflattenObj = (obj) => unflatten(JSON.parse(JSON.stringify(obj)));

// Traverse the object by decoding dot-connected `keyPath`
const list = keyPath.split('.').reduce((obj, key) => {
/**
* Traverse the given object by decoding dot-connected `keyPath`.
* @param {object} obj Unflatten object.
* @param {string} keyPath Dot-connected field name.
* @returns {object[]} Values.
*/
const getItemList = (obj, keyPath) =>
keyPath.split('.').reduce((_obj, key) => {
const _key = key.match(/^\d+$/) ? Number(key) : key;

// Create a new array when adding a new item
obj[_key] ||= [];
_obj[_key] ||= [];

return _obj[_key];
}, obj);

return obj[_key];
}, unflattenObj);
/**
* Update the value in a list field.
* @param {LocaleCode} locale Target locale.
* @param {string} keyPath Dot-connected field name.
* @param {({ valueList, viewList }) => void} manipulate A function to manipulate the list, which
* takes one object argument containing the value list and view state list. The typical usage is
* `list.splice()`.
*/
export const updateListField = (locale, keyPath, manipulate) => {
const currentValues = unflattenObj(get(entryDraft).currentValues[locale]);
const viewStates = unflattenObj(get(entryDraft).viewStates[locale]);

manipulate(list);
manipulate({
valueList: getItemList(currentValues, keyPath),
viewList: getItemList(viewStates, keyPath),
});

entryDraft.update((draft) => ({
...draft,
Expand All @@ -299,9 +318,13 @@ export const updateListField = (locale, keyPath, manipulate) => {
draft,
prop: 'currentValues',
locale,
target: flatten(unflattenObj),
target: flatten(currentValues),
}),
},
viewStates: {
...draft.viewStates,
[locale]: flatten(viewStates),
},
}));
};

Expand Down
20 changes: 14 additions & 6 deletions src/lib/typedefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,12 @@
* @see https://www.npmjs.com/package/flatten
*/

/**
* Flattened {@link EntryContent} object, where key is a key path, but value will be a file to be
* uploaded.
* @typedef {{ [key: string]: File }} FlattenedEntryContentFileList
*/

/**
* Flattened {@link EntryContent} object, where key is a key path, but value will be the value’s
* validity, using the same properties as the native HTML5 constraint validation.
Expand All @@ -460,9 +466,9 @@
*/

/**
* Flattened {@link EntryContent} object, where key is a key path, but value will be a file to be
* uploaded.
* @typedef {{ [key: string]: File }} FlattenedEntryContentFileList
* Flattened {@link EntryContent} object, where key is a key path, but value will be the value’s
* UI state.
* @typedef {{ [key: string]: any }} FlattenedEntryContentViewState
*/

/**
Expand All @@ -476,13 +482,15 @@
* @property {CollectionFile} [collectionFile] File details. (File collection only)
* @property {Entry} [originalEntry] Original entry or `undefined` if it’s a new entry draft.
* @property {{ [key: LocaleCode]: FlattenedEntryContent }} originalValues Key is a locale code,
* value is an flattened object containing all the original field values.
* value is a flattened object containing all the original field values.
* @property {{ [key: LocaleCode]: FlattenedEntryContent }} currentValues Key is a locale code,
* value is an flattened object containing all the current field values while editing.
* value is a flattened object containing all the current field values while editing.
* @property {{ [key: LocaleCode]: FlattenedEntryContentFileList }} files Files to be uploaded.
* @property {{ [key: LocaleCode]: FlattenedEntryContentValidityState }} validities Key is a locale
* code, value is an flattened object containing validation results of all the current field values
* code, value is a flattened object containing validation results of all the current field values
* while editing.
* @property {{ [key: LocaleCode]: FlattenedEntryContentViewState }} viewStates Key is a locale
* code, value is a flattened object containing the UI state.
*/

/**
Expand Down

0 comments on commit afe0a49

Please sign in to comment.