decap-cms-core
Version:
Decap CMS core application, see decap-cms package for the main distribution.
428 lines (417 loc) • 15.4 kB
JavaScript
import { List, Set, fromJS, OrderedMap } from 'immutable';
import get from 'lodash/get';
import escapeRegExp from 'lodash/escapeRegExp';
import { stringTemplate } from 'decap-cms-lib-widgets';
import consoleError from '../lib/consoleError';
import { CONFIG_SUCCESS } from '../actions/config';
import { FILES, FOLDER } from '../constants/collectionTypes';
import { COMMIT_DATE, COMMIT_AUTHOR } from '../constants/commitProps';
import { INFERABLE_FIELDS, IDENTIFIER_FIELDS, SORTABLE_FIELDS } from '../constants/fieldInference';
import { getFormatExtensions } from '../formats/formats';
import { selectMediaFolder } from './entries';
import { summaryFormatter } from '../lib/formatters';
const {
keyToPathArray
} = stringTemplate;
const defaultState = fromJS({});
function collections(state = defaultState, action) {
switch (action.type) {
case CONFIG_SUCCESS:
{
const collections = action.payload.collections;
let newState = OrderedMap({});
collections.forEach(collection => {
newState = newState.set(collection.name, fromJS(collection));
});
return newState;
}
default:
return state;
}
}
const selectors = {
[FOLDER]: {
entryExtension(collection) {
const ext = collection.get('extension') || get(getFormatExtensions(), collection.get('format') || 'frontmatter');
if (!ext) {
throw new Error(`No extension found for format ${collection.get('format')}`);
}
return ext.replace(/^\./, '');
},
fields(collection) {
return collection.get('fields');
},
entryPath(collection, slug) {
const folder = collection.get('folder').replace(/\/$/, '');
return `${folder}/${slug}.${this.entryExtension(collection)}`;
},
entrySlug(collection, path) {
const folder = collection.get('folder').replace(/\/$/, '');
const slug = path.split(folder + '/').pop()?.replace(new RegExp(`\\.${escapeRegExp(this.entryExtension(collection))}$`), '');
return slug;
},
allowNewEntries(collection) {
return collection.get('create');
},
allowDeletion(collection) {
return collection.get('delete', true);
},
templateName(collection) {
return collection.get('name');
}
},
[FILES]: {
fileForEntry(collection, slug) {
const files = collection.get('files');
return files && files.filter(f => f?.get('name') === slug).get(0);
},
fields(collection, slug) {
const file = this.fileForEntry(collection, slug);
return file && file.get('fields');
},
entryPath(collection, slug) {
const file = this.fileForEntry(collection, slug);
return file && file.get('file');
},
entrySlug(collection, path) {
const file = collection.get('files').filter(f => f?.get('file') === path).get(0);
return file && file.get('name');
},
entryLabel(collection, slug) {
const file = this.fileForEntry(collection, slug);
return file && file.get('label');
},
allowNewEntries() {
return false;
},
allowDeletion(collection) {
return collection.get('delete', false);
},
templateName(_collection, slug) {
return slug;
}
}
};
function getFieldsWithMediaFolders(fields) {
const fieldsWithMediaFolders = fields.reduce((acc, f) => {
if (f.has('media_folder')) {
acc = [...acc, f];
}
if (f.has('fields')) {
const fields = f.get('fields')?.toArray();
acc = [...acc, ...getFieldsWithMediaFolders(fields)];
} else if (f.has('field')) {
const field = f.get('field');
acc = [...acc, ...getFieldsWithMediaFolders([field])];
} else if (f.has('types')) {
const types = f.get('types')?.toArray();
acc = [...acc, ...getFieldsWithMediaFolders(types)];
}
return acc;
}, []);
return fieldsWithMediaFolders;
}
export function getFileFromSlug(collection, slug) {
return collection.get('files')?.toArray().find(f => f.get('name') === slug);
}
export function selectFieldsWithMediaFolders(collection, slug) {
if (collection.has('folder')) {
const fields = collection.get('fields').toArray();
return getFieldsWithMediaFolders(fields);
} else if (collection.has('files')) {
const fields = getFileFromSlug(collection, slug)?.get('fields').toArray() || [];
return getFieldsWithMediaFolders(fields);
}
return [];
}
export function selectMediaFolders(config, collection, entry) {
const fields = selectFieldsWithMediaFolders(collection, entry.get('slug'));
const folders = fields.map(f => selectMediaFolder(config, collection, entry, f));
if (collection.has('files')) {
const file = getFileFromSlug(collection, entry.get('slug'));
if (file) {
folders.unshift(selectMediaFolder(config, collection, entry, undefined));
}
}
if (collection.has('media_folder')) {
// stop evaluating media folders at collection level
collection = collection.delete('files');
folders.unshift(selectMediaFolder(config, collection, entry, undefined));
}
return Set(folders).toArray();
}
export function selectFields(collection, slug) {
return selectors[collection.get('type')].fields(collection, slug);
}
export function selectFolderEntryExtension(collection) {
return selectors[FOLDER].entryExtension(collection);
}
export function selectFileEntryLabel(collection, slug) {
return selectors[FILES].entryLabel(collection, slug);
}
export function selectEntryPath(collection, slug) {
return selectors[collection.get('type')].entryPath(collection, slug);
}
export function selectEntrySlug(collection, path) {
return selectors[collection.get('type')].entrySlug(collection, path);
}
export function selectAllowNewEntries(collection) {
return selectors[collection.get('type')].allowNewEntries(collection);
}
export function selectAllowDeletion(collection) {
return selectors[collection.get('type')].allowDeletion(collection);
}
export function selectTemplateName(collection, slug) {
return selectors[collection.get('type')].templateName(collection, slug);
}
export function getFieldsNames(fields, prefix = '') {
let names = fields.map(f => `${prefix}${f.get('name')}`);
fields.forEach((f, index) => {
if (f.has('fields')) {
const fields = f.get('fields')?.toArray();
names = [...names, ...getFieldsNames(fields, `${names[index]}.`)];
} else if (f.has('field')) {
const field = f.get('field');
names = [...names, ...getFieldsNames([field], `${names[index]}.`)];
} else if (f.has('types')) {
const types = f.get('types')?.toArray();
names = [...names, ...getFieldsNames(types, `${names[index]}.`)];
}
});
return names;
}
export function selectField(collection, key) {
const array = keyToPathArray(key);
let name;
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();
} else if (field?.has('field')) {
fields = [field?.get('field')];
} else if (field?.has('types')) {
fields = field?.get('types')?.toArray();
}
}
return field;
}
export function traverseFields(fields, updater, done = () => false) {
if (done()) {
return fields;
}
fields = fields.map(f => {
const field = updater(f);
if (done()) {
return field;
} else if (field.has('fields')) {
return field.set('fields', traverseFields(field.get('fields'), updater, done));
} else if (field.has('field')) {
return field.set('field', traverseFields(List([field.get('field')]), updater, done).get(0));
} else if (field.has('types')) {
return field.set('types', traverseFields(field.get('types'), updater, done));
} else {
return field;
}
}).toList();
return fields;
}
export function updateFieldByKey(collection, key, updater) {
const selected = selectField(collection, key);
if (!selected) {
return collection;
}
let updated = false;
function updateAndBreak(f) {
const field = f;
if (field === selected) {
updated = true;
return updater(field);
} else {
return field;
}
}
collection = collection.set('fields', traverseFields(collection.get('fields', List()), updateAndBreak, () => updated));
return collection;
}
export function selectIdentifier(collection) {
const identifier = collection.get('identifier_field');
const identifierFields = identifier ? [identifier, ...IDENTIFIER_FIELDS] : [...IDENTIFIER_FIELDS];
const fieldNames = getFieldsNames(collection.get('fields', List()).toArray());
return identifierFields.find(id => fieldNames.find(name => name.toLowerCase().trim() === id.toLowerCase().trim()));
}
export function selectInferredField(collection, fieldName) {
if (fieldName === 'title' && collection.get('identifier_field')) {
return selectIdentifier(collection);
}
const inferableField = INFERABLE_FIELDS[fieldName];
const fields = collection.get('fields');
let field;
// If collection has no fields or fieldName is not defined within inferables list, return null
if (!fields || !inferableField) return null;
// Try to return a field of the specified type with one of the synonyms
const mainTypeFields = fields.filter(f => f?.get('widget', 'string') === inferableField.type).map(f => f?.get('name'));
field = mainTypeFields.filter(f => inferableField.synonyms.indexOf(f) !== -1);
if (field && field.size > 0) return field.first();
// Try to return a field for each of the specified secondary types
const secondaryTypeFields = fields.filter(f => inferableField.secondaryTypes.indexOf(f?.get('widget', 'string')) !== -1).map(f => f?.get('name'));
field = secondaryTypeFields.filter(f => inferableField.synonyms.indexOf(f) !== -1);
if (field && field.size > 0) return field.first();
// Try to return the first field of the specified type
if (inferableField.fallbackToFirstField && mainTypeFields.size > 0) return mainTypeFields.first();
// Coundn't infer the field. Show error and return null.
if (inferableField.showError) {
consoleError(`The Field ${fieldName} is missing for the collection “${collection.get('name')}”`, `Decap CMS tries to infer the entry ${fieldName} automatically, but one couldn't be found for entries of the collection “${collection.get('name')}”. Please check your site configuration.`);
}
return null;
}
export function selectEntryCollectionTitle(collection, entry) {
// prefer formatted summary over everything else
const summaryTemplate = collection.get('summary');
if (summaryTemplate) return summaryFormatter(summaryTemplate, entry, collection);
// if the collection is a file collection return the label of the entry
if (collection.get('type') == FILES) {
const label = selectFileEntryLabel(collection, entry.get('slug'));
if (label) return label;
}
// try to infer a title field from the entry data
const entryData = entry.get('data');
const titleField = selectInferredField(collection, 'title');
const result = titleField && entryData.getIn(keyToPathArray(titleField));
// if the custom field does not yield a result, fallback to 'title'
if (!result && titleField !== 'title') {
return entryData.getIn(keyToPathArray('title'));
}
return result;
}
export function selectDefaultSortableFields(collection, backend, hasIntegration) {
let defaultSortable = SORTABLE_FIELDS.map(type => {
const field = selectInferredField(collection, type);
if (backend.isGitBackend() && type === 'author' && !field && !hasIntegration) {
// default to commit author if not author field is found
return COMMIT_AUTHOR;
}
return field;
}).filter(Boolean);
if (backend.isGitBackend() && !hasIntegration) {
// always have commit date by default
defaultSortable = [COMMIT_DATE, ...defaultSortable];
}
// Return as objects with field property
return defaultSortable.map(field => ({
field
}));
}
export function selectSortableFields(collection, t) {
const fields = collection.get('sortable_fields').toArray().map(sortableField => {
// Extract the field name and custom label from the sortable field object
const key = sortableField.get('field');
const customLabel = sortableField.get('label');
if (key === COMMIT_DATE) {
const label = customLabel || t('collection.defaultFields.updatedOn.label');
return {
key,
field: {
name: key,
label
}
};
}
const field = selectField(collection, key);
if (key === COMMIT_AUTHOR && !field) {
const label = customLabel || t('collection.defaultFields.author.label');
return {
key,
field: {
name: key,
label
}
};
}
let fieldObj = field?.toJS();
// If custom label is provided, override the field's label
if (fieldObj && customLabel) {
fieldObj = {
...fieldObj,
label: customLabel
};
}
// If no label exists at all, use the field name
if (fieldObj && !fieldObj.label) {
fieldObj = {
...fieldObj,
label: fieldObj.name || key
};
}
return {
key,
field: fieldObj
};
}).filter(item => !!item.field).map(item => ({
...item.field,
key: item.key
}));
return fields;
}
export function selectDefaultSortField(collection) {
const sortableFields = collection.get('sortable_fields').toArray();
const defaultField = sortableFields.find(field => field.get('default_sort') !== undefined);
if (!defaultField) {
return null;
}
const fieldName = defaultField.get('field');
const defaultSortValue = defaultField.get('default_sort');
// Determine direction based on default_sort value
let direction;
if (defaultSortValue === true || defaultSortValue === 'asc') {
direction = 'asc';
} else if (defaultSortValue === 'desc') {
direction = 'desc';
} else {
direction = 'asc'; // fallback
}
return {
field: fieldName,
direction
};
}
export function selectSortDataPath(collection, key) {
if (key === COMMIT_DATE) {
return 'updatedOn';
} else if (key === COMMIT_AUTHOR && !selectField(collection, key)) {
return 'author';
} else {
return `data.${key}`;
}
}
export function selectViewFilters(collection) {
const viewFilters = collection.get('view_filters').toJS();
return viewFilters;
}
export function selectViewGroups(collection) {
const viewGroups = collection.get('view_groups').toJS();
return viewGroups;
}
export function selectFieldsComments(collection, entryMap) {
let fields = [];
if (collection.has('folder')) {
fields = collection.get('fields').toArray();
} else if (collection.has('files')) {
const file = collection.get('files').find(f => f?.get('name') === entryMap.get('slug'));
fields = file.get('fields').toArray();
}
const comments = {};
const names = getFieldsNames(fields);
names.forEach(name => {
const field = selectField(collection, name);
if (field?.has('comment')) {
comments[name] = field.get('comment');
}
});
return comments;
}
export function selectHasMetaPath(collection) {
return collection.has('folder') && collection.get('type') === FOLDER && collection.has('meta') && collection.get('meta')?.has('path');
}
export default collections;