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 { colors, colorsRaw, components, lengths, Asset } from 'netlify-cms-ui-default';
|
||||||
import { VIEW_STYLE_LIST, VIEW_STYLE_GRID } from 'Constants/collectionViews';
|
import { VIEW_STYLE_LIST, VIEW_STYLE_GRID } from 'Constants/collectionViews';
|
||||||
import { summaryFormatter } from 'Lib/formatters';
|
import { summaryFormatter } from 'Lib/formatters';
|
||||||
|
import { keyToPathArray } from 'Lib/stringTemplate';
|
||||||
|
|
||||||
const ListCard = styled.li`
|
const ListCard = styled.li`
|
||||||
${components.card};
|
${components.card};
|
||||||
@ -128,7 +129,7 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
const { entry, inferedFields, collection } = ownProps;
|
const { entry, inferedFields, collection } = ownProps;
|
||||||
const label = entry.get('label');
|
const label = entry.get('label');
|
||||||
const entryData = entry.get('data');
|
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 summaryTemplate = collection.get('summary');
|
||||||
const summary = summaryTemplate
|
const summary = summaryTemplate
|
||||||
? summaryFormatter(summaryTemplate, entry, collection)
|
? summaryFormatter(summaryTemplate, entry, collection)
|
||||||
|
@ -7,7 +7,7 @@ import Frame from 'react-frame-component';
|
|||||||
import { lengths } from 'netlify-cms-ui-default';
|
import { lengths } from 'netlify-cms-ui-default';
|
||||||
import { resolveWidget, getPreviewTemplate, getPreviewStyles } from 'Lib/registry';
|
import { resolveWidget, getPreviewTemplate, getPreviewStyles } from 'Lib/registry';
|
||||||
import { ErrorBoundary } from 'UI';
|
import { ErrorBoundary } from 'UI';
|
||||||
import { selectTemplateName, selectInferedField } from 'Reducers/collections';
|
import { selectTemplateName, selectInferedField, selectField } from 'Reducers/collections';
|
||||||
import { INFERABLE_FIELDS } from 'Constants/fieldInference';
|
import { INFERABLE_FIELDS } from 'Constants/fieldInference';
|
||||||
import EditorPreviewContent from './EditorPreviewContent.js';
|
import EditorPreviewContent from './EditorPreviewContent.js';
|
||||||
import PreviewHOC from './PreviewHOC';
|
import PreviewHOC from './PreviewHOC';
|
||||||
@ -87,8 +87,15 @@ export default class PreviewPane extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const labelledWidgets = ['string', 'text', 'number'];
|
const labelledWidgets = ['string', 'text', 'number'];
|
||||||
if (Object.keys(this.inferedFields).indexOf(name) !== -1) {
|
const inferedField = Object.entries(this.inferedFields)
|
||||||
value = this.inferedFields[name].defaultPreview(value);
|
.filter(([key]) => {
|
||||||
|
const fieldToMatch = selectField(this.props.collection, key);
|
||||||
|
return fieldToMatch === field;
|
||||||
|
})
|
||||||
|
.map(([, value]) => value)[0];
|
||||||
|
|
||||||
|
if (inferedField) {
|
||||||
|
value = inferedField.defaultPreview(value);
|
||||||
} else if (
|
} else if (
|
||||||
value &&
|
value &&
|
||||||
labelledWidgets.indexOf(field.get('widget')) !== -1 &&
|
labelledWidgets.indexOf(field.get('widget')) !== -1 &&
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
compileStringTemplate,
|
compileStringTemplate,
|
||||||
parseDateFromEntry,
|
parseDateFromEntry,
|
||||||
SLUG_MISSING_REQUIRED_DATE,
|
SLUG_MISSING_REQUIRED_DATE,
|
||||||
|
keyToPathArray,
|
||||||
} from './stringTemplate';
|
} from './stringTemplate';
|
||||||
import { selectIdentifier } from '../reducers/collections';
|
import { selectIdentifier } from '../reducers/collections';
|
||||||
import { Collection, SlugConfig, Config, EntryMap } from '../types/redux';
|
import { Collection, SlugConfig, Config, EntryMap } from '../types/redux';
|
||||||
@ -100,9 +101,7 @@ export const slugFormatter = (
|
|||||||
) => {
|
) => {
|
||||||
const slugTemplate = collection.get('slug') || '{{slug}}';
|
const slugTemplate = collection.get('slug') || '{{slug}}';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
const identifier = entryData.getIn(keyToPathArray(selectIdentifier(collection) as string));
|
||||||
// @ts-ignore
|
|
||||||
const identifier = entryData.get(selectIdentifier(collection)) as string;
|
|
||||||
if (!identifier) {
|
if (!identifier) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Collection must have a field name that is a valid entry identifier, or must have `identifier_field` set',
|
'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 entryData = entry.get('data');
|
||||||
const date = parseDateFromEntry(entry, collection) || null;
|
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);
|
const summary = compileStringTemplate(summaryTemplate, date, identifier, entryData);
|
||||||
return summary;
|
return summary;
|
||||||
};
|
};
|
||||||
@ -223,7 +222,7 @@ export const folderFormatter = (
|
|||||||
fields = addFileTemplateFields(entry.get('path'), fields);
|
fields = addFileTemplateFields(entry.get('path'), fields);
|
||||||
|
|
||||||
const date = parseDateFromEntry(entry, collection) || null;
|
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 processSegment = getProcessSegment(slugConfig);
|
||||||
|
|
||||||
const mediaFolder = compileStringTemplate(
|
const mediaFolder = compileStringTemplate(
|
||||||
|
@ -23,7 +23,10 @@ const FIELD_PREFIX = 'fields.';
|
|||||||
const templateContentPattern = '[^}{]+';
|
const templateContentPattern = '[^}{]+';
|
||||||
const templateVariablePattern = `{{(${templateContentPattern})}}`;
|
const templateVariablePattern = `{{(${templateContentPattern})}}`;
|
||||||
|
|
||||||
export const keyToPathArray = (key: string) => {
|
export const keyToPathArray = (key?: string) => {
|
||||||
|
if (!key) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const parts = [];
|
const parts = [];
|
||||||
const separator = '';
|
const separator = '';
|
||||||
const chars = key.split(separator);
|
const chars = key.split(separator);
|
||||||
|
@ -6,6 +6,8 @@ import collections, {
|
|||||||
selectEntrySlug,
|
selectEntrySlug,
|
||||||
selectFieldsMediaFolders,
|
selectFieldsMediaFolders,
|
||||||
selectMediaFolders,
|
selectMediaFolders,
|
||||||
|
getFieldsNames,
|
||||||
|
selectField,
|
||||||
} from '../collections';
|
} from '../collections';
|
||||||
import { FILES, FOLDER } from 'Constants/collectionTypes';
|
import { FILES, FOLDER } from 'Constants/collectionTypes';
|
||||||
|
|
||||||
@ -237,4 +239,70 @@ describe('collections', () => {
|
|||||||
).toEqual(['file_media_folder', 'image_media_folder']);
|
).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,
|
EntryMap,
|
||||||
} from '../types/redux';
|
} from '../types/redux';
|
||||||
import { selectMediaFolder } from './entries';
|
import { selectMediaFolder } from './entries';
|
||||||
|
import { keyToPathArray } from '../lib/stringTemplate';
|
||||||
|
|
||||||
const collections = (state = null, action: CollectionsAction) => {
|
const collections = (state = null, action: CollectionsAction) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@ -186,10 +187,46 @@ export const selectAllowDeletion = (collection: Collection) =>
|
|||||||
selectors[collection.get('type')].allowDeletion(collection);
|
selectors[collection.get('type')].allowDeletion(collection);
|
||||||
export const selectTemplateName = (collection: Collection, slug: string) =>
|
export const selectTemplateName = (collection: Collection, slug: string) =>
|
||||||
selectors[collection.get('type')].templateName(collection, slug);
|
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) => {
|
export const selectIdentifier = (collection: Collection) => {
|
||||||
const identifier = collection.get('identifier_field');
|
const identifier = collection.get('identifier_field');
|
||||||
const identifierFields = identifier ? [identifier, ...IDENTIFIER_FIELDS] : IDENTIFIER_FIELDS;
|
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 =>
|
return identifierFields.find(id =>
|
||||||
fieldNames.find(name => name?.toLowerCase().trim() === id.toLowerCase().trim()),
|
fieldNames.find(name => name?.toLowerCase().trim() === id.toLowerCase().trim()),
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user