Skip to content

Commit 1fc2f50

Browse files
authored
feat: add cloudinary support (#1932)
1 parent 9662eb2 commit 1fc2f50

File tree

15 files changed

+281
-17
lines changed

15 files changed

+281
-17
lines changed

packages/netlify-cms-core/src/actions/mediaLibrary.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ export function openMediaLibrary(payload = {}) {
6363
const state = getState();
6464
const mediaLibrary = state.mediaLibrary.get('externalLibrary');
6565
if (mediaLibrary) {
66-
const { controlID: id, value, config = Map(), forImage } = payload;
67-
mediaLibrary.show({ id, value, config: config.toJS(), imagesOnly: forImage });
66+
const { controlID: id, value, config = Map(), allowMultiple, forImage } = payload;
67+
mediaLibrary.show({ id, value, config: config.toJS(), allowMultiple, imagesOnly: forImage });
6868
}
6969
dispatch({ type: MEDIA_LIBRARY_OPEN, payload });
7070
};

packages/netlify-cms-core/src/reducers/index.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,12 @@ export const selectUnpublishedEntriesByStatus = (state, status) =>
5656
export const selectIntegration = (state, collection, hook) =>
5757
fromIntegrations.selectIntegration(state.integrations, collection, hook);
5858

59-
export const getAsset = (state, path) =>
60-
fromMedias.getAsset(state.config.get('public_folder'), state.medias, path);
59+
export const getAsset = (state, path) => {
60+
/**
61+
* If an external media library is in use, just return the path.
62+
*/
63+
if (state.mediaLibrary.get('externalLibrary')) {
64+
return path;
65+
}
66+
return fromMedias.getAsset(state.config.get('public_folder'), state.medias, path);
67+
};

packages/netlify-cms-editor-component-image/src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ const image = {
1717
label: 'Image',
1818
name: 'image',
1919
widget: 'image',
20+
media_library: {
21+
allow_multiple: false,
22+
},
2023
},
2124
{
2225
label: 'Alt Text',
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Docs coming soon!
2+
3+
Netlify CMS was recently converted from a single npm package to a "monorepo" of over 20 packages.
4+
That's over 20 Readme's! We haven't created one for this package yet, but we will soon.
5+
6+
In the meantime, you can:
7+
8+
1. Check out the [main readme](https://github.com/netlify/netlify-cms/#readme) or the [documentation
9+
site](https://www.netlifycms.org) for more info.
10+
2. Reach out to the [community chat](https://gitter.im/netlify/netlifycms/) if you need help.
11+
3. Help out and [write the readme yourself](https://github.com/netlify/netlify-cms/edit/master/packages/netlify-cms-media-library-cloudinary/README.md)!
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "netlify-cms-media-library-cloudinary",
3+
"description": "Cloudinary integration for Netlify CMS",
4+
"version": "1.0.0",
5+
"repository": "https://github.com/netlify/netlify-cms/tree/master/packages/netlify-cms-media-library-cloudinary",
6+
"bugs": "https://github.com/netlify/netlify-cms/issues",
7+
"main": "dist/netlify-cms-media-library-cloudinary.js",
8+
"license": "MIT",
9+
"keywords": [
10+
"netlify",
11+
"netlify-cms",
12+
"cloudinary",
13+
"image",
14+
"images",
15+
"media",
16+
"assets",
17+
"files",
18+
"uploads"
19+
],
20+
"sideEffects": false,
21+
"scripts": {
22+
"watch": "webpack -w",
23+
"develop": "npm run watch",
24+
"build": "cross-env NODE_ENV=production webpack"
25+
},
26+
"devDependencies": {
27+
"cross-env": "^5.2.0",
28+
"webpack": "^4.16.1",
29+
"webpack-cli": "^3.1.0"
30+
},
31+
"peerDependencies": {
32+
"netlify-cms-lib-util": "^2.0.4"
33+
},
34+
"dependencies": {}
35+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { pick } from 'lodash';
2+
import { loadScript } from 'netlify-cms-lib-util';
3+
4+
const defaultOptions = {
5+
use_secure_url: true,
6+
use_transformations: true,
7+
output_filename_only: false,
8+
};
9+
/**
10+
* This configuration hash cannot be overriden, as the values here are required
11+
* for the integration to work properly.
12+
*/
13+
const enforcedConfig = {
14+
button_class: undefined,
15+
inline_container: undefined,
16+
insert_transformation: false,
17+
z_index: '99999',
18+
};
19+
20+
const defaultConfig = {
21+
multiple: false,
22+
};
23+
24+
function getAssetUrl(asset, { use_secure_url, use_transformations, output_filename_only }) {
25+
/**
26+
* Allow output of the file name only, in which case the rest of the url (including)
27+
* transformations) can be handled by the static site generator.
28+
*/
29+
if (output_filename_only) {
30+
return `${asset.public_id}.${asset.format}`;
31+
}
32+
33+
/**
34+
* Get url from `derived` property if it exists. This property contains the
35+
* transformed version of image if transformations have been applied.
36+
*/
37+
const urlObject = asset.derived && use_transformations ? asset.derived[0] : asset;
38+
39+
/**
40+
* Retrieve the `https` variant of the image url if the `useSecureUrl` option
41+
* is set to `true` (this is the default setting).
42+
*/
43+
const urlKey = use_secure_url ? 'secure_url' : 'url';
44+
45+
return urlObject[urlKey];
46+
}
47+
48+
async function init({ options, handleInsert }) {
49+
const { config: providedConfig = {}, ...integrationOptions } = options;
50+
const resolvedOptions = { ...defaultOptions, ...integrationOptions };
51+
const cloudinaryConfig = { ...defaultConfig, ...providedConfig, ...enforcedConfig };
52+
const cloudinaryBehaviorConfigKeys = ['default_transformations', 'max_files', 'multiple'];
53+
const cloudinaryBehaviorConfig = pick(cloudinaryConfig, cloudinaryBehaviorConfigKeys);
54+
55+
await loadScript('https://media-library.cloudinary.com/global/all.js');
56+
57+
const insertHandler = data => {
58+
const assets = data.assets.map(asset => getAssetUrl(asset, resolvedOptions));
59+
handleInsert(cloudinaryConfig.multiple ? assets : assets[0]);
60+
};
61+
62+
const mediaLibrary = window.cloudinary.createMediaLibrary(cloudinaryConfig, { insertHandler });
63+
64+
return {
65+
show: ({ config: instanceConfig = {}, allowMultiple }) => {
66+
/**
67+
* Ensure multiple selection is not available if the field is configured
68+
* to disallow it.
69+
*/
70+
if (allowMultiple === false) {
71+
instanceConfig.multiple = false;
72+
}
73+
return mediaLibrary.show({ config: { ...cloudinaryBehaviorConfig, instanceConfig } });
74+
},
75+
hide: () => mediaLibrary.hide(),
76+
enableStandalone: () => true,
77+
};
78+
}
79+
80+
const cloudinaryMediaLibrary = { name: 'cloudinary', init };
81+
82+
export default cloudinaryMediaLibrary;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const { getConfig } = require('../../scripts/webpack.js');
2+
3+
module.exports = getConfig();

packages/netlify-cms-media-library-uploadcare/src/index.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,20 @@ async function init({ options = { config: {} }, handleInsert }) {
113113
* On show, create a new widget, cache it in the widgets object, and open.
114114
* No hide method is provided because the widget doesn't provide it.
115115
*/
116-
show: ({ value, config: instanceConfig = {}, imagesOnly }) => {
116+
show: ({ value, config: instanceConfig = {}, allowMultiple, imagesOnly }) => {
117117
const config = { ...baseConfig, imagesOnly, ...instanceConfig };
118+
const multiple = allowMultiple === false ? false : !!config.multiple;
119+
const resolvedConfig = { ...config, multiple };
118120
const files = getFiles(value);
119121

120122
/**
121123
* Resolve the promise only if it's ours. Only the jQuery promise objects
122124
* from the Uploadcare library will have a `state` method.
123125
*/
124126
if (files && !files.state) {
125-
files.then(result => openDialog(result, config, handleInsert));
127+
files.then(result => openDialog(result, resolvedConfig, handleInsert));
126128
} else {
127-
openDialog(files, config, handleInsert);
129+
openDialog(files, resolvedConfig, handleInsert);
128130
}
129131
},
130132

packages/netlify-cms-widget-file/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"build": "cross-env NODE_ENV=production webpack"
2323
},
2424
"dependencies": {
25+
"common-tags": "^1.8.0",
2526
"uuid": "^3.3.2"
2627
},
2728
"devDependencies": {

packages/netlify-cms-widget-file/src/withFileControl.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import React from 'react';
22
import PropTypes from 'prop-types';
33
import ImmutablePropTypes from 'react-immutable-proptypes';
44
import styled from 'react-emotion';
5-
import { List } from 'immutable';
5+
import { Map, List } from 'immutable';
6+
import { once } from 'lodash';
67
import uuid from 'uuid/v4';
8+
import { oneLine } from 'common-tags';
79
import { lengths, components, buttons } from 'netlify-cms-ui-default';
810

911
const MAX_DISPLAY_LENGTH = 50;
@@ -64,6 +66,15 @@ function isMultiple(value) {
6466
return Array.isArray(value) || List.isList(value);
6567
}
6668

69+
const warnDeprecatedOptions = once(field =>
70+
console.warn(oneLine`
71+
Netlify CMS config: ${field.get('name')} field: property "options" has been deprecated for the
72+
${field.get('widget')} widget and will be removed in the next major release. Rather than
73+
\`field.options.media_library\`, apply media library options for this widget under
74+
\`field.media_library\`.
75+
`),
76+
);
77+
6778
export default function withFileControl({ forImage } = {}) {
6879
return class FileControl extends React.Component {
6980
static propTypes = {
@@ -126,12 +137,28 @@ export default function withFileControl({ forImage } = {}) {
126137
handleChange = e => {
127138
const { field, onOpenMediaLibrary, value } = this.props;
128139
e.preventDefault();
140+
let mediaLibraryFieldOptions;
141+
142+
/**
143+
* `options` hash as a general field property is deprecated, only used
144+
* when external media libraries were first introduced. Not to be
145+
* confused with `options` for the select widget, which serves a different
146+
* purpose.
147+
*/
148+
if (field.hasIn(['options', 'media_library'])) {
149+
warnDeprecatedOptions(field);
150+
mediaLibraryFieldOptions = field.getIn(['options', 'media_library'], Map());
151+
} else {
152+
mediaLibraryFieldOptions = field.get('media_library', Map());
153+
}
154+
129155
return onOpenMediaLibrary({
130156
controlID: this.controlID,
131157
forImage,
132158
privateUpload: field.get('private'),
133159
value,
134-
config: field.getIn(['options', 'media_library', 'config']),
160+
allowMultiple: !!mediaLibraryFieldOptions.get('allow_multiple', true),
161+
config: mediaLibraryFieldOptions.get('config'),
135162
});
136163
};
137164

0 commit comments

Comments
 (0)