Feat: Allow using subfields as identifier field (#3219)
This commit is contained in:
parent
e7589a96ef
commit
c4125625f9
@ -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)
|
||||
|
@ -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 &&
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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<EntryField>()).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<EntryField>()).map(field => field?.get('name'));
|
||||
const fieldNames = getFieldsNames(collection.get('fields', List<EntryField>()).toArray());
|
||||
return identifierFields.find(id =>
|
||||
fieldNames.find(name => name?.toLowerCase().trim() === id.toLowerCase().trim()),
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user