Feat: Allow using subfields as identifier field (#3219)

This commit is contained in:
Erez Rokah 2020-02-12 08:30:44 +02:00 committed by GitHub
parent e7589a96ef
commit c4125625f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 126 additions and 11 deletions

View File

@ -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)

View File

@ -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 &&

View File

@ -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(

View File

@ -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);

View File

@ -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),
);
});
});
});

View File

@ -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()),
);