Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add cloudinary support #1932

Merged
merged 4 commits into from
Dec 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/netlify-cms-core/src/actions/mediaLibrary.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ export function openMediaLibrary(payload = {}) {
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 });
const { controlID: id, value, config = Map(), allowMultiple, forImage } = payload;
mediaLibrary.show({ id, value, config: config.toJS(), allowMultiple, imagesOnly: forImage });
}
dispatch({ type: MEDIA_LIBRARY_OPEN, payload });
};
Expand Down
11 changes: 9 additions & 2 deletions packages/netlify-cms-core/src/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,12 @@ export const selectUnpublishedEntriesByStatus = (state, status) =>
export const selectIntegration = (state, collection, hook) =>
fromIntegrations.selectIntegration(state.integrations, collection, hook);

export const getAsset = (state, path) =>
fromMedias.getAsset(state.config.get('public_folder'), state.medias, path);
export const getAsset = (state, path) => {
/**
* If an external media library is in use, just return the path.
*/
if (state.mediaLibrary.get('externalLibrary')) {
return path;
}
return fromMedias.getAsset(state.config.get('public_folder'), state.medias, path);
};
3 changes: 3 additions & 0 deletions packages/netlify-cms-editor-component-image/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const image = {
label: 'Image',
name: 'image',
widget: 'image',
media_library: {
allow_multiple: false,
},
},
{
label: 'Alt Text',
Expand Down
11 changes: 11 additions & 0 deletions packages/netlify-cms-media-library-cloudinary/README.md
Original file line number Diff line number Diff line change
@@ -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-cloudinary/README.md)!
35 changes: 35 additions & 0 deletions packages/netlify-cms-media-library-cloudinary/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "netlify-cms-media-library-cloudinary",
"description": "Cloudinary integration for Netlify CMS",
"version": "1.0.0",
"repository": "https://github.com/netlify/netlify-cms/tree/master/packages/netlify-cms-media-library-cloudinary",
"bugs": "https://github.com/netlify/netlify-cms/issues",
"main": "dist/netlify-cms-media-library-cloudinary.js",
"license": "MIT",
"keywords": [
"netlify",
"netlify-cms",
"cloudinary",
"image",
"images",
"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"
},
"dependencies": {}
}
82 changes: 82 additions & 0 deletions packages/netlify-cms-media-library-cloudinary/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { pick } from 'lodash';
import { loadScript } from 'netlify-cms-lib-util';

const defaultOptions = {
use_secure_url: true,
use_transformations: true,
output_filename_only: false,
};
/**
* This configuration hash cannot be overriden, as the values here are required
* for the integration to work properly.
*/
const enforcedConfig = {
button_class: undefined,
inline_container: undefined,
insert_transformation: false,
z_index: '99999',
};

const defaultConfig = {
multiple: false,
};

function getAssetUrl(asset, { use_secure_url, use_transformations, output_filename_only }) {
/**
* Allow output of the file name only, in which case the rest of the url (including)
* transformations) can be handled by the static site generator.
*/
if (output_filename_only) {
return `${asset.public_id}.${asset.format}`;
}

/**
* Get url from `derived` property if it exists. This property contains the
* transformed version of image if transformations have been applied.
*/
const urlObject = asset.derived && use_transformations ? asset.derived[0] : asset;

/**
* Retrieve the `https` variant of the image url if the `useSecureUrl` option
* is set to `true` (this is the default setting).
*/
const urlKey = use_secure_url ? 'secure_url' : 'url';

return urlObject[urlKey];
}

async function init({ options, handleInsert }) {
const { config: providedConfig = {}, ...integrationOptions } = options;
const resolvedOptions = { ...defaultOptions, ...integrationOptions };
const cloudinaryConfig = { ...defaultConfig, ...providedConfig, ...enforcedConfig };
const cloudinaryBehaviorConfigKeys = ['default_transformations', 'max_files', 'multiple'];
const cloudinaryBehaviorConfig = pick(cloudinaryConfig, cloudinaryBehaviorConfigKeys);

await loadScript('https://media-library.cloudinary.com/global/all.js');

const insertHandler = data => {
const assets = data.assets.map(asset => getAssetUrl(asset, resolvedOptions));
handleInsert(cloudinaryConfig.multiple ? assets : assets[0]);
};

const mediaLibrary = window.cloudinary.createMediaLibrary(cloudinaryConfig, { insertHandler });

return {
show: ({ config: instanceConfig = {}, allowMultiple }) => {
/**
* Ensure multiple selection is not available if the field is configured
* to disallow it.
*/
if (allowMultiple === false) {
instanceConfig.multiple = false;
}
return mediaLibrary.show({ config: { ...cloudinaryBehaviorConfig, instanceConfig } });
},
hide: () => mediaLibrary.hide(),
enableStandalone: () => true,
};
}

const cloudinaryMediaLibrary = { name: 'cloudinary', init };

export default cloudinaryMediaLibrary;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { getConfig } = require('../../scripts/webpack.js');

module.exports = getConfig();
8 changes: 5 additions & 3 deletions packages/netlify-cms-media-library-uploadcare/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,20 @@ async function init({ options = { config: {} }, handleInsert }) {
* 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 }) => {
show: ({ value, config: instanceConfig = {}, allowMultiple, imagesOnly }) => {
const config = { ...baseConfig, imagesOnly, ...instanceConfig };
const multiple = allowMultiple === false ? false : !!config.multiple;
const resolvedConfig = { ...config, multiple };
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));
files.then(result => openDialog(result, resolvedConfig, handleInsert));
} else {
openDialog(files, config, handleInsert);
openDialog(files, resolvedConfig, handleInsert);
}
},

Expand Down
1 change: 1 addition & 0 deletions packages/netlify-cms-widget-file/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"build": "cross-env NODE_ENV=production webpack"
},
"dependencies": {
"common-tags": "^1.8.0",
"uuid": "^3.3.2"
},
"devDependencies": {
Expand Down
31 changes: 29 additions & 2 deletions packages/netlify-cms-widget-file/src/withFileControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ 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 { Map, List } from 'immutable';
import { once } from 'lodash';
import uuid from 'uuid/v4';
import { oneLine } from 'common-tags';
import { lengths, components, buttons } from 'netlify-cms-ui-default';

const MAX_DISPLAY_LENGTH = 50;
Expand Down Expand Up @@ -64,6 +66,15 @@ function isMultiple(value) {
return Array.isArray(value) || List.isList(value);
}

const warnDeprecatedOptions = once(field =>
console.warn(oneLine`
Netlify CMS config: ${field.get('name')} field: property "options" has been deprecated for the
${field.get('widget')} widget and will be removed in the next major release. Rather than
\`field.options.media_library\`, apply media library options for this widget under
\`field.media_library\`.
`),
);

export default function withFileControl({ forImage } = {}) {
return class FileControl extends React.Component {
static propTypes = {
Expand Down Expand Up @@ -126,12 +137,28 @@ export default function withFileControl({ forImage } = {}) {
handleChange = e => {
const { field, onOpenMediaLibrary, value } = this.props;
e.preventDefault();
let mediaLibraryFieldOptions;

/**
* `options` hash as a general field property is deprecated, only used
* when external media libraries were first introduced. Not to be
* confused with `options` for the select widget, which serves a different
* purpose.
*/
if (field.hasIn(['options', 'media_library'])) {
warnDeprecatedOptions(field);
mediaLibraryFieldOptions = field.getIn(['options', 'media_library'], Map());
} else {
mediaLibraryFieldOptions = field.get('media_library', Map());
}

return onOpenMediaLibrary({
controlID: this.controlID,
forImage,
privateUpload: field.get('private'),
value,
config: field.getIn(['options', 'media_library', 'config']),
allowMultiple: !!mediaLibraryFieldOptions.get('allow_multiple', true),
config: mediaLibraryFieldOptions.get('config'),
});
};

Expand Down
2 changes: 2 additions & 0 deletions packages/netlify-cms/src/media-libraries.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import cms from 'netlify-cms-core/src';
import uploadcare from 'netlify-cms-media-library-uploadcare/src';
import cloudinary from 'netlify-cms-media-library-cloudinary/src';

const { registerMediaLibrary } = cms;

registerMediaLibrary(uploadcare);
registerMediaLibrary(cloudinary);
84 changes: 84 additions & 0 deletions website/content/docs/cloudinary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: Cloudinary
group: media
weight: '10'
---
Cloudinary is a digital asset management platform with a broad feature set, including support for responsive image generation and url based image transformation. They also provide a powerful media library UI for managing assets, and tools for organizing your assets into a hierarchy.

The Cloudinary media library integration for Netlify CMS uses Cloudinary's own media library interface within Netlify CMS. To get started, you'll need a Cloudinary account and Netlify CMS 2.3.0 or greater.

## Creating a Cloudinary Account

You can [sign up for Cloudinary](https://cloudinary.com/users/register/free) for free. Once you're logged in, you'll need to retrieve your Cloud name and API key from the upper left corner of the Cloudinary console.

![Cloudinary console screenshot](/img/cloudinary-console-details.png)

## Connecting Cloudinary to Netlify CMS

To use the Cloudinary media library within Netlify CMS, you'll need to update your Netlify CMS configuration file with the information from your Cloudinary account:

```yml
media_library:
name: cloudinary
config:
cloud_name: your_cloud_name
api_key: your_api_key
```

## Netlify CMS configuration options

The following options are specific to the Netlify CMS integration for Cloudinary:

* **`output_filename_only`**: _(default: `false`)_\
By default, the value provided for a selected image is a complete URL for the asset on Cloudinary's CDN. Setting `output_filename_only` to `true` will instead produce just the filename (e.g. `image.jpg`).
* **`use_transformations`**: _(default: `true`)_\
If `true`, uses derived url when available (the url will have image transformation segments included). Has no effect if `output_filename_only` is set to `true`.
* **`use_secure_url`**: _(default: `true`)_\
Controls whether an `http` or `https` URL is provided. Has no effect if `output_filename_only` is set to `true`.

## Cloudinary configuration options

The Cloudinary media library integration can be configured in two ways: globally and per field. Global options will be overridden by field options. All options are listed in Cloudinary's [media library documentation](https://cloudinary.com/documentation/media_library_widget#3_set_the_configuration_options), but only the following options are available or recommended for the Netlify CMS integration:

### Authentication

* `cloud_name`
* `api_key`
* `username` _\- pre-fills a username in the Cloudinary login form_

### Media library behavior

* `default_transformations` _\- only the first [image transformation](#image-transformations) is used, be sure to use the `Library` column transformation names from the_ [_transformation reference_]("https://cloudinary.com/documentation/image_transformation_reference")
* `max_files`
* `multiple` _\- has no impact on images inside the [markdown widget](/docs/widgets/#markdown)_

## Image transformations

The Cloudinary integration allows images to be transformed in two ways: directly within Netlify CMS, and separately from the CMS via Cloudinary's [dynamic URL's](https://cloudinary.com/documentation/image_transformations#delivering_media_assets_using_dynamic_urls). If you transform images within the Cloudinary media library, the transformed image URL will be output by default. This gives the editor complete freedom to make changes to the image output.

## Art direction and responsive images

If you prefer to provide art direction so that images are transformed in a specific way, or dynamically retrieve images based on viewport size, you can do so by providing your own base Cloudinary URL and only storing the asset filenames in your content:

1. Either globally or for specific fields, configure the Cloudinary extension to only output the asset filename:

```yml
# global
media_library:
name: cloudinary
output_filename_only: true

# field
fields:
- { name: image, widget: image, media_library: { output_filename_only: true } }
```

2. Provide a dynamic URL in the site template where the image is used:

```hbs
{{! handlebars example }}

<img src="https://res.cloudinary.com/<cloud_name>/<resource_type>/<type>/<version>/<transformations>/{{image}}"/>
```

Your dynamic URL can be formed conditionally to provide any desired transformations - please see Cloudinary's [image transformation reference](https://cloudinary.com/documentation/image_transformation_reference) for available transformations.
9 changes: 4 additions & 5 deletions website/content/docs/uploadcare.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,8 @@ For example:
name: cover
label: Cover Image
widget: image
options:
media_library:
config:
multiple: true
previewStep: false
media_library:
config:
multiple: true
previewStep: false
```
10 changes: 9 additions & 1 deletion website/content/docs/widgets/file.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ The file widget allows editors to upload a file or select an existing one from t

- **Name:** `file`
- **UI:** file picker button opens media gallery
- **Data type:** file path string, based on `media_folder`/`public_folder` configuration
- **Data type:** file path string
- **Options:**
- `default`: accepts a file path string; defaults to null
- `media_library`: media library settings to apply when a media library is opened by the
current widget
- `allow_multiple`: _(default: `true`)_ when set to `false`, prevents multiple selection for any media library extension, but must be supported by the extension in use
- `config`: a configuration object that will be passed directly to the media library being
used - available options are determined by the library
- **Example:**
```yaml
- label: "Manual PDF"
name: "manual_pdf"
widget: "file"
default: "/uploads/general-manual.pdf"
media_library:
config:
multiple: true
```
Loading