diff --git a/packages/netlify-cms-core/src/components/Collection/Entries/EntryCard.js b/packages/netlify-cms-core/src/components/Collection/Entries/EntryCard.js index 27728032..3776bb04 100644 --- a/packages/netlify-cms-core/src/components/Collection/Entries/EntryCard.js +++ b/packages/netlify-cms-core/src/components/Collection/Entries/EntryCard.js @@ -6,6 +6,7 @@ import { Link } from 'react-router-dom'; import { colors, colorsRaw, components, lengths, Asset } from 'netlify-cms-ui-default'; import { VIEW_STYLE_LIST, VIEW_STYLE_GRID } from 'Constants/collectionViews'; import { summaryFormatter } from 'Lib/formatters'; +import { keyToPathArray } from 'Lib/stringTemplate'; const ListCard = styled.li` ${components.card}; @@ -128,7 +129,7 @@ const mapStateToProps = (state, ownProps) => { const { entry, inferedFields, collection } = ownProps; const label = entry.get('label'); const entryData = entry.get('data'); - const defaultTitle = label || entryData.get(inferedFields.titleField); + const defaultTitle = label || entryData.getIn(keyToPathArray(inferedFields.titleField)); const summaryTemplate = collection.get('summary'); const summary = summaryTemplate ? summaryFormatter(summaryTemplate, entry, collection) diff --git a/packages/netlify-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js b/packages/netlify-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js index c1779bc0..654a68d5 100644 --- a/packages/netlify-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js +++ b/packages/netlify-cms-core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js @@ -7,7 +7,7 @@ import Frame from 'react-frame-component'; import { lengths } from 'netlify-cms-ui-default'; import { resolveWidget, getPreviewTemplate, getPreviewStyles } from 'Lib/registry'; import { ErrorBoundary } from 'UI'; -import { selectTemplateName, selectInferedField } from 'Reducers/collections'; +import { selectTemplateName, selectInferedField, selectField } from 'Reducers/collections'; import { INFERABLE_FIELDS } from 'Constants/fieldInference'; import EditorPreviewContent from './EditorPreviewContent.js'; import PreviewHOC from './PreviewHOC'; @@ -87,8 +87,15 @@ export default class PreviewPane extends React.Component { } const labelledWidgets = ['string', 'text', 'number']; - if (Object.keys(this.inferedFields).indexOf(name) !== -1) { - value = this.inferedFields[name].defaultPreview(value); + const inferedField = Object.entries(this.inferedFields) + .filter(([key]) => { + const fieldToMatch = selectField(this.props.collection, key); + return fieldToMatch === field; + }) + .map(([, value]) => value)[0]; + + if (inferedField) { + value = inferedField.defaultPreview(value); } else if ( value && labelledWidgets.indexOf(field.get('widget')) !== -1 && diff --git a/packages/netlify-cms-core/src/lib/formatters.ts b/packages/netlify-cms-core/src/lib/formatters.ts index e3c61448..08a38eb6 100644 --- a/packages/netlify-cms-core/src/lib/formatters.ts +++ b/packages/netlify-cms-core/src/lib/formatters.ts @@ -5,6 +5,7 @@ import { compileStringTemplate, parseDateFromEntry, SLUG_MISSING_REQUIRED_DATE, + keyToPathArray, } from './stringTemplate'; import { selectIdentifier } from '../reducers/collections'; import { Collection, SlugConfig, Config, EntryMap } from '../types/redux'; @@ -100,9 +101,7 @@ export const slugFormatter = ( ) => { const slugTemplate = collection.get('slug') || '{{slug}}'; - // eslint-disable-next-line @typescript-eslint/ban-ts-ignore - // @ts-ignore - const identifier = entryData.get(selectIdentifier(collection)) as string; + const identifier = entryData.getIn(keyToPathArray(selectIdentifier(collection) as string)); if (!identifier) { throw new Error( 'Collection must have a field name that is a valid entry identifier, or must have `identifier_field` set', @@ -203,7 +202,7 @@ export const summaryFormatter = ( ) => { const entryData = entry.get('data'); const date = parseDateFromEntry(entry, collection) || null; - const identifier = entryData.get(selectIdentifier(collection)); + const identifier = entryData.getIn(keyToPathArray(selectIdentifier(collection) as string)); const summary = compileStringTemplate(summaryTemplate, date, identifier, entryData); return summary; }; @@ -223,7 +222,7 @@ export const folderFormatter = ( fields = addFileTemplateFields(entry.get('path'), fields); const date = parseDateFromEntry(entry, collection) || null; - const identifier = fields.get(selectIdentifier(collection) as string); + const identifier = fields.getIn(keyToPathArray(selectIdentifier(collection) as string)); const processSegment = getProcessSegment(slugConfig); const mediaFolder = compileStringTemplate( diff --git a/packages/netlify-cms-core/src/lib/stringTemplate.ts b/packages/netlify-cms-core/src/lib/stringTemplate.ts index 99345245..5f3caff9 100644 --- a/packages/netlify-cms-core/src/lib/stringTemplate.ts +++ b/packages/netlify-cms-core/src/lib/stringTemplate.ts @@ -23,7 +23,10 @@ const FIELD_PREFIX = 'fields.'; const templateContentPattern = '[^}{]+'; const templateVariablePattern = `{{(${templateContentPattern})}}`; -export const keyToPathArray = (key: string) => { +export const keyToPathArray = (key?: string) => { + if (!key) { + return []; + } const parts = []; const separator = ''; const chars = key.split(separator); diff --git a/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js b/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js index 043139de..7462982f 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js @@ -6,6 +6,8 @@ import collections, { selectEntrySlug, selectFieldsMediaFolders, selectMediaFolders, + getFieldsNames, + selectField, } from '../collections'; import { FILES, FOLDER } from 'Constants/collectionTypes'; @@ -237,4 +239,70 @@ describe('collections', () => { ).toEqual(['file_media_folder', 'image_media_folder']); }); }); + + describe('getFieldsNames', () => { + it('should get flat fields names', () => { + const collection = fromJS({ + fields: [{ name: 'en' }, { name: 'es' }], + }); + expect(getFieldsNames(collection.get('fields').toArray())).toEqual(['en', 'es']); + }); + + it('should get nested fields names', () => { + const collection = fromJS({ + fields: [ + { name: 'en', fields: [{ name: 'title' }, { name: 'body' }] }, + { name: 'es', fields: [{ name: 'title' }, { name: 'body' }] }, + { name: 'it', field: { name: 'title', fields: [{ name: 'subTitle' }] } }, + ], + }); + expect(getFieldsNames(collection.get('fields').toArray())).toEqual([ + 'en', + 'es', + 'it', + 'en.title', + 'en.body', + 'es.title', + 'es.body', + 'it.title', + 'it.title.subTitle', + ]); + }); + }); + + describe('selectField', () => { + it('should return top field by key', () => { + const collection = fromJS({ + fields: [{ name: 'en' }, { name: 'es' }], + }); + expect(selectField(collection, 'en')).toBe(collection.get('fields').get(0)); + }); + + it('should return nested field by key', () => { + const collection = fromJS({ + fields: [ + { name: 'en', fields: [{ name: 'title' }, { name: 'body' }] }, + { name: 'es', fields: [{ name: 'title' }, { name: 'body' }] }, + { name: 'it', field: { name: 'title', fields: [{ name: 'subTitle' }] } }, + ], + }); + + expect(selectField(collection, 'en.title')).toBe( + collection + .get('fields') + .get(0) + .get('fields') + .get(0), + ); + + expect(selectField(collection, 'it.title.subTitle')).toBe( + collection + .get('fields') + .get(2) + .get('field') + .get('fields') + .get(0), + ); + }); + }); }); diff --git a/packages/netlify-cms-core/src/reducers/collections.ts b/packages/netlify-cms-core/src/reducers/collections.ts index a6a3f1e1..80b747ff 100644 --- a/packages/netlify-cms-core/src/reducers/collections.ts +++ b/packages/netlify-cms-core/src/reducers/collections.ts @@ -14,6 +14,7 @@ import { EntryMap, } from '../types/redux'; import { selectMediaFolder } from './entries'; +import { keyToPathArray } from '../lib/stringTemplate'; const collections = (state = null, action: CollectionsAction) => { switch (action.type) { @@ -186,10 +187,46 @@ export const selectAllowDeletion = (collection: Collection) => selectors[collection.get('type')].allowDeletion(collection); export const selectTemplateName = (collection: Collection, slug: string) => selectors[collection.get('type')].templateName(collection, slug); + +export const getFieldsNames = (fields: EntryField[], prefix = '') => { + let names = fields.map(f => `${prefix}${f.get('name')}`); + + fields.forEach((f, index) => { + if (f.has('fields')) { + const fields = f.get('fields')?.toArray() as EntryField[]; + names = [...names, ...getFieldsNames(fields, `${names[index]}.`)]; + } + if (f.has('field')) { + const field = f.get('field') as EntryField; + names = [...names, ...getFieldsNames([field], `${names[index]}.`)]; + } + }); + + return names; +}; + +export const selectField = (collection: Collection, key: string) => { + const array = keyToPathArray(key); + let name: string | undefined; + let field; + let fields = collection.get('fields', List()).toArray(); + while ((name = array.shift()) && fields) { + field = fields.find(f => f.get('name') === name); + if (field?.has('fields')) { + fields = field?.get('fields')?.toArray() as EntryField[]; + } + if (field?.has('field')) { + fields = [field?.get('field') as EntryField]; + } + } + + return field; +}; + export const selectIdentifier = (collection: Collection) => { const identifier = collection.get('identifier_field'); const identifierFields = identifier ? [identifier, ...IDENTIFIER_FIELDS] : IDENTIFIER_FIELDS; - const fieldNames = collection.get('fields', List()).map(field => field?.get('name')); + const fieldNames = getFieldsNames(collection.get('fields', List()).toArray()); return identifierFields.find(id => fieldNames.find(name => name?.toLowerCase().trim() === id.toLowerCase().trim()), );