Skip to content

Commit

Permalink
feat(media): add external media library support, Uploadcare integrati…
Browse files Browse the repository at this point in the history
…on (#1602)
  • Loading branch information
erquhart committed Aug 30, 2018
1 parent ae28f63 commit 0596904
Show file tree
Hide file tree
Showing 34 changed files with 712 additions and 132 deletions.
9 changes: 7 additions & 2 deletions dev-test/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ backend:
name: test-repo

display_url: https://example.com
media_folder: "assets/uploads"

publish_mode: editorial_workflow
media_folder: assets/uploads

collections: # A list of collections the CMS should be able to edit
- name: "posts" # Used in routes, ie.: /admin/collections/:slug/edit
Expand All @@ -19,7 +19,12 @@ collections: # A list of collections the CMS should be able to edit
fields: # The fields each document in this collection have
- {label: "Title", name: "title", widget: "string", tagname: "h1"}
- {label: "Publish Date", name: "date", widget: "datetime", format: "YYYY-MM-DD hh:mma"}
- {label: "Cover Image", name: "image", widget: "image", required: false, tagname: ""}
- label: "Cover Image"
name: "image"
widget: "image"
required: false
tagname: ""

- {label: "Body", name: "body", widget: "markdown", hint: "Main content goes here."}
meta:
- {label: "SEO Description", name: "description", widget: "text"}
Expand Down
4 changes: 1 addition & 3 deletions dev-test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,10 @@
var PostPreview = createClass({
render: function() {
var entry = this.props.entry;
var image = entry.getIn(['data', 'image']);
var bg = image && this.props.getAsset(image);
return h('div', {},
h('div', {className: "cover"},
h('h1', {}, entry.getIn(['data', 'title'])),
bg ? h('img', {src: bg.toString()}) : null
this.props.widgetFor('image'),
),
h('p', {},
h('small', {}, "Written " + entry.getIn(['data', 'date']))
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ module.exports = {
'netlify-cms-lib-util': '<rootDir>/packages/netlify-cms-lib-util/src/index.js',
'netlify-cms-ui-default': '<rootDir>/packages/netlify-cms-ui-default/src/index.js',
},
testEnvironment: 'node',
testURL: 'http://localhost:8080',
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"clean": "rimraf packages/*/dist dev-test/*.js",
"reset": "npm run clean && lerna clean --yes",
"cache-ci": "node scripts/cache.js",
"test": "run-s jest e2e",
"test": "run-s jest e2e lint",
"test-ci": "run-s cache-ci jest e2e-ci lint-quiet",
"jest": "cross-env NODE_ENV=test jest --no-cache",
"e2e-prep": "npm run build && cp -r packages/netlify-cms/dist dev-test/",
Expand Down
54 changes: 51 additions & 3 deletions packages/netlify-cms-core/src/actions/mediaLibrary.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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) {
Expand Down
14 changes: 2 additions & 12 deletions packages/netlify-cms-core/src/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -61,11 +56,6 @@ function bootstrap(opts = {}) {
store.dispatch(mergeConfig(config));
}

/**
* Pass initial state into AssetProxy factory.
*/
setStore(store);

/**
* Create connected root component.
*/
Expand Down
51 changes: 32 additions & 19 deletions packages/netlify-cms-core/src/components/App/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -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() {
Expand Down Expand Up @@ -127,7 +131,9 @@ class App extends React.Component {
logoutUser,
isFetching,
publishMode,
useMediaLibrary,
openMediaLibrary,
showMediaButton,
} = this.props;

if (config === null) {
Expand Down Expand Up @@ -160,6 +166,7 @@ class App extends React.Component {
openMediaLibrary={openMediaLibrary}
hasWorkflow={hasWorkflow}
displayUrl={config.get('display_url')}
showMediaButton={showMediaButton}
/>
<AppMainContainer>
{isFetching && <TopBarProgress />}
Expand All @@ -180,7 +187,7 @@ class App extends React.Component {
/>
<Route component={NotFoundPage} />
</Switch>
<MediaLibrary />
{useMediaLibrary ? <MediaLibrary /> : null}
</div>
</AppMainContainer>
</div>
Expand All @@ -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,
Expand Down
11 changes: 7 additions & 4 deletions packages/netlify-cms-core/src/components/App/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export default class Header extends React.Component {
openMediaLibrary,
hasWorkflow,
displayUrl,
showMediaButton,
} = this.props;

const createableCollections = collections
Expand All @@ -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 && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -153,6 +158,8 @@ class EditorControl extends React.Component {
boundGetAsset,
onChange,
openMediaLibrary,
clearMediaControl,
removeMediaControl,
addAsset,
removeInsertedMedia,
onValidate,
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -244,6 +253,8 @@ const mapStateToProps = state => ({

const mapDispatchToProps = {
openMediaLibrary,
clearMediaControl,
removeMediaControl,
removeInsertedMedia,
addAsset,
query,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -191,6 +193,8 @@ export default class Widget extends Component {
metadata,
onChange,
onOpenMediaLibrary,
onRemoveMediaControl,
onClearMediaControl,
onAddAsset,
onRemoveInsertedMedia,
getAsset,
Expand Down Expand Up @@ -219,6 +223,8 @@ export default class Widget extends Component {
onChange,
onChangeObject: this.onChangeObject,
onOpenMediaLibrary,
onClearMediaControl,
onRemoveMediaControl,
onAddAsset,
onRemoveInsertedMedia,
getAsset,
Expand Down
Loading

0 comments on commit 0596904

Please sign in to comment.