feat(media): add external media library support, Uploadcare integration (#1602)
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
import { Map } from 'immutable';
|
||||
import { actions as notifActions } from 'redux-notifications';
|
||||
import { currentBackend } from 'src/backend';
|
||||
import { createAssetProxy } from 'ValueObjects/AssetProxy';
|
||||
@ -10,6 +11,7 @@ const { notifSend } = notifActions;
|
||||
|
||||
export const MEDIA_LIBRARY_OPEN = 'MEDIA_LIBRARY_OPEN';
|
||||
export const MEDIA_LIBRARY_CLOSE = 'MEDIA_LIBRARY_CLOSE';
|
||||
export const MEDIA_LIBRARY_CREATE = 'MEDIA_LIBRARY_CREATE';
|
||||
export const MEDIA_INSERT = 'MEDIA_INSERT';
|
||||
export const MEDIA_REMOVE_INSERTED = 'MEDIA_REMOVE_INSERTED';
|
||||
export const MEDIA_LOAD_REQUEST = 'MEDIA_LOAD_REQUEST';
|
||||
@ -25,12 +27,58 @@ export const MEDIA_DISPLAY_URL_REQUEST = 'MEDIA_DISPLAY_URL_REQUEST';
|
||||
export const MEDIA_DISPLAY_URL_SUCCESS = 'MEDIA_DISPLAY_URL_SUCCESS';
|
||||
export const MEDIA_DISPLAY_URL_FAILURE = 'MEDIA_DISPLAY_URL_FAILURE';
|
||||
|
||||
export function openMediaLibrary(payload) {
|
||||
return { type: MEDIA_LIBRARY_OPEN, payload };
|
||||
export function createMediaLibrary(instance) {
|
||||
const api = {
|
||||
show: instance.show || (() => {}),
|
||||
hide: instance.hide || (() => {}),
|
||||
onClearControl: instance.onClearControl || (() => {}),
|
||||
onRemoveControl: instance.onRemoveControl || (() => {}),
|
||||
enableStandalone: instance.enableStandalone || (() => {}),
|
||||
};
|
||||
return { type: MEDIA_LIBRARY_CREATE, payload: api };
|
||||
}
|
||||
|
||||
export function clearMediaControl(id) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const mediaLibrary = state.mediaLibrary.get('externalLibrary');
|
||||
if (mediaLibrary) {
|
||||
mediaLibrary.onClearControl({ id });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function removeMediaControl(id) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const mediaLibrary = state.mediaLibrary.get('externalLibrary');
|
||||
if (mediaLibrary) {
|
||||
mediaLibrary.onRemoveControl({ id });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function openMediaLibrary(payload = {}) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const mediaLibrary = state.mediaLibrary.get('externalLibrary');
|
||||
if (mediaLibrary) {
|
||||
const { controlID: id, value, config = Map(), forImage } = payload;
|
||||
mediaLibrary.show({ id, value, config: config.toJS(), imagesOnly: forImage });
|
||||
}
|
||||
dispatch({ type: MEDIA_LIBRARY_OPEN, payload });
|
||||
};
|
||||
}
|
||||
|
||||
export function closeMediaLibrary() {
|
||||
return { type: MEDIA_LIBRARY_CLOSE };
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const mediaLibrary = state.mediaLibrary.get('externalLibrary');
|
||||
if (mediaLibrary) {
|
||||
mediaLibrary.hide();
|
||||
}
|
||||
dispatch({ type: MEDIA_LIBRARY_CLOSE });
|
||||
};
|
||||
}
|
||||
|
||||
export function insertMedia(mediaPath) {
|
||||
|
14
packages/netlify-cms-core/src/bootstrap.js
vendored
14
packages/netlify-cms-core/src/bootstrap.js
vendored
@ -4,12 +4,12 @@ import { Provider } from 'react-redux';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { ConnectedRouter } from 'react-router-redux';
|
||||
import history from 'Routing/history';
|
||||
import configureStore from 'Redux/configureStore';
|
||||
import store from 'Redux';
|
||||
import { mergeConfig } from 'Actions/config';
|
||||
import { setStore } from 'ValueObjects/AssetProxy';
|
||||
import { ErrorBoundary } from 'UI';
|
||||
import App from 'App/App';
|
||||
import 'EditorWidgets';
|
||||
import 'src/mediaLibrary';
|
||||
import 'what-input';
|
||||
|
||||
const ROOT_ID = 'nc-root';
|
||||
@ -47,11 +47,6 @@ function bootstrap(opts = {}) {
|
||||
return newRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Redux store.
|
||||
*/
|
||||
const store = configureStore();
|
||||
|
||||
/**
|
||||
* Dispatch config to store if received. This config will be merged into
|
||||
* config.yml if it exists, and any portion that produces a conflict will be
|
||||
@ -61,11 +56,6 @@ function bootstrap(opts = {}) {
|
||||
store.dispatch(mergeConfig(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass initial state into AssetProxy factory.
|
||||
*/
|
||||
setStore(store);
|
||||
|
||||
/**
|
||||
* Create connected root component.
|
||||
*/
|
||||
|
@ -7,11 +7,11 @@ import { connect } from 'react-redux';
|
||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||
import { Notifs } from 'redux-notifications';
|
||||
import TopBarProgress from 'react-topbar-progress-indicator';
|
||||
import { loadConfig as actionLoadConfig } from 'Actions/config';
|
||||
import { loginUser as actionLoginUser, logoutUser as actionLogoutUser } from 'Actions/auth';
|
||||
import { loadConfig } from 'Actions/config';
|
||||
import { loginUser, logoutUser } from 'Actions/auth';
|
||||
import { currentBackend } from 'src/backend';
|
||||
import { createNewEntry } from 'Actions/collections';
|
||||
import { openMediaLibrary as actionOpenMediaLibrary } from 'Actions/mediaLibrary';
|
||||
import { openMediaLibrary } from 'Actions/mediaLibrary';
|
||||
import MediaLibrary from 'MediaLibrary/MediaLibrary';
|
||||
import { Toast } from 'UI';
|
||||
import { Loader, colors } from 'netlify-cms-ui-default';
|
||||
@ -53,13 +53,16 @@ class App extends React.Component {
|
||||
auth: ImmutablePropTypes.map,
|
||||
config: ImmutablePropTypes.map,
|
||||
collections: ImmutablePropTypes.orderedMap,
|
||||
loadConfig: PropTypes.func.isRequired,
|
||||
loginUser: PropTypes.func.isRequired,
|
||||
logoutUser: PropTypes.func.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
user: ImmutablePropTypes.map,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
publishMode: PropTypes.oneOf([SIMPLE, EDITORIAL_WORKFLOW]),
|
||||
siteId: PropTypes.string,
|
||||
useMediaLibrary: PropTypes.bool,
|
||||
openMediaLibrary: PropTypes.func.isRequired,
|
||||
showMediaButton: PropTypes.bool,
|
||||
};
|
||||
|
||||
static configError(config) {
|
||||
@ -77,11 +80,12 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(actionLoadConfig());
|
||||
const { loadConfig } = this.props;
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
handleLogin(credentials) {
|
||||
this.props.dispatch(actionLoginUser(credentials));
|
||||
this.props.loginUser(credentials);
|
||||
}
|
||||
|
||||
authenticating() {
|
||||
@ -127,7 +131,9 @@ class App extends React.Component {
|
||||
logoutUser,
|
||||
isFetching,
|
||||
publishMode,
|
||||
useMediaLibrary,
|
||||
openMediaLibrary,
|
||||
showMediaButton,
|
||||
} = this.props;
|
||||
|
||||
if (config === null) {
|
||||
@ -160,6 +166,7 @@ class App extends React.Component {
|
||||
openMediaLibrary={openMediaLibrary}
|
||||
hasWorkflow={hasWorkflow}
|
||||
displayUrl={config.get('display_url')}
|
||||
showMediaButton={showMediaButton}
|
||||
/>
|
||||
<AppMainContainer>
|
||||
{isFetching && <TopBarProgress />}
|
||||
@ -180,7 +187,7 @@ class App extends React.Component {
|
||||
/>
|
||||
<Route component={NotFoundPage} />
|
||||
</Switch>
|
||||
<MediaLibrary />
|
||||
{useMediaLibrary ? <MediaLibrary /> : null}
|
||||
</div>
|
||||
</AppMainContainer>
|
||||
</div>
|
||||
@ -189,25 +196,31 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const { auth, config, collections, globalUI } = state;
|
||||
const { auth, config, collections, globalUI, mediaLibrary } = state;
|
||||
const user = auth && auth.get('user');
|
||||
const isFetching = globalUI.get('isFetching');
|
||||
const publishMode = config && config.get('publish_mode');
|
||||
return { auth, config, collections, user, isFetching, publishMode };
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
const useMediaLibrary = !mediaLibrary.get('externalLibrary');
|
||||
const showMediaButton = mediaLibrary.get('showMediaButton');
|
||||
return {
|
||||
dispatch,
|
||||
openMediaLibrary: () => {
|
||||
dispatch(actionOpenMediaLibrary());
|
||||
},
|
||||
logoutUser: () => {
|
||||
dispatch(actionLogoutUser());
|
||||
},
|
||||
auth,
|
||||
config,
|
||||
collections,
|
||||
user,
|
||||
isFetching,
|
||||
publishMode,
|
||||
showMediaButton,
|
||||
useMediaLibrary,
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
openMediaLibrary,
|
||||
loadConfig,
|
||||
loginUser,
|
||||
logoutUser,
|
||||
};
|
||||
|
||||
export default hot(module)(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
|
@ -125,6 +125,7 @@ export default class Header extends React.Component {
|
||||
openMediaLibrary,
|
||||
hasWorkflow,
|
||||
displayUrl,
|
||||
showMediaButton,
|
||||
} = this.props;
|
||||
|
||||
const createableCollections = collections
|
||||
@ -150,10 +151,12 @@ export default class Header extends React.Component {
|
||||
Workflow
|
||||
</AppHeaderNavLink>
|
||||
) : null}
|
||||
<AppHeaderButton onClick={openMediaLibrary}>
|
||||
<Icon type="media-alt" />
|
||||
Media
|
||||
</AppHeaderButton>
|
||||
{showMediaButton ? (
|
||||
<AppHeaderButton onClick={openMediaLibrary}>
|
||||
<Icon type="media-alt" />
|
||||
Media
|
||||
</AppHeaderButton>
|
||||
) : null}
|
||||
</nav>
|
||||
<AppHeaderActions>
|
||||
{createableCollections.size > 0 && (
|
||||
|
@ -8,7 +8,12 @@ import { colors, colorsRaw, transitions, lengths, borders } from 'netlify-cms-ui
|
||||
import { resolveWidget, getEditorComponents } from 'Lib/registry';
|
||||
import { addAsset } from 'Actions/media';
|
||||
import { query, clearSearch } from 'Actions/search';
|
||||
import { openMediaLibrary, removeInsertedMedia } from 'Actions/mediaLibrary';
|
||||
import {
|
||||
openMediaLibrary,
|
||||
removeInsertedMedia,
|
||||
clearMediaControl,
|
||||
removeMediaControl,
|
||||
} from 'Actions/mediaLibrary';
|
||||
import { getAsset } from 'Reducers';
|
||||
import Widget from './Widget';
|
||||
|
||||
@ -153,6 +158,8 @@ class EditorControl extends React.Component {
|
||||
boundGetAsset,
|
||||
onChange,
|
||||
openMediaLibrary,
|
||||
clearMediaControl,
|
||||
removeMediaControl,
|
||||
addAsset,
|
||||
removeInsertedMedia,
|
||||
onValidate,
|
||||
@ -210,6 +217,8 @@ class EditorControl extends React.Component {
|
||||
onChange={(newValue, newMetadata) => onChange(fieldName, newValue, newMetadata)}
|
||||
onValidate={onValidate && partial(onValidate, fieldName)}
|
||||
onOpenMediaLibrary={openMediaLibrary}
|
||||
onClearMediaControl={clearMediaControl}
|
||||
onRemoveMediaControl={removeMediaControl}
|
||||
onRemoveInsertedMedia={removeInsertedMedia}
|
||||
onAddAsset={addAsset}
|
||||
getAsset={boundGetAsset}
|
||||
@ -244,6 +253,8 @@ const mapStateToProps = state => ({
|
||||
|
||||
const mapDispatchToProps = {
|
||||
openMediaLibrary,
|
||||
clearMediaControl,
|
||||
removeMediaControl,
|
||||
removeInsertedMedia,
|
||||
addAsset,
|
||||
query,
|
||||
|
@ -35,6 +35,8 @@ export default class Widget extends Component {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onValidate: PropTypes.func,
|
||||
onOpenMediaLibrary: PropTypes.func.isRequired,
|
||||
onClearMediaControl: PropTypes.func.isRequired,
|
||||
onRemoveMediaControl: PropTypes.func.isRequired,
|
||||
onAddAsset: PropTypes.func.isRequired,
|
||||
onRemoveInsertedMedia: PropTypes.func.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
@ -191,6 +193,8 @@ export default class Widget extends Component {
|
||||
metadata,
|
||||
onChange,
|
||||
onOpenMediaLibrary,
|
||||
onRemoveMediaControl,
|
||||
onClearMediaControl,
|
||||
onAddAsset,
|
||||
onRemoveInsertedMedia,
|
||||
getAsset,
|
||||
@ -219,6 +223,8 @@ export default class Widget extends Component {
|
||||
onChange,
|
||||
onChangeObject: this.onChangeObject,
|
||||
onOpenMediaLibrary,
|
||||
onClearMediaControl,
|
||||
onRemoveMediaControl,
|
||||
onAddAsset,
|
||||
onRemoveInsertedMedia,
|
||||
getAsset,
|
||||
|
@ -38,6 +38,14 @@ const getConfigSchema = () => ({
|
||||
display_url: { type: 'string', examples: ['https://example.com'] },
|
||||
media_folder: { type: 'string', examples: ['assets/uploads'] },
|
||||
public_folder: { type: 'string', examples: ['/uploads'] },
|
||||
media_library: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', examples: ['uploadcare'] },
|
||||
config: { type: 'object' },
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
publish_mode: {
|
||||
type: 'string',
|
||||
enum: ['editorial_workflow'],
|
||||
@ -128,7 +136,8 @@ const getConfigSchema = () => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['backend', 'media_folder', 'collections'],
|
||||
required: ['backend', 'collections'],
|
||||
anyOf: [{ required: ['media_folder'] }, { required: ['media_library'] }],
|
||||
});
|
||||
|
||||
class ConfigError extends Error {
|
||||
|
@ -11,6 +11,7 @@ const registry = {
|
||||
widgets: {},
|
||||
editorComponents: Map(),
|
||||
widgetValueSerializers: {},
|
||||
mediaLibraries: [],
|
||||
};
|
||||
|
||||
export default {
|
||||
@ -27,6 +28,8 @@ export default {
|
||||
getWidgetValueSerializer,
|
||||
registerBackend,
|
||||
getBackend,
|
||||
registerMediaLibrary,
|
||||
getMediaLibrary,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -109,3 +112,17 @@ export function registerBackend(name, BackendClass) {
|
||||
export function getBackend(name) {
|
||||
return registry.backends[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Media Libraries
|
||||
*/
|
||||
export function registerMediaLibrary(mediaLibrary, options) {
|
||||
if (registry.mediaLibraries.find(ml => mediaLibrary.name === ml.name)) {
|
||||
throw new Error(`A media library named ${mediaLibrary.name} has already been registered.`);
|
||||
}
|
||||
registry.mediaLibraries.push({ ...mediaLibrary, options });
|
||||
}
|
||||
|
||||
export function getMediaLibrary(name) {
|
||||
return registry.mediaLibraries.find(ml => ml.name === name);
|
||||
}
|
||||
|
24
packages/netlify-cms-core/src/mediaLibrary.js
Normal file
24
packages/netlify-cms-core/src/mediaLibrary.js
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* This module is currently concerned only with external media libraries
|
||||
* registered via `registerMediaLibrary`.
|
||||
*/
|
||||
import { once } from 'lodash';
|
||||
import { getMediaLibrary } from 'Lib/registry';
|
||||
import store from 'Redux';
|
||||
import { createMediaLibrary, insertMedia } from 'Actions/mediaLibrary';
|
||||
|
||||
const initializeMediaLibrary = once(async function initializeMediaLibrary(name, options) {
|
||||
const lib = getMediaLibrary(name);
|
||||
const handleInsert = url => store.dispatch(insertMedia(url));
|
||||
const instance = await lib.init({ options, handleInsert });
|
||||
store.dispatch(createMediaLibrary(instance));
|
||||
});
|
||||
|
||||
store.subscribe(() => {
|
||||
const state = store.getState();
|
||||
const mediaLibraryName = state.config.getIn(['media_library', 'name']);
|
||||
if (mediaLibraryName && !state.mediaLibrary.get('externalLibrary')) {
|
||||
const mediaLibraryConfig = state.config.get('media_library').toJS();
|
||||
initializeMediaLibrary(mediaLibraryName, mediaLibraryConfig);
|
||||
}
|
||||
});
|
@ -1,9 +1,9 @@
|
||||
import { get } from 'lodash';
|
||||
import { Map } from 'immutable';
|
||||
import uuid from 'uuid/v4';
|
||||
import {
|
||||
MEDIA_LIBRARY_OPEN,
|
||||
MEDIA_LIBRARY_CLOSE,
|
||||
MEDIA_LIBRARY_CREATE,
|
||||
MEDIA_INSERT,
|
||||
MEDIA_REMOVE_INSERTED,
|
||||
MEDIA_LOAD_REQUEST,
|
||||
@ -20,16 +20,23 @@ import {
|
||||
MEDIA_DISPLAY_URL_FAILURE,
|
||||
} from 'Actions/mediaLibrary';
|
||||
|
||||
const mediaLibrary = (
|
||||
state = Map({ isVisible: false, controlMedia: Map(), displayURLs: Map() }),
|
||||
action,
|
||||
) => {
|
||||
const privateUploadChanged =
|
||||
state.get('privateUpload') !== get(action, ['payload', 'privateUpload']);
|
||||
let displayURLPath;
|
||||
const defaultState = {
|
||||
isVisible: false,
|
||||
showMediaButton: true,
|
||||
controlMedia: Map(),
|
||||
displayURLs: Map(),
|
||||
};
|
||||
|
||||
const mediaLibrary = (state = Map(defaultState), action) => {
|
||||
switch (action.type) {
|
||||
case MEDIA_LIBRARY_CREATE:
|
||||
return state.withMutations(map => {
|
||||
map.set('externalLibrary', action.payload);
|
||||
map.set('showMediaButton', action.payload.enableStandalone());
|
||||
});
|
||||
case MEDIA_LIBRARY_OPEN: {
|
||||
const { controlID, forImage, privateUpload } = action.payload || {};
|
||||
const { controlID, forImage, privateUpload } = action.payload;
|
||||
const privateUploadChanged = state.get('privateUpload') !== privateUpload;
|
||||
if (privateUploadChanged) {
|
||||
return Map({
|
||||
isVisible: true,
|
||||
@ -51,12 +58,14 @@ const mediaLibrary = (
|
||||
case MEDIA_LIBRARY_CLOSE:
|
||||
return state.set('isVisible', false);
|
||||
case MEDIA_INSERT: {
|
||||
const { mediaPath } = action.payload;
|
||||
const controlID = state.get('controlID');
|
||||
const mediaPath = get(action, ['payload', 'mediaPath']);
|
||||
return state.setIn(['controlMedia', controlID], mediaPath);
|
||||
return state.withMutations(map => {
|
||||
map.setIn(['controlMedia', controlID], mediaPath);
|
||||
});
|
||||
}
|
||||
case MEDIA_REMOVE_INSERTED: {
|
||||
const controlID = get(action, ['payload', 'controlID']);
|
||||
const controlID = action.payload.controlID;
|
||||
return state.setIn(['controlMedia', controlID], '');
|
||||
}
|
||||
case MEDIA_LOAD_REQUEST:
|
||||
@ -65,7 +74,15 @@ const mediaLibrary = (
|
||||
map.set('isPaginating', action.payload.page > 1);
|
||||
});
|
||||
case MEDIA_LOAD_SUCCESS: {
|
||||
const { files = [], page, canPaginate, dynamicSearch, dynamicSearchQuery } = action.payload;
|
||||
const {
|
||||
files = [],
|
||||
page,
|
||||
canPaginate,
|
||||
dynamicSearch,
|
||||
dynamicSearchQuery,
|
||||
privateUpload,
|
||||
} = action.payload;
|
||||
const privateUploadChanged = state.get('privateUpload') !== privateUpload;
|
||||
|
||||
if (privateUploadChanged) {
|
||||
return state;
|
||||
@ -88,15 +105,18 @@ const mediaLibrary = (
|
||||
}
|
||||
});
|
||||
}
|
||||
case MEDIA_LOAD_FAILURE:
|
||||
case MEDIA_LOAD_FAILURE: {
|
||||
const privateUploadChanged = state.get('privateUpload') !== action.payload.privateUpload;
|
||||
if (privateUploadChanged) {
|
||||
return state;
|
||||
}
|
||||
return state.set('isLoading', false);
|
||||
}
|
||||
case MEDIA_PERSIST_REQUEST:
|
||||
return state.set('isPersisting', true);
|
||||
case MEDIA_PERSIST_SUCCESS: {
|
||||
const { file } = action.payload;
|
||||
const { file, privateUpload } = action.payload;
|
||||
const privateUploadChanged = state.get('privateUpload') !== privateUpload;
|
||||
if (privateUploadChanged) {
|
||||
return state;
|
||||
}
|
||||
@ -107,15 +127,18 @@ const mediaLibrary = (
|
||||
map.set('isPersisting', false);
|
||||
});
|
||||
}
|
||||
case MEDIA_PERSIST_FAILURE:
|
||||
case MEDIA_PERSIST_FAILURE: {
|
||||
const privateUploadChanged = state.get('privateUpload') !== action.payload.privateUpload;
|
||||
if (privateUploadChanged) {
|
||||
return state;
|
||||
}
|
||||
return state.set('isPersisting', false);
|
||||
}
|
||||
case MEDIA_DELETE_REQUEST:
|
||||
return state.set('isDeleting', true);
|
||||
case MEDIA_DELETE_SUCCESS: {
|
||||
const { id, key } = action.payload.file;
|
||||
const { id, key, privateUpload } = action.payload.file;
|
||||
const privateUploadChanged = state.get('privateUpload') !== privateUpload;
|
||||
if (privateUploadChanged) {
|
||||
return state;
|
||||
}
|
||||
@ -126,28 +149,31 @@ const mediaLibrary = (
|
||||
map.set('isDeleting', false);
|
||||
});
|
||||
}
|
||||
case MEDIA_DELETE_FAILURE:
|
||||
case MEDIA_DELETE_FAILURE: {
|
||||
const privateUploadChanged = state.get('privateUpload') !== action.payload.privateUpload;
|
||||
if (privateUploadChanged) {
|
||||
return state;
|
||||
}
|
||||
return state.set('isDeleting', false);
|
||||
}
|
||||
|
||||
case MEDIA_DISPLAY_URL_REQUEST:
|
||||
return state.setIn(['displayURLs', action.payload.key, 'isFetching'], true);
|
||||
|
||||
case MEDIA_DISPLAY_URL_SUCCESS:
|
||||
displayURLPath = ['displayURLs', action.payload.key];
|
||||
case MEDIA_DISPLAY_URL_SUCCESS: {
|
||||
const displayURLPath = ['displayURLs', action.payload.key];
|
||||
return state
|
||||
.setIn([...displayURLPath, 'isFetching'], false)
|
||||
.setIn([...displayURLPath, 'url'], action.payload.url);
|
||||
}
|
||||
|
||||
case MEDIA_DISPLAY_URL_FAILURE:
|
||||
displayURLPath = ['displayURLs', action.payload.key];
|
||||
case MEDIA_DISPLAY_URL_FAILURE: {
|
||||
const displayURLPath = ['displayURLs', action.payload.key];
|
||||
return state
|
||||
.setIn([...displayURLPath, 'isFetching'], false)
|
||||
.setIn([...displayURLPath, 'err'], action.payload.err)
|
||||
.deleteIn([...displayURLPath, 'url']);
|
||||
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -1,17 +0,0 @@
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import waitUntilAction from './middleware/waitUntilAction';
|
||||
import reducer from 'Reducers/combinedReducer';
|
||||
|
||||
export default function configureStore(initialState) {
|
||||
const store = createStore(
|
||||
reducer,
|
||||
initialState,
|
||||
compose(
|
||||
applyMiddleware(thunkMiddleware, waitUntilAction),
|
||||
window.devToolsExtension ? window.devToolsExtension() : f => f,
|
||||
),
|
||||
);
|
||||
|
||||
return store;
|
||||
}
|
14
packages/netlify-cms-core/src/redux/index.js
Normal file
14
packages/netlify-cms-core/src/redux/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import waitUntilAction from './middleware/waitUntilAction';
|
||||
import reducer from 'Reducers/combinedReducer';
|
||||
|
||||
const store = createStore(
|
||||
reducer,
|
||||
compose(
|
||||
applyMiddleware(thunkMiddleware, waitUntilAction),
|
||||
window.devToolsExtension ? window.devToolsExtension() : f => f,
|
||||
),
|
||||
);
|
||||
|
||||
export default store;
|
@ -1,13 +1,9 @@
|
||||
import { resolvePath } from 'netlify-cms-lib-util';
|
||||
import { currentBackend } from 'src/backend';
|
||||
import store from 'Redux';
|
||||
import { getIntegrationProvider } from 'Integrations';
|
||||
import { selectIntegration } from 'Reducers';
|
||||
|
||||
let store;
|
||||
export const setStore = storeObj => {
|
||||
store = storeObj;
|
||||
};
|
||||
|
||||
export default function AssetProxy(value, fileObj, uploaded = false, asset) {
|
||||
const config = store.getState().config;
|
||||
this.value = value;
|
||||
|
@ -6,3 +6,4 @@ export { resolvePath, basename, fileExtensionWithSeparator, fileExtension } from
|
||||
export { filterPromises, resolvePromiseProperties, then } from './promise';
|
||||
export unsentRequest from './unsentRequest';
|
||||
export { filterByPropExtension, parseResponse, responseParser } from './backendUtil';
|
||||
export loadScript from './loadScript';
|
||||
|
24
packages/netlify-cms-lib-util/src/loadScript.js
Normal file
24
packages/netlify-cms-lib-util/src/loadScript.js
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Simple script loader that returns a promise.
|
||||
*/
|
||||
export default function loadScript(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let done = false;
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
script.onload = script.onreadystatechange = function() {
|
||||
if (
|
||||
!done &&
|
||||
(!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete')
|
||||
) {
|
||||
done = true;
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
};
|
||||
script.onerror = error => reject(error);
|
||||
head.appendChild(script);
|
||||
});
|
||||
}
|
11
packages/netlify-cms-media-library-uploadcare/README.md
Normal file
11
packages/netlify-cms-media-library-uploadcare/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Docs coming soon!
|
||||
|
||||
Netlify CMS was recently converted from a single npm package to a "monorepo" of over 20 packages.
|
||||
That's over 20 Readme's! We haven't created one for this package yet, but we will soon.
|
||||
|
||||
In the meantime, you can:
|
||||
|
||||
1. Check out the [main readme](https://github.com/netlify/netlify-cms/#readme) or the [documentation
|
||||
site](https://www.netlifycms.org) for more info.
|
||||
2. Reach out to the [community chat](https://gitter.im/netlify/netlifycms/) if you need help.
|
||||
3. Help out and [write the readme yourself](https://github.com/netlify/netlify-cms/edit/master/packages/netlify-cms-media-library-uploadcare/README.md)!
|
33
packages/netlify-cms-media-library-uploadcare/package.json
Normal file
33
packages/netlify-cms-media-library-uploadcare/package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "netlify-cms-media-library-uploadcare",
|
||||
"description": "Uploadcare integration for Netlify CMS",
|
||||
"version": "0.1.0",
|
||||
"repository": "https://github.com/netlify/netlify-cms/tree/master/packages/netlify-cms-media-library-uploadcare",
|
||||
"bugs": "https://github.com/netlify/netlify-cms/issues",
|
||||
"main": "dist/netlify-cms-media-library-uploadcare.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"netlify",
|
||||
"netlify-cms",
|
||||
"uploadcare",
|
||||
"media",
|
||||
"assets",
|
||||
"files",
|
||||
"uploads"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"watch": "webpack -w",
|
||||
"develop": "npm run watch",
|
||||
"build": "cross-env NODE_ENV=production webpack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^5.2.0",
|
||||
"webpack": "^4.16.1",
|
||||
"webpack-cli": "^3.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"netlify-cms-lib-util": "^2.0.4"
|
||||
},
|
||||
"private": true
|
||||
}
|
146
packages/netlify-cms-media-library-uploadcare/src/index.js
Normal file
146
packages/netlify-cms-media-library-uploadcare/src/index.js
Normal file
@ -0,0 +1,146 @@
|
||||
import { loadScript } from 'netlify-cms-lib-util';
|
||||
|
||||
/**
|
||||
* Default Uploadcare widget configuration, can be overriden via config.yml.
|
||||
*/
|
||||
const defaultConfig = {
|
||||
previewStep: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine whether an array of urls represents an unaltered set of Uploadcare
|
||||
* group urls. If they've been changed or any are missing, a new group will need
|
||||
* to be created to represent the current values.
|
||||
*/
|
||||
function isFileGroup(files) {
|
||||
const basePatternString = `~${files.length}/nth/`;
|
||||
const mapExpression = (val, idx) => new RegExp(`${basePatternString}${idx}/$`);
|
||||
const expressions = Array.from({ length: files.length }, mapExpression);
|
||||
return expressions.every(exp => files.some(url => exp.test(url)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fileGroupInfo object wrapped in a promise-like object.
|
||||
*/
|
||||
function getFileGroup(files) {
|
||||
/**
|
||||
* Capture the group id from the first file in the files array.
|
||||
*/
|
||||
const groupId = new RegExp(`^.+/([^/]+~${files.length})/nth/`).exec(files[0])[1];
|
||||
|
||||
/**
|
||||
* The `openDialog` method handles the jQuery promise object returned by
|
||||
* `fileFrom`, but requires the promise returned by `loadFileGroup` to provide
|
||||
* the result of it's `done` method.
|
||||
*/
|
||||
return new Promise(resolve =>
|
||||
window.uploadcare.loadFileGroup(groupId).done(group => resolve(group)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a url or array/List of urls to Uploadcare file objects wrapped in
|
||||
* promises, or Uploadcare groups when possible. Output is wrapped in a promise
|
||||
* because the value we're returning may be a promise that we created.
|
||||
*/
|
||||
function getFiles(value, cdnBase) {
|
||||
if (typeof value === 'object') {
|
||||
const arr = Array.isArray(value) ? value : value.toJS();
|
||||
return isFileGroup(arr) ? getFileGroup(arr) : arr.map(val => getFile(val, cdnBase));
|
||||
}
|
||||
return value && typeof value === 'string' ? getFile(value, cdnBase) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a single url to an Uploadcare file object wrapped in a promise-like
|
||||
* object. Group urls that get passed here were not a part of a complete and
|
||||
* untouched group, so they'll be uploaded as new images (only way to do it).
|
||||
*/
|
||||
function getFile(url, cdnBase) {
|
||||
const groupPattern = /~\d+\/nth\/\d+\//;
|
||||
const baseUrls = ['https://ucarecdn.com', cdnBase].filter(v => v);
|
||||
const uploaded = baseUrls.some(baseUrl => url.startsWith(baseUrl) && !groupPattern.test(url));
|
||||
return window.uploadcare.fileFrom(uploaded ? 'uploaded' : 'url', url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the standalone dialog. A single instance is created and destroyed for
|
||||
* each use.
|
||||
*/
|
||||
function openDialog(files, config, handleInsert) {
|
||||
window.uploadcare.openDialog(files, config).done(({ promise }) =>
|
||||
promise().then(({ cdnUrl, count }) => {
|
||||
if (config.multiple) {
|
||||
const urls = Array.from({ length: count }, (val, idx) => `${cdnUrl}nth/${idx}/`);
|
||||
handleInsert(urls);
|
||||
} else {
|
||||
handleInsert(cdnUrl);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization function will only run once, returns an API object for Netlify
|
||||
* CMS to call methods on.
|
||||
*/
|
||||
async function init({ options = { config: {} }, handleInsert }) {
|
||||
const { publicKey, ...globalConfig } = options.config;
|
||||
const baseConfig = { ...defaultConfig, ...globalConfig };
|
||||
|
||||
window.UPLOADCARE_LIVE = false;
|
||||
window.UPLOADCARE_MANUAL_START = true;
|
||||
window.UPLOADCARE_PUBLIC_KEY = publicKey;
|
||||
|
||||
/**
|
||||
* Loading scripts via url because the uploadcare widget includes
|
||||
* non-strict-mode code that's incompatible with our build system
|
||||
*/
|
||||
await loadScript('https://unpkg.com/uploadcare-widget@^3.6.0/uploadcare.full.js');
|
||||
await loadScript(
|
||||
'https://unpkg.com/uploadcare-widget-tab-effects@^1.2.1/dist/uploadcare.tab-effects.js',
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the effects tab by default because the effects tab is awesome. Can
|
||||
* be disabled via config.
|
||||
*/
|
||||
window.uploadcare.registerTab('preview', window.uploadcareTabEffects);
|
||||
|
||||
return {
|
||||
/**
|
||||
* On show, create a new widget, cache it in the widgets object, and open.
|
||||
* No hide method is provided because the widget doesn't provide it.
|
||||
*/
|
||||
show: ({ value, config: instanceConfig = {}, imagesOnly }) => {
|
||||
const config = { ...baseConfig, imagesOnly, ...instanceConfig };
|
||||
const files = getFiles(value);
|
||||
|
||||
/**
|
||||
* Resolve the promise only if it's ours. Only the jQuery promise objects
|
||||
* from the Uploadcare library will have a `state` method.
|
||||
*/
|
||||
if (files && !files.state) {
|
||||
files.then(result => openDialog(result, config, handleInsert));
|
||||
} else {
|
||||
openDialog(files, config, handleInsert);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Uploadcare doesn't provide a "media library" widget for viewing and
|
||||
* selecting existing files, so we return `false` here so Netlify CMS only
|
||||
* opens the Uploadcare widget when called from an editor control. This
|
||||
* results in the "Media" button in the global nav being hidden.
|
||||
*/
|
||||
enableStandalone: () => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The object that will be registered only needs a (default) name and `init`
|
||||
* method. The `init` method returns the API object.
|
||||
*/
|
||||
const uploadcareMediaLibrary = { name: 'uploadcare', init };
|
||||
|
||||
export default uploadcareMediaLibrary;
|
@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
@ -31,6 +31,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"emotion": "^9.2.6",
|
||||
"immutable": "^3.7.6",
|
||||
"netlify-cms-ui-default": "^2.0.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.4.1",
|
||||
|
@ -1,11 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'react-emotion';
|
||||
import { List } from 'immutable';
|
||||
import { WidgetPreviewContainer } from 'netlify-cms-ui-default';
|
||||
|
||||
const FilePreview = ({ value, getAsset }) => (
|
||||
<WidgetPreviewContainer>
|
||||
{value ? <a href={getAsset(value)}>{value}</a> : null}
|
||||
</WidgetPreviewContainer>
|
||||
const FileLink = styled(({ value, getAsset }) => (
|
||||
<a href={getAsset(value)} rel="noopener noreferrer" target="_blank">
|
||||
{value}
|
||||
</a>
|
||||
))`
|
||||
display: block;
|
||||
`;
|
||||
|
||||
function FileLinkList({ values, getAsset }) {
|
||||
return (
|
||||
<div>
|
||||
{values.map(value => (
|
||||
<FileLink key={value} value={value} getAsset={getAsset} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FileContent({ value, getAsset }) {
|
||||
if (Array.isArray(value) || List.isList(value)) {
|
||||
return <FileLinkList values={value} getAsset={getAsset} />;
|
||||
}
|
||||
return <FileLink value={value} getAsset={getAsset} />;
|
||||
}
|
||||
|
||||
const FilePreview = props => (
|
||||
<WidgetPreviewContainer>{props.value ? <FileContent {...props} /> : null}</WidgetPreviewContainer>
|
||||
);
|
||||
|
||||
FilePreview.propTypes = {
|
||||
|
@ -2,19 +2,18 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled from 'react-emotion';
|
||||
import { List } from 'immutable';
|
||||
import uuid from 'uuid/v4';
|
||||
import { lengths, components, buttons } from 'netlify-cms-ui-default';
|
||||
|
||||
const MAX_DISPLAY_LENGTH = 50;
|
||||
|
||||
const FileContent = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const ImageWrapper = styled.div`
|
||||
flex-basis: 155px;
|
||||
width: 155px;
|
||||
height: 100px;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const Image = styled.img`
|
||||
@ -24,16 +23,31 @@ const Image = styled.img`
|
||||
border-radius: ${lengths.borderRadius};
|
||||
`;
|
||||
|
||||
const MultiImageWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const FileInfo = styled.div`
|
||||
button:not(:first-child) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FileName = styled.span`
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
const FileLink = styled.a`
|
||||
margin-bottom: 20px;
|
||||
font-weight: normal;
|
||||
color: inherit;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const FileLinkList = styled.ul`
|
||||
list-style-type: none;
|
||||
`;
|
||||
|
||||
const FileWidgetButton = styled.button`
|
||||
@ -46,6 +60,10 @@ const FileWidgetButtonRemove = styled.button`
|
||||
${components.badgeDanger};
|
||||
`;
|
||||
|
||||
function isMultiple(value) {
|
||||
return Array.isArray(value) || List.isList(value);
|
||||
}
|
||||
|
||||
export default function withFileControl({ forImage } = {}) {
|
||||
return class FileControl extends React.Component {
|
||||
static propTypes = {
|
||||
@ -56,8 +74,10 @@ export default function withFileControl({ forImage } = {}) {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemoveInsertedMedia: PropTypes.func.isRequired,
|
||||
onOpenMediaLibrary: PropTypes.func.isRequired,
|
||||
onClearMediaControl: PropTypes.func.isRequired,
|
||||
onRemoveMediaControl: PropTypes.func.isRequired,
|
||||
classNameWrapper: PropTypes.string.isRequired,
|
||||
value: PropTypes.node,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -99,56 +119,94 @@ export default function withFileControl({ forImage } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.onRemoveMediaControl(this.controlID);
|
||||
}
|
||||
|
||||
handleChange = e => {
|
||||
const { field, onOpenMediaLibrary } = this.props;
|
||||
const { field, onOpenMediaLibrary, value } = this.props;
|
||||
e.preventDefault();
|
||||
return onOpenMediaLibrary({
|
||||
controlID: this.controlID,
|
||||
forImage,
|
||||
privateUpload: field.get('private'),
|
||||
value,
|
||||
config: field.getIn(['options', 'media_library', 'config']),
|
||||
});
|
||||
};
|
||||
|
||||
handleRemove = e => {
|
||||
e.preventDefault();
|
||||
this.props.onClearMediaControl(this.controlID);
|
||||
return this.props.onChange('');
|
||||
};
|
||||
|
||||
renderFileName = () => {
|
||||
const { value } = this.props;
|
||||
renderFileLink = value => {
|
||||
const size = MAX_DISPLAY_LENGTH;
|
||||
if (!value || value.length <= size) {
|
||||
return value;
|
||||
}
|
||||
return `${value.substring(0, size / 2)}\u2026${value.substring(
|
||||
const text = `${value.substring(0, size / 2)}\u2026${value.substring(
|
||||
value.length - size / 2 + 1,
|
||||
value.length,
|
||||
)}`;
|
||||
};
|
||||
|
||||
renderSelection = subject => {
|
||||
const fileName = this.renderFileName();
|
||||
const { getAsset, value } = this.props;
|
||||
return (
|
||||
<FileContent>
|
||||
{forImage ? (
|
||||
<ImageWrapper>
|
||||
<Image src={getAsset(value)} />
|
||||
</ImageWrapper>
|
||||
) : null}
|
||||
<FileInfo>
|
||||
<FileName>{fileName}</FileName>
|
||||
<FileWidgetButton onClick={this.handleChange}>
|
||||
Choose different {subject}
|
||||
</FileWidgetButton>
|
||||
<FileWidgetButtonRemove onClick={this.handleRemove}>
|
||||
Remove {subject}
|
||||
</FileWidgetButtonRemove>
|
||||
</FileInfo>
|
||||
</FileContent>
|
||||
<FileLink href={value} rel="noopener" target="_blank">
|
||||
{text}
|
||||
</FileLink>
|
||||
);
|
||||
};
|
||||
|
||||
renderFileLinks = () => {
|
||||
const { value } = this.props;
|
||||
|
||||
if (isMultiple(value)) {
|
||||
return (
|
||||
<FileLinkList>
|
||||
{value.map(val => (
|
||||
<li key={val}>{this.renderFileLink(val)}</li>
|
||||
))}
|
||||
</FileLinkList>
|
||||
);
|
||||
}
|
||||
return this.renderFileLink(value);
|
||||
};
|
||||
|
||||
renderImages = () => {
|
||||
const { getAsset, value } = this.props;
|
||||
if (isMultiple(value)) {
|
||||
return (
|
||||
<MultiImageWrapper>
|
||||
{value.map(val => (
|
||||
<ImageWrapper key={val}>
|
||||
<Image src={getAsset(val)} />
|
||||
</ImageWrapper>
|
||||
))}
|
||||
</MultiImageWrapper>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ImageWrapper>
|
||||
<Image src={getAsset(value)} />
|
||||
</ImageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
renderSelection = subject => (
|
||||
<div>
|
||||
{forImage ? this.renderImages() : null}
|
||||
<FileInfo>
|
||||
{forImage ? null : this.renderFileLinks()}
|
||||
<FileWidgetButton onClick={this.handleChange}>
|
||||
Choose different {subject}
|
||||
</FileWidgetButton>
|
||||
<FileWidgetButtonRemove onClick={this.handleRemove}>
|
||||
Remove {subject}
|
||||
</FileWidgetButtonRemove>
|
||||
</FileInfo>
|
||||
</div>
|
||||
);
|
||||
|
||||
renderNoSelection = (subject, article) => (
|
||||
<FileWidgetButton onClick={this.handleChange}>
|
||||
Choose {article} {subject}
|
||||
|
@ -30,6 +30,7 @@
|
||||
"webpack-cli": "^3.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"immutable": "^3.7.6",
|
||||
"netlify-cms-ui-default": "^2.0.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.4.1",
|
||||
|
@ -1,18 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'react-emotion';
|
||||
import { List } from 'immutable';
|
||||
import { WidgetPreviewContainer } from 'netlify-cms-ui-default';
|
||||
|
||||
const Image = styled.img`
|
||||
const StyledImage = styled(({ getAsset, value }) => (
|
||||
<img src={getAsset(value)} role="presentation" />
|
||||
))`
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
`;
|
||||
|
||||
const ImagePreview = ({ value, getAsset }) => (
|
||||
<WidgetPreviewContainer>
|
||||
{value ? <Image src={getAsset(value)} role="presentation" /> : null}
|
||||
</WidgetPreviewContainer>
|
||||
);
|
||||
const ImagePreviewContent = props => {
|
||||
const { value, getAsset } = props;
|
||||
if (Array.isArray(value) || List.isList(value)) {
|
||||
return value.map(val => <StyledImage key={val} value={val} getAsset={getAsset} />);
|
||||
}
|
||||
return <StyledImage {...props} />;
|
||||
};
|
||||
|
||||
const ImagePreview = props => {
|
||||
return (
|
||||
<WidgetPreviewContainer>
|
||||
{props.value ? <ImagePreviewContent {...props} /> : null}
|
||||
</WidgetPreviewContainer>
|
||||
);
|
||||
};
|
||||
|
||||
ImagePreview.propTypes = {
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
|
@ -2,5 +2,6 @@ import CMS, { init } from 'netlify-cms-core/src';
|
||||
import './backends';
|
||||
import './widgets';
|
||||
import './editor-components';
|
||||
import './media-libraries';
|
||||
|
||||
export { CMS as default, init };
|
||||
|
6
packages/netlify-cms/src/media-libraries.js
Normal file
6
packages/netlify-cms/src/media-libraries.js
Normal file
@ -0,0 +1,6 @@
|
||||
import cms from 'netlify-cms-core/src';
|
||||
import uploadcare from 'netlify-cms-media-library-uploadcare/src';
|
||||
|
||||
const { registerMediaLibrary } = cms;
|
||||
|
||||
registerMediaLibrary(uploadcare);
|
Reference in New Issue
Block a user