diff --git a/src/lib/components/contents/details/preview/field-preview.svelte b/src/lib/components/contents/details/preview/field-preview.svelte index 2e0060d6..03231756 100644 --- a/src/lib/components/contents/details/preview/field-preview.svelte +++ b/src/lib/components/contents/details/preview/field-preview.svelte @@ -2,10 +2,7 @@ import { escapeRegExp } from '@sveltia/utils/string'; import { previews } from '$lib/components/contents/details/widgets'; import { entryDraft } from '$lib/services/contents/draft'; - import { - joinExpanderKeyPathSegments, - syncExpanderStates, - } from '$lib/services/contents/draft/editor'; + import { getExpanderKeys, syncExpanderStates } from '$lib/services/contents/draft/editor'; import { defaultI18nConfig } from '$lib/services/contents/i18n'; /** @@ -33,8 +30,9 @@ ? /** @type {RelationField | SelectField} */ (fieldConfig).multiple : undefined; $: isList = widgetName === 'list' || (hasMultiple && multiple); - $: ({ collection, collectionFile, currentValues } = + $: ({ collectionName, fileName, collection, collectionFile, currentValues } = $entryDraft ?? /** @type {EntryDraft} */ ({})); + $: valueMap = currentValues[locale]; $: ({ i18nEnabled, defaultLocale } = (collectionFile ?? collection)?._i18n ?? defaultI18nConfig); $: canTranslate = i18nEnabled && (i18n === true || i18n === 'translate'); $: canDuplicate = i18nEnabled && i18n === 'duplicate'; @@ -42,11 +40,11 @@ // Multiple values are flattened in the value map object $: currentValue = isList - ? Object.entries(currentValues[locale]) + ? Object.entries(valueMap) .filter(([_keyPath]) => _keyPath.match(keyPathRegex)) .map(([, val]) => val) .filter((val) => val !== undefined) - : currentValues[locale][keyPath]; + : valueMap[keyPath]; /** * Called whenever the preview field is clicked. Highlight the corresponding editor field by @@ -55,9 +53,7 @@ const highlightEditorField = () => { syncExpanderStates( Object.fromEntries( - keyPath - .split('.') - .map((_key, index, arr) => [joinExpanderKeyPathSegments(arr, index + 1), true]), + getExpanderKeys({ collectionName, fileName, valueMap, keyPath }).map((key) => [key, true]), ), ); diff --git a/src/lib/components/contents/details/widgets/list/list-editor.svelte b/src/lib/components/contents/details/widgets/list/list-editor.svelte index 9df69835..0460819e 100644 --- a/src/lib/components/contents/details/widgets/list/list-editor.svelte +++ b/src/lib/components/contents/details/widgets/list/list-editor.svelte @@ -121,7 +121,13 @@ // Initialize the expander state syncExpanderStates({ [parentExpandedKeyPath]: !minimizeCollapsed, - ...Object.fromEntries(items.map((__, index) => [`${keyPath}.${index}`, !collapsed])), + ...Object.fromEntries( + items.map((__, index) => { + const key = `${keyPath}.${index}`; + + return [key, expanderStates?._[key] ?? !collapsed]; + }), + ), }); }); diff --git a/src/lib/components/contents/details/widgets/object/object-editor.svelte b/src/lib/components/contents/details/widgets/object/object-editor.svelte index 3c4b167b..97054d22 100644 --- a/src/lib/components/contents/details/widgets/object/object-editor.svelte +++ b/src/lib/components/contents/details/widgets/object/object-editor.svelte @@ -100,7 +100,9 @@ widgetId = generateUUID('short'); // Initialize the expander state - syncExpanderStates({ [parentExpandedKeyPath]: !collapsed }); + syncExpanderStates({ + [parentExpandedKeyPath]: expanderStates?._[parentExpandedKeyPath] ?? !collapsed, + }); }); /** diff --git a/src/lib/services/contents/draft/editor.js b/src/lib/services/contents/draft/editor.js index 31fd4e53..cfc314f0 100644 --- a/src/lib/services/contents/draft/editor.js +++ b/src/lib/services/contents/draft/editor.js @@ -1,8 +1,9 @@ import { IndexedDB, LocalStorage } from '@sveltia/utils/storage'; import equal from 'fast-deep-equal'; import { get, writable } from 'svelte/store'; -import { entryDraft } from '$lib/services/contents/draft'; import { backend } from '$lib/services/backends'; +import { entryDraft } from '$lib/services/contents/draft'; +import { getFieldConfig } from '$lib/services/contents/entry'; /** @type {IndexedDB | null | undefined} */ let settingsDB = undefined; @@ -76,30 +77,75 @@ export const syncExpanderStates = (stateMap) => { }; /** - * Join key path segments for the expander UI state. - * @param {string[]} arr - Key path array, e.g. `testimonials.0.authors.2.foo.bar`. - * @param {number} end - End index for `Array.slice()`. - * @returns {string} Joined string, e.g. `testimonials.0.authors.10.foo#.bar#`. + * Get a list of keys for the expander states, given the key path. The returned keys could include + * nested lists and objects. + * @param {object} args - Partial arguments for {@link getFieldConfig}. + * @param {string} args.collectionName - Collection name. + * @param {string} [args.fileName] - File name. + * @param {FlattenedEntryContent} args.valueMap - Object holding current entry values. + * @param {FieldKeyPath} args.keyPath - Key path, e.g. `testimonials.0.authors.2.foo`. + * @returns {string[]} Keys, e.g. `['testimonials', 'testimonials.0', 'testimonials.0.authors', + * 'testimonials.0.authors.2', 'testimonials.0.authors.2.foo']`. */ -export const joinExpanderKeyPathSegments = (arr, end) => - arr - .slice(0, end) - .map((k) => `${k}#`) - .join('.') - .replaceAll(/#\.(\d+)#/g, '.$1'); +export const getExpanderKeys = ({ collectionName, fileName, valueMap, keyPath }) => { + const keys = new Set(); + + keyPath.split('.').forEach((_keyPart, index, arr) => { + const _keyPath = arr.slice(0, index + 1).join('.'); + const config = getFieldConfig({ collectionName, fileName, valueMap, keyPath: _keyPath }); + + if (config?.widget === 'object') { + if (_keyPath.match(/\.\d+$/)) { + keys.add(_keyPath); + } + + keys.add(`${_keyPath}#`); + } else if (config?.widget === 'list') { + keys.add(_keyPath.match(/\.\d+$/) ? _keyPath : `${_keyPath}#`); + } else if (index > 0) { + const parentKeyPath = arr.slice(0, index).join('.'); + + const parentConfig = getFieldConfig({ + collectionName, + fileName, + valueMap, + keyPath: parentKeyPath, + }); + + if (parentConfig?.widget === 'object' && /** @type {ObjectField} */ (parentConfig).fields) { + keys.add(`${parentKeyPath}.${parentConfig.name}#`); + } + + if (parentConfig?.widget === 'list' && /** @type {ListField} */ (parentConfig).field) { + keys.add(_keyPath); + } + } + }); + + return [...keys]; +}; /** * Expand any invalid fields, including the parent list/object(s). + * @param {object} args - Partial arguments for {@link getFieldConfig}. + * @param {string} args.collectionName - Collection name. + * @param {string} [args.fileName] - File name. + * @param {Record} args.currentValues - Field values. */ -export const expandInvalidFields = () => { +export const expandInvalidFields = ({ collectionName, fileName, currentValues }) => { /** @type {Record} */ const stateMap = {}; - Object.values(get(entryDraft)?.validities ?? {}).forEach((validities) => { + Object.entries(get(entryDraft)?.validities ?? {}).forEach(([locale, validities]) => { Object.entries(validities).forEach(([keyPath, { valid }]) => { if (!valid) { - keyPath.split('.').forEach((_key, index, arr) => { - stateMap[joinExpanderKeyPathSegments(arr, index + 1)] = true; + getExpanderKeys({ + collectionName, + fileName, + valueMap: currentValues[locale], + keyPath, + }).forEach((key) => { + stateMap[key] = true; }); } }); diff --git a/src/lib/services/contents/draft/save.js b/src/lib/services/contents/draft/save.js index e0fe5a1a..3c32010c 100644 --- a/src/lib/services/contents/draft/save.js +++ b/src/lib/services/contents/draft/save.js @@ -574,13 +574,6 @@ export const createSavingEntryData = async ({ * @throws {Error} When the entry could not be validated or saved. */ export const saveEntry = async ({ skipCI = undefined } = {}) => { - if (!validateEntry()) { - expandInvalidFields(); - - throw new Error('validation_failed'); - } - - const _user = /** @type {User} */ (get(user)); const draft = /** @type {EntryDraft} */ (get(entryDraft)); const { @@ -596,6 +589,14 @@ export const saveEntry = async ({ skipCI = undefined } = {}) => { files, } = draft; + if (!validateEntry()) { + expandInvalidFields({ collectionName, fileName, currentValues }); + + throw new Error('validation_failed'); + } + + const _user = /** @type {User} */ (get(user)); + const { identifier_field: identifierField = 'title', slug: slugTemplate = `{{${identifierField}}}`,