UI updates (#151)
* infer card title * Infer entry body & image * infer image * Better terminology: EntryListing accept a single Collection * remove log * Refactored Collections VO into selectors * use selectors when showning card * fixed size cards * Added 'bio' and 'biography' to collection description inference synonyms * Removed unused card file * throw error instance * bugfix for file based collections * lint * moved components with css to own folder * Search Bugfix: More than one collection might be returned * Changed sidebar implementation. Closes #104 & #152 * Show spinning loading for unpublished entries * Refactored Sidebar into a separate container * Make preview widgets more robust
This commit is contained in:
@ -1,19 +1,22 @@
|
||||
import { OrderedMap, fromJS } from 'immutable';
|
||||
import consoleError from '../lib/consoleError';
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
import { FILES, FOLDER } from '../constants/collectionTypes';
|
||||
|
||||
const hasProperty = (config, property) => ({}.hasOwnProperty.call(config, property));
|
||||
|
||||
const collections = (state = null, action) => {
|
||||
const configCollections = action.payload && action.payload.collections;
|
||||
switch (action.type) {
|
||||
case CONFIG_SUCCESS:
|
||||
const configCollections = action.payload && action.payload.collections;
|
||||
return OrderedMap().withMutations((map) => {
|
||||
(configCollections || []).forEach((configCollection) => {
|
||||
if (hasProperty(configCollection, 'folder')) {
|
||||
configCollection.type = FOLDER; // eslint-disable-line no-param-reassign
|
||||
} else if (hasProperty(configCollection, 'files')) {
|
||||
configCollection.type = FILES; // eslint-disable-line no-param-reassign
|
||||
} else {
|
||||
throw new Error('Unknown collection type. Collections can be either Folder based or File based. Please verify your site configuration');
|
||||
}
|
||||
map.set(configCollection.name, fromJS(configCollection));
|
||||
});
|
||||
@ -23,4 +26,126 @@ const collections = (state = null, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatToExtension = format => ({
|
||||
markdown: 'md',
|
||||
yaml: 'yml',
|
||||
json: 'json',
|
||||
html: 'html',
|
||||
}[format]);
|
||||
|
||||
const inferables = {
|
||||
title: {
|
||||
type: 'string',
|
||||
secondaryTypes: [],
|
||||
synonyms: ['title', 'name', 'label', 'headline'],
|
||||
fallbackToFirstField: true,
|
||||
showError: true,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
secondaryTypes: ['text', 'markdown'],
|
||||
synonyms: ['shortDescription', 'short_description', 'shortdescription', 'description', 'brief', 'body', 'content', 'biography', 'bio'],
|
||||
fallbackToFirstField: false,
|
||||
showError: false,
|
||||
},
|
||||
image: {
|
||||
type: 'image',
|
||||
secondaryTypes: [],
|
||||
synonyms: ['image', 'thumbnail', 'thumb', 'picture', 'avatar'],
|
||||
fallbackToFirstField: false,
|
||||
showError: false,
|
||||
},
|
||||
};
|
||||
|
||||
const selectors = {
|
||||
[FOLDER]: {
|
||||
entryExtension(collection) {
|
||||
return collection.get('extension') || formatToExtension(collection.get('format') || 'markdown');
|
||||
},
|
||||
fields(collection) {
|
||||
return collection.get('fields');
|
||||
},
|
||||
entryPath(collection, slug) {
|
||||
return `${ collection.get('folder') }/${ slug }.${ this.entryExtension(collection) }`;
|
||||
},
|
||||
entrySlug(collection, path) {
|
||||
return path.split('/').pop().replace(/\.[^\.]+$/, '');
|
||||
},
|
||||
listMethod() {
|
||||
return 'entriesByFolder';
|
||||
},
|
||||
allowNewEntries(collection) {
|
||||
return collection.get('create');
|
||||
},
|
||||
templateName(collection) {
|
||||
return collection.get('name');
|
||||
},
|
||||
},
|
||||
[FILES]: {
|
||||
fileForEntry(collection, slug) {
|
||||
const files = collection.get('files');
|
||||
return 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');
|
||||
},
|
||||
listMethod() {
|
||||
return 'entriesByFiles';
|
||||
},
|
||||
allowNewEntries() {
|
||||
return false;
|
||||
},
|
||||
templateName(collection, slug) {
|
||||
return slug;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const selectFields = (collection, slug) => selectors[collection.get('type')].fields(collection, slug);
|
||||
export const selectEntryPath = (collection, slug) => selectors[collection.get('type')].entryPath(collection, slug);
|
||||
export const selectEntrySlug = (collection, path) => selectors[collection.get('type')].entrySlug(collection, path);
|
||||
export const selectListMethod = collection => selectors[collection.get('type')].listMethod();
|
||||
export const selectAllowNewEntries = collection => selectors[collection.get('type')].allowNewEntries(collection);
|
||||
export const selectTemplateName = (collection, slug) => selectors[collection.get('type')].templateName(collection, slug);
|
||||
export const selectInferedField = (collection, fieldName) => {
|
||||
const inferableField = inferables[fieldName];
|
||||
const fields = collection.get('fields');
|
||||
let field;
|
||||
|
||||
// If colllection 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') === 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')) !== -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') }”`,
|
||||
`Netlify 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 default collections;
|
||||
|
@ -1,17 +0,0 @@
|
||||
/* Reducer for some global UI state that we want to share between components
|
||||
* Now being used for isFetching state to display global loading indicator
|
||||
* */
|
||||
|
||||
const globalReducer = (state = { isFetching: false }, action) => {
|
||||
if ((action.type.indexOf('REQUEST') > -1)) {
|
||||
return { isFetching: true };
|
||||
} else if (
|
||||
(action.type.indexOf('SUCCESS') > -1) ||
|
||||
(action.type.indexOf('FAILURE') > -1)
|
||||
) {
|
||||
return { isFetching: false };
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default globalReducer;
|
27
src/reducers/globalUI.js
Normal file
27
src/reducers/globalUI.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { Map } from 'immutable';
|
||||
import { TOGGLE_SIDEBAR, OPEN_SIDEBAR } from '../actions/globalUI';
|
||||
/*
|
||||
* Reducer for some global UI state that we want to share between components
|
||||
* */
|
||||
const globalUI = (state = Map({ isFetching: false, sidebarIsOpen: true }), action) => {
|
||||
// Generic, global loading indicator
|
||||
if ((action.type.indexOf('REQUEST') > -1)) {
|
||||
return state.set('isFetching', true);
|
||||
} else if (
|
||||
(action.type.indexOf('SUCCESS') > -1) ||
|
||||
(action.type.indexOf('FAILURE') > -1)
|
||||
) {
|
||||
return state.set('isFetching', false);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case TOGGLE_SIDEBAR:
|
||||
return state.set('sidebarIsOpen', !state.get('sidebarIsOpen'));
|
||||
case OPEN_SIDEBAR:
|
||||
return state.set('sidebarIsOpen', action.payload.open);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default globalUI;
|
@ -7,7 +7,7 @@ import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow';
|
||||
import entryDraft from './entryDraft';
|
||||
import collections from './collections';
|
||||
import medias, * as fromMedias from './medias';
|
||||
import global from './global';
|
||||
import globalUI from './globalUI';
|
||||
|
||||
const reducers = {
|
||||
auth,
|
||||
@ -19,7 +19,7 @@ const reducers = {
|
||||
editorialWorkflow,
|
||||
entryDraft,
|
||||
medias,
|
||||
global,
|
||||
globalUI,
|
||||
};
|
||||
|
||||
export default reducers;
|
||||
|
Reference in New Issue
Block a user