diff --git a/README.md b/README.md index a3fa5925..1d5d4984 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ Registers a template for a collection. * react_component: A React component that renders the collection data. Three props will be passed to your component during render: * entry: Immutable collection containing the entry data. * widgetFor: Returns the appropriate widget preview component for a given field. - * getMedia: Returns the correct filePath or in-memory preview for uploaded images. + * getAsset: Returns the correct filePath or in-memory preview for uploaded images. **Example:** @@ -192,7 +192,7 @@ var PostPreview = createClass({ render: function() { var entry = this.props.entry; var image = entry.getIn(['data', 'image']); - var bg = this.props.getMedia(image); + var bg = this.props.getAsset(image); return h('div', {}, h('h1', {}, entry.getIn(['data', 'title'])), h('img', {src: bg.toString()}), diff --git a/docs/architecture.md b/docs/architecture.md index 5ea7de81..f631847c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -32,14 +32,14 @@ Selectors are functions defined within reducers used to compute derived data fro **selectEntries:** Selects all entries for a given collection. -**getMedia:** Selects a single MediaProxy object for the given URI: +**getAsset:** Selects a single AssetProxy object for the given URI: ## Value Objects: -**MediaProxy:** MediaProxy is a Value Object that holds information regarding a media file (such as an image, for example), whether it's persisted online or hold locally in cache. +**AssetProxy:** AssetProxy is a Value Object that holds information regarding an asset file (such as an image, for example), whether it's persisted online or hold locally in cache. -For files persisted online, the MediaProxy only keeps information about it's URI. For local files, the MediaProxy will keep a reference to the actual File object while generating the expected final URIs and on-demand blobs for local preview. +For files persisted online, the AssetProxy only keeps information about it's URI. For local files, the AssetProxy will keep a reference to the actual File object while generating the expected final URIs and on-demand blobs for local preview. -The MediaProxy object can be used directly inside a media tag (such as ``), as it will always return something that can be used by the media tag to render correctly (either the URI for the online file or a single-use blob). +The AssetProxy object can be used directly inside a media tag (such as ``), as it will always return something that can be used by the media tag to render correctly (either the URI for the online file or a single-use blob). ## Components structure and Workflows Components are separated into two main categories: Container components and presentational components. @@ -52,10 +52,10 @@ For either updating an existing entry or creating a new one, the `EntryEditor` i - Widgets are used for editing entry fields. There are different widgets for different field types, and they are always defined in a pair containing a `control` and a `preview` components. The control component is responsible for presenting the user with the appropriate interface for manipulating the current field value, while the preview component is responsible for displaying value with the appropriate styling. #### Widget components implementation: -The control component receives 3 callbacks as props: onChange, onAddMedia & onRemoveMedia. +The control component receives 3 callbacks as props: onChange, onAddAsset & onRemoveAsset. - onChange (Required): Should be called when the users changes the current value. It will ultimately end up updating the EntryDraft object in the Redux Store, thus updating the preview component. - - onAddMedia & onRemoveMedia (optionals): If the field accepts file uploads for media (images, for example), these callbacks should be invoked with a `MediaProxy` value object. `onAddMedia` will get the current media stored in the Redux state tree while `onRemoveMedia` will remove it. MediaProxy objects are stored in the `Medias` object and referenced in the `EntryDraft` object on the state tree. + - onAddAsset & onRemoveAsset (optionals): If the field accepts file uploads for media (images, for example), these callbacks should be invoked with a `AssetProxy` value object. `onAddAsset` will get the current media stored in the Redux state tree while `onRemoveAsset` will remove it. AssetProxy objects are stored in the `Medias` object and referenced in the `EntryDraft` object on the state tree. -Both control and preview widgets receive a `getMedia` selector via props. Displaying the media (or its uri) for the user should always be done via `getMedia`, as it returns a MediaProxy that can return the correct value for both medias already persisted on server and cached media not yet uploaded. +Both control and preview widgets receive a `getAsset` selector via props. Displaying the media (or its uri) for the user should always be done via `getAsset`, as it returns a AssetProxy that can return the correct value for both medias already persisted on server and cached media not yet uploaded. -The actual persistence of the content and medias inserted into the control component are delegated to the backend implementation. The backend will be called with the updated values and a a list of mediaProxy objects for each field of the entry, and should return a promise that can resolve into the persisted entry object and the list of the persisted media URIs. +The actual persistence of the content and medias inserted into the control component are delegated to the backend implementation. The backend will be called with the updated values and a a list of assetProxy objects for each field of the entry, and should return a promise that can resolve into the persisted entry object and the list of the persisted media URIs. diff --git a/example/index.html b/example/index.html index 600f89da..73c98009 100644 --- a/example/index.html +++ b/example/index.html @@ -73,7 +73,7 @@ render: function() { var entry = this.props.entry; var image = entry.getIn(['data', 'image']); - var bg = image && this.props.getMedia(image); + var bg = image && this.props.getAsset(image); return h('div', {}, h('div', {className: "cover"}, h('h1', {}, entry.getIn(['data', 'title'])), @@ -104,7 +104,7 @@ h('dd', {}, posts && posts.get('author') || 'None'), h('dt', {}, 'Default Thumbnail'), - h('dd', {}, thumb && h('img', {src: this.props.getMedia(thumb).toString()})) + h('dd', {}, thumb && h('img', {src: this.props.getAsset(thumb).toString()})) ) ); } diff --git a/src/actions/config.js b/src/actions/config.js index 48496851..2ef633ad 100644 --- a/src/actions/config.js +++ b/src/actions/config.js @@ -2,7 +2,6 @@ import yaml from "js-yaml"; import { set, defaultsDeep } from "lodash"; import { currentBackend } from "../backends/backend"; import { authenticate } from "../actions/auth"; -import * as MediaProxy from "../valueObjects/MediaProxy"; import * as publishModes from "../constants/publishModes"; export const CONFIG_REQUEST = "CONFIG_REQUEST"; @@ -64,7 +63,6 @@ export function configFailed(err) { export function configDidLoad(config) { return (dispatch) => { - MediaProxy.setConfig(config); dispatch(configLoaded(config)); }; } diff --git a/src/actions/editorialWorkflow.js b/src/actions/editorialWorkflow.js index d6ea6c92..c8cd575d 100644 --- a/src/actions/editorialWorkflow.js +++ b/src/actions/editorialWorkflow.js @@ -2,7 +2,7 @@ import uuid from 'uuid'; import { actions as notifActions } from 'redux-notifications'; import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; import { currentBackend } from '../backends/backend'; -import { getMedia } from '../reducers'; +import { getAsset } from '../reducers'; import { status, EDITORIAL_WORKFLOW } from '../constants/publishModes'; const { notifSend } = notifActions; @@ -175,13 +175,13 @@ export function persistUnpublishedEntry(collection, entryDraft, existingUnpublis return (dispatch, getState) => { const state = getState(); const backend = currentBackend(state.config); - const mediaProxies = entryDraft.get('mediaFiles').map(path => getMedia(state, path)); + const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path)); const entry = entryDraft.get('entry'); const transactionID = uuid.v4(); dispatch(unpublishedEntryPersisting(collection, entry, transactionID)); const persistAction = existingUnpublishedEntry ? backend.persistUnpublishedEntry : backend.persistEntry; - persistAction.call(backend, state.config, collection, entryDraft, mediaProxies.toJS()) + persistAction.call(backend, state.config, collection, entryDraft, assetProxies.toJS()) .then(() => { dispatch(notifSend({ message: 'Entry saved', diff --git a/src/actions/entries.js b/src/actions/entries.js index 0b46abf8..f995c319 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -2,7 +2,7 @@ import { List } from 'immutable'; import { actions as notifActions } from 'redux-notifications'; import { currentBackend } from '../backends/backend'; import { getIntegrationProvider } from '../integrations'; -import { getMedia, selectIntegration } from '../reducers'; +import { getAsset, selectIntegration } from '../reducers'; import { createEntry } from '../valueObjects/Entry'; const { notifSend } = notifActions; @@ -52,6 +52,17 @@ export function entryLoaded(collection, entry) { }; } +export function entryLoadError(error, collection, slug) { + return { + type: ENTRY_FAILURE, + payload: { + error, + collection: collection.get('name'), + slug, + }, + }; +} + export function entriesLoading(collection) { return { type: ENTRIES_REQUEST, @@ -161,7 +172,15 @@ export function loadEntry(entry, collection, slug) { return backend.getEntry(collection, slug) .then(loadedEntry => ( dispatch(entryLoaded(collection, loadedEntry)) - )); + )) + .catch((error) => { + dispatch(notifSend({ + message: `Failed to load entry: ${ error.message }`, + kind: 'danger', + dismissAfter: 4000, + })); + dispatch(entryLoadError(error, collection, slug)); + }); }; } @@ -171,8 +190,9 @@ export function loadEntries(collection, page = 0) { return; } const state = getState(); + const backend = currentBackend(state.config); const integration = selectIntegration(state, collection.get('name'), 'listEntries'); - const provider = integration ? getIntegrationProvider(state.integrations, integration) : currentBackend(state.config); + const provider = integration ? getIntegrationProvider(state.integrations, backend.getToken, integration) : backend; dispatch(entriesLoading(collection)); provider.listEntries(collection, page).then( response => dispatch(entriesLoaded(collection, response.entries, response.pagination)), @@ -196,11 +216,11 @@ export function persistEntry(collection, entryDraft) { return (dispatch, getState) => { const state = getState(); const backend = currentBackend(state.config); - const mediaProxies = entryDraft.get('mediaFiles').map(path => getMedia(state, path)); + const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path)); const entry = entryDraft.get('entry'); dispatch(entryPersisting(collection, entry)); backend - .persistEntry(state.config, collection, entryDraft, mediaProxies.toJS()) + .persistEntry(state.config, collection, entryDraft, assetProxies.toJS()) .then(() => { dispatch(notifSend({ message: 'Entry saved', diff --git a/src/actions/media.js b/src/actions/media.js index 1e5bd81f..da9f4553 100644 --- a/src/actions/media.js +++ b/src/actions/media.js @@ -1,10 +1,10 @@ -export const ADD_MEDIA = 'ADD_MEDIA'; -export const REMOVE_MEDIA = 'REMOVE_MEDIA'; +export const ADD_ASSET = 'ADD_ASSET'; +export const REMOVE_ASSET = 'REMOVE_ASSET'; -export function addMedia(mediaProxy) { - return { type: ADD_MEDIA, payload: mediaProxy }; +export function addAsset(assetProxy) { + return { type: ADD_ASSET, payload: assetProxy }; } -export function removeMedia(path) { - return { type: REMOVE_MEDIA, payload: path }; +export function removeAsset(path) { + return { type: REMOVE_ASSET, payload: path }; } diff --git a/src/actions/search.js b/src/actions/search.js index 4180c9f8..2295422f 100644 --- a/src/actions/search.js +++ b/src/actions/search.js @@ -109,7 +109,7 @@ export function searchEntries(searchTerm, page = 0) { dispatch(searchFailure(searchTerm, 'Search integration is not configured.')); } const provider = integration ? - getIntegrationProvider(state.integrations, integration) + getIntegrationProvider(state.integrations, currentBackend(state.config).getToken, integration) : currentBackend(state.config); dispatch(searchingEntries(searchTerm)); provider.search(collections, searchTerm, page).then( @@ -129,7 +129,7 @@ export function query(namespace, collection, searchFields, searchTerm) { dispatch(searchFailure(namespace, searchTerm, 'Search integration is not configured.')); } const provider = integration ? - getIntegrationProvider(state.integrations, integration) + getIntegrationProvider(state.integrations, currentBackend(state.config).getToken, integration) : currentBackend(state.config); dispatch(querying(namespace, collection, searchFields, searchTerm)); provider.searchBy(searchFields, collection, searchTerm).then( diff --git a/src/backends/backend.js b/src/backends/backend.js index 0ec267d7..07d480d2 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -1,6 +1,5 @@ import TestRepoBackend from "./test-repo/implementation"; import GitHubBackend from "./github/implementation"; -import NetlifyGitBackend from "./netlify-git/implementation"; import NetlifyAuthBackend from "./netlify-auth/implementation"; import { resolveFormat } from "../formats/formats"; import { selectListMethod, selectEntrySlug, selectEntryPath, selectAllowNewEntries } from "../reducers/collections"; @@ -79,6 +78,8 @@ class Backend { } } + getToken = () => this.implementation.getToken(); + listEntries(collection) { const listMethod = this.implementation[selectListMethod(collection)]; return listMethod.call(this.implementation, collection) @@ -218,8 +219,6 @@ export function resolveBackend(config) { return new Backend(new TestRepoBackend(config), authStore); case "github": return new Backend(new GitHubBackend(config), authStore); - case "netlify-git": - return new Backend(new NetlifyGitBackend(config), authStore); case "netlify-auth": return new Backend(new NetlifyAuthBackend(config), authStore); default: diff --git a/src/backends/github/API.js b/src/backends/github/API.js index 34d082c1..e58863cb 100644 --- a/src/backends/github/API.js +++ b/src/backends/github/API.js @@ -1,7 +1,7 @@ import LocalForage from "localforage"; import { Base64 } from "js-base64"; import _ from "lodash"; -import MediaProxy from "../../valueObjects/MediaProxy"; +import AssetProxy from "../../valueObjects/AssetProxy"; import { SIMPLE, EDITORIAL_WORKFLOW, status } from "../../constants/publishModes"; export default class API { @@ -356,7 +356,7 @@ export default class API { } uploadBlob(item) { - const content = item instanceof MediaProxy ? item.toBase64() : this.toBase64(item.raw); + const content = item instanceof AssetProxy ? item.toBase64() : this.toBase64(item.raw); return content.then(contentBase64 => this.request(`${ this.repoURL }/git/blobs`, { method: "POST", diff --git a/src/backends/github/implementation.js b/src/backends/github/implementation.js index 9e96be2b..2bd95afc 100644 --- a/src/backends/github/implementation.js +++ b/src/backends/github/implementation.js @@ -14,6 +14,7 @@ export default class GitHub { this.repo = config.getIn(["backend", "repo"], ""); this.branch = config.getIn(["backend", "branch"], "master"); + this.token = ''; } authComponent() { @@ -21,17 +22,23 @@ export default class GitHub { } setUser(user) { - this.api = new API({ token: user.token, branch: this.branch, repo: this.repo }); + this.token = user.token; + this.api = new API({ token: this.token, branch: this.branch, repo: this.repo }); } authenticate(state) { - this.api = new API({ token: state.token, branch: this.branch, repo: this.repo }); + this.token = state.token; + this.api = new API({ token: this.token, branch: this.branch, repo: this.repo }); return this.api.user().then((user) => { user.token = state.token; return user; }); } + getToken() { + return Promise.resolve(this.token); + } + entriesByFolder(collection) { return this.api.listFiles(collection.get("folder")) .then(this.fetchFiles); diff --git a/src/backends/netlify-auth/API.js b/src/backends/netlify-auth/API.js index 4b7ab63c..43d48235 100644 --- a/src/backends/netlify-auth/API.js +++ b/src/backends/netlify-auth/API.js @@ -4,20 +4,23 @@ export default class API extends GithubAPI { constructor(config) { super(config); this.api_root = config.api_root; - this.jwtToken = config.jwtToken; + this.tokenPromise = config.tokenPromise; this.commitAuthor = config.commitAuthor; this.repoURL = ""; } - requestHeaders(headers = {}) { - const baseHeader = { - Authorization: `Bearer ${ this.jwtToken }`, - "Content-Type": "application/json", - ...headers, - }; + getRequestHeaders(headers = {}) { + return this.tokenPromise() + .then((jwtToken) => { + const baseHeader = { + "Authorization": `Bearer ${ jwtToken }`, + "Content-Type": "application/json", + ...headers, + }; - return baseHeader; + return baseHeader; + }); } @@ -39,9 +42,10 @@ export default class API extends GithubAPI { } request(path, options = {}) { - const headers = this.requestHeaders(options.headers || {}); const url = this.urlFor(path, options); - return fetch(url, { ...options, headers }).then((response) => { + return this.getRequestHeaders(options.headers || {}) + .then(headers => fetch(url, { ...options, headers })) + .then((response) => { const contentType = response.headers.get("Content-Type"); if (contentType && contentType.match(/json/)) { return this.parseJsonResponse(response); diff --git a/src/backends/netlify-auth/implementation.js b/src/backends/netlify-auth/implementation.js index 3156f007..d63d18a6 100644 --- a/src/backends/netlify-auth/implementation.js +++ b/src/backends/netlify-auth/implementation.js @@ -1,5 +1,5 @@ import NetlifyAuthClient from "netlify-auth-js"; -import { omit } from "lodash"; +import { pick } from "lodash"; import GitHubBackend from "../github/implementation"; import API from "./API"; import AuthenticationPage from "./AuthenticationPage"; @@ -25,20 +25,26 @@ export default class NetlifyAuth extends GitHubBackend { setUser() { const user = this.authClient.currentUser(); if (!user) return Promise.reject(); - return this.authenticate(user); } authenticate(user) { - return user.jwt().then((token) => { - const userData = { - name: `${ user.user_metadata.firstname } ${ user.user_metadata.lastname }`, - email: user.email, - metadata: user.user_metadata, - }; - this.api = new API({ api_root: this.github_proxy_url, jwtToken: token, commitAuthor: omit(userData, ["metadata"]) }); - return userData; + this.tokenPromise = user.jwt.bind(user); + const userData = { + name: `${ user.user_metadata.firstname } ${ user.user_metadata.lastname }`, + email: user.email, + metadata: user.user_metadata, + }; + this.api = new API({ + api_root: this.github_proxy_url, + tokenPromise: this.tokenPromise, + commitAuthor: pick(userData, ["name", "email"]), }); + return Promise.resolve(userData); + } + + getToken() { + return this.tokenPromise(); } authComponent() { diff --git a/src/backends/netlify-git/API.js b/src/backends/netlify-git/API.js deleted file mode 100644 index 886c9f0b..00000000 --- a/src/backends/netlify-git/API.js +++ /dev/null @@ -1,287 +0,0 @@ -import LocalForage from 'localforage'; -import MediaProxy from '../../valueObjects/MediaProxy'; -import { Base64 } from 'js-base64'; - -export default class API { - constructor(token, url, branch) { - this.token = token; - this.url = url; - this.branch = branch; - this.repoURL = ''; - } - - requestHeaders(headers = {}) { - return { - Authorization: `Bearer ${this.token}`, - 'Content-Type': 'application/json', - ...headers - }; - } - - parseJsonResponse(response) { - return response.json().then((json) => { - if (!response.ok) { - return Promise.reject(json); - } - - return json; - }); - } - - urlFor(path, options) { - const params = []; - if (options.params) { - for (const key in options.params) { - params.push(`${key}=${encodeURIComponent(options.params[key])}`); - } - } - if (params.length) { - path += `?${params.join('&')}`; - } - return this.url + path; - } - - request(path, options = {}) { - const headers = this.requestHeaders(options.headers || {}); - const url = this.urlFor(path, options); - return fetch(url, { ...options, headers: headers }).then((response) => { - if (response.headers.get('Content-Type').match(/json/) && !options.raw) { - return this.parseJsonResponse(response); - } - - return response.text(); - }); - } - - checkMetadataRef() { - return this.request(`${this.repoURL}/refs/meta/_netlify_cms`, { - cache: 'no-store', - }) - .then(response => response.object) - .catch(error => { - // Meta ref doesn't exist - const readme = { - raw: '# Netlify CMS\n\nThis tree is used by the Netlify CMS to store metadata information for specific files and branches.' - }; - - return this.uploadBlob(readme) - .then(item => this.request(`${this.repoURL}/trees`, { - method: 'POST', - body: JSON.stringify({ tree: [{ path: 'README.md', mode: '100644', type: 'blob', sha: item.sha }] }) - })) - .then(tree => this.commit('First Commit', tree)) - .then(response => this.createRef('meta', '_netlify_cms', response.sha)) - .then(response => response.object); - }); - } - - storeMetadata(key, data) { - return this.checkMetadataRef() - .then((branchData) => { - const fileTree = { - [`${key}.json`]: { - path: `${key}.json`, - raw: JSON.stringify(data), - file: true - } - }; - - return this.uploadBlob(fileTree[`${key}.json`]) - .then(item => this.updateTree(branchData.sha, '/', fileTree)) - .then(changeTree => this.commit(`Updating “${key}” metadata`, changeTree)) - .then(response => this.patchRef('meta', '_netlify_cms', response.sha)); - }).then(() => { - LocalForage.setItem(`gh.meta.${key}`, { - expires: Date.now() + 300000, // In 5 minutes - data - }); - }); - } - - retrieveMetadata(key, data) { - const cache = LocalForage.getItem(`gh.meta.${key}`); - return cache.then((cached) => { - if (cached && cached.expires > Date.now()) { return cached.data; } - - return this.request(`${this.repoURL}/files/${key}.json?ref=refs/meta/_netlify_cms`, { - params: { ref: 'refs/meta/_netlify_cms' }, - headers: { 'Content-Type': 'application/vnd.netlify.raw' }, - cache: 'no-store', - }) - .then(response => JSON.parse(response)); - }); - } - - readFile(path, sha, branch = this.branch) { - const cache = sha ? LocalForage.getItem(`gh.${sha}`) : Promise.resolve(null); - return cache.then((cached) => { - if (cached) { return cached; } - - return this.request(`${this.repoURL}/files/${path}`, { - headers: { 'Content-Type': 'application/vnd.netlify.raw' }, - params: { ref: branch }, - cache: false, - raw: true - }).then((result) => { - if (sha) { - LocalForage.setItem(`gh.${sha}`, result); - } - return result; - }); - }); - } - - listFiles(path) { - return this.request(`${this.repoURL}/files/${path}`, { - params: { ref: this.branch } - }); - } - - persistFiles(entry, mediaFiles, options) { - let filename, part, parts, subtree; - const fileTree = {}; - const uploadPromises = []; - - const files = mediaFiles.concat(entry); - - files.forEach((file) => { - if (file.uploaded) { return; } - uploadPromises.push(this.uploadBlob(file)); - parts = file.path.split('/').filter((part) => part); - filename = parts.pop(); - subtree = fileTree; - while (part = parts.shift()) { - subtree[part] = subtree[part] || {}; - subtree = subtree[part]; - } - subtree[filename] = file; - file.file = true; - }); - return Promise.all(uploadPromises) - .then(() => this.getBranch()) - .then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree)) - .then(changeTree => this.commit(options.commitMessage, changeTree)) - .then((response) => this.patchBranch(this.branch, response.sha)); - } - - createRef(type, name, sha) { - return this.request(`${this.repoURL}/refs`, { - method: 'POST', - body: JSON.stringify({ ref: `refs/${type}/${name}`, sha }), - }); - } - - patchRef(type, name, sha) { - return this.request(`${this.repoURL}/refs/${type}/${name}`, { - method: 'PATCH', - body: JSON.stringify({ sha }) - }); - } - - deleteRef(type, name, sha) { - return this.request(`${this.repoURL}/refs/${type}/${name}`, { - method: 'DELETE', - }); - } - - getBranch(branch = this.branch) { - return this.request(`${this.repoURL}/refs/heads/${this.branch}`); - } - - createBranch(branchName, sha) { - return this.createRef('heads', branchName, sha); - } - - patchBranch(branchName, sha) { - return this.patchRef('heads', branchName, sha); - } - - deleteBranch(branchName) { - return this.deleteRef('heads', branchName); - } - - createPR(title, head, base = 'master') { - const body = 'Automatically generated by Netlify CMS'; - return this.request(`${this.repoURL}/pulls`, { - method: 'POST', - body: JSON.stringify({ title, body, head, base }), - }); - } - - getTree(sha) { - return sha ? this.request(`${this.repoURL}/trees/${sha}`) : Promise.resolve({ tree: [] }); - } - - toBase64(str) { - return Promise.resolve( - Base64.encode(str) - ); - } - - uploadBlob(item) { - const content = item instanceof MediaProxy ? item.toBase64() : this.toBase64(item.raw); - - return content.then((contentBase64) => { - return this.request(`${this.repoURL}/blobs`, { - method: 'POST', - body: JSON.stringify({ - content: contentBase64, - encoding: 'base64' - }) - }).then((response) => { - item.sha = response.sha; - item.uploaded = true; - return item; - }); - }); - } - - updateTree(sha, path, fileTree) { - return this.getTree(sha) - .then((tree) => { - var obj, filename, fileOrDir; - var updates = []; - var added = {}; - - for (var i = 0, len = tree.tree.length; i < len; i++) { - obj = tree.tree[i]; - if (fileOrDir = fileTree[obj.path]) { - added[obj.path] = true; - if (fileOrDir.file) { - updates.push({ path: obj.path, mode: obj.mode, type: obj.type, sha: fileOrDir.sha }); - } else { - updates.push(this.updateTree(obj.sha, obj.path, fileOrDir)); - } - } - } - for (filename in fileTree) { - fileOrDir = fileTree[filename]; - if (added[filename]) { continue; } - updates.push( - fileOrDir.file ? - { path: filename, mode: '100644', type: 'blob', sha: fileOrDir.sha } : - this.updateTree(null, filename, fileOrDir) - ); - } - return Promise.all(updates) - .then((updates) => { - return this.request(`${this.repoURL}/trees`, { - method: 'POST', - body: JSON.stringify({ base_tree: sha, tree: updates }) - }); - }).then((response) => { - return { path: path, mode: '040000', type: 'tree', sha: response.sha, parentSha: sha }; - }); - }); - } - - commit(message, changeTree) { - const tree = changeTree.sha; - const parents = changeTree.parentSha ? [changeTree.parentSha] : []; - return this.request(`${this.repoURL}/commits`, { - method: 'POST', - body: JSON.stringify({ message, tree, parents }) - }); - } - -} diff --git a/src/backends/netlify-git/AuthenticationPage.js b/src/backends/netlify-git/AuthenticationPage.js deleted file mode 100644 index 0840a332..00000000 --- a/src/backends/netlify-git/AuthenticationPage.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -export default class AuthenticationPage extends React.Component { - static propTypes = { - onLogin: React.PropTypes.func.isRequired - }; - - state = {}; - - handleLogin = e => { - e.preventDefault(); - const { email, password } = this.state; - this.setState({ authenticating: true }); - fetch(`${AuthenticationPage.url}/token`, { - method: 'POST', - body: 'grant_type=client_credentials', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': 'Basic ' + btoa(`${email}:${password}`) - } - }).then((response) => { - if (response.ok) { - return response.json().then((data) => { - this.props.onLogin(Object.assign({ email }, data)); - }); - } - response.json().then((data) => { - this.setState({ loginError: data.msg }); - }); - }); - }; - - handleChange(key) { - return (e) => { - this.setState({ [key]: e.target.value }); - }; - } - - render() { - const { loginError } = this.state; - - return
- {loginError &&

{loginError}

} -

- -

-

- -

-

- -

-
; - } -} diff --git a/src/backends/netlify-git/implementation.js b/src/backends/netlify-git/implementation.js deleted file mode 100644 index 589b1b83..00000000 --- a/src/backends/netlify-git/implementation.js +++ /dev/null @@ -1,62 +0,0 @@ -import semaphore from 'semaphore'; -import { createEntry } from '../../valueObjects/Entry'; -import AuthenticationPage from './AuthenticationPage'; -import API from './API'; - -const MAX_CONCURRENT_DOWNLOADS = 10; - -export default class NetlifyGit { - constructor(config) { - this.config = config; - if (config.getIn(['backend', 'url']) == null) { - throw 'The netlify-git backend needs a "url" in the backend configuration.'; - } - this.url = config.getIn(['backend', 'url']); - this.branch = config.getIn(['backend', 'branch']) || 'master'; - AuthenticationPage.url = this.url; - } - - authComponent() { - return AuthenticationPage; - } - - setUser(user) { - this.api = new API(user.access_token, this.url, this.branch || 'master'); - } - - authenticate(state) { - return Promise.resolve(state); - } - - entries(collection) { - return this.api.listFiles(collection.get('folder')).then((files) => { - const sem = semaphore(MAX_CONCURRENT_DOWNLOADS); - const promises = []; - files.map((file) => { - promises.push(new Promise((resolve, reject) => { - return sem.take(() => this.api.readFile(file.path, file.sha).then((data) => { - resolve(createEntry(collection.get('name'), file.path.split('/').pop().replace(/\.[^\.]+$/, ''), file.path, { raw: data })); - sem.leave(); - }).catch((err) => { - sem.leave(); - reject(err); - })); - })); - }); - return Promise.all(promises); - }).then((entries) => ({ - pagination: {}, - entries - })); - } - - lookupEntry(collection, slug) { - return this.entries(collection).then((response) => ( - response.entries.filter((entry) => entry.slug === slug)[0] - )); - } - - persistEntry(entry, mediaFiles = [], options = {}) { - return this.api.persistFiles(entry, mediaFiles, options); - } -} diff --git a/src/backends/test-repo/implementation.js b/src/backends/test-repo/implementation.js index 0ef37f10..f00d9916 100644 --- a/src/backends/test-repo/implementation.js +++ b/src/backends/test-repo/implementation.js @@ -36,6 +36,10 @@ export default class TestRepo { return Promise.resolve({ email: state.email, name: nameFromEmail(state.email) }); } + getToken() { + return Promise.resolve(''); + } + entriesByFolder(collection) { const entries = []; const folder = collection.get('folder'); @@ -67,7 +71,7 @@ export default class TestRepo { getEntry(collection, slug, path) { return Promise.resolve({ file: { path }, - data: getFile(path).content + data: getFile(path).content, }); } diff --git a/src/components/ControlPanel/ControlPane.js b/src/components/ControlPanel/ControlPane.js index 202e7f7d..400e8532 100644 --- a/src/components/ControlPanel/ControlPane.js +++ b/src/components/ControlPanel/ControlPane.js @@ -10,7 +10,7 @@ function isHidden(field) { export default class ControlPane extends Component { controlFor(field) { - const { entry, fieldsMetaData, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props; + const { entry, fieldsMetaData, getAsset, onChange, onAddAsset, onRemoveAsset } = this.props; const widget = resolveWidget(field.get('widget')); const fieldName = field.get('name'); const value = entry.getIn(['data', fieldName]); @@ -25,9 +25,9 @@ export default class ControlPane extends Component { value, metadata, onChange: (newValue, newMetadata) => onChange(fieldName, newValue, newMetadata), - onAddMedia, - onRemoveMedia, - getMedia, + onAddAsset, + onRemoveAsset, + getAsset, }) } @@ -60,8 +60,8 @@ ControlPane.propTypes = { entry: ImmutablePropTypes.map.isRequired, fields: ImmutablePropTypes.list.isRequired, fieldsMetaData: ImmutablePropTypes.map.isRequired, - getMedia: PropTypes.func.isRequired, - onAddMedia: PropTypes.func.isRequired, + getAsset: PropTypes.func.isRequired, + onAddAsset: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, - onRemoveMedia: PropTypes.func.isRequired, + onRemoveAsset: PropTypes.func.isRequired, }; diff --git a/src/components/EntryEditor/EntryEditor.js b/src/components/EntryEditor/EntryEditor.js index 8da72618..b3e6617d 100644 --- a/src/components/EntryEditor/EntryEditor.js +++ b/src/components/EntryEditor/EntryEditor.js @@ -26,10 +26,10 @@ class EntryEditor extends Component { entry, fields, fieldsMetaData, - getMedia, + getAsset, onChange, - onAddMedia, - onRemoveMedia, + onAddAsset, + onRemoveAsset, onPersist, onCancelEdit, } = this.props; @@ -53,10 +53,10 @@ class EntryEditor extends Component { entry={entry} fields={fields} fieldsMetaData={fieldsMetaData} - getMedia={getMedia} + getAsset={getAsset} onChange={onChange} - onAddMedia={onAddMedia} - onRemoveMedia={onRemoveMedia} + onAddAsset={onAddAsset} + onRemoveAsset={onRemoveAsset} /> @@ -67,7 +67,7 @@ class EntryEditor extends Component { entry={entry} fields={fields} fieldsMetaData={fieldsMetaData} - getMedia={getMedia} + getAsset={getAsset} /> @@ -91,11 +91,11 @@ EntryEditor.propTypes = { entry: ImmutablePropTypes.map.isRequired, fields: ImmutablePropTypes.list.isRequired, fieldsMetaData: ImmutablePropTypes.map.isRequired, - getMedia: PropTypes.func.isRequired, - onAddMedia: PropTypes.func.isRequired, + getAsset: PropTypes.func.isRequired, + onAddAsset: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, onPersist: PropTypes.func.isRequired, - onRemoveMedia: PropTypes.func.isRequired, + onRemoveAsset: PropTypes.func.isRequired, onCancelEdit: PropTypes.func.isRequired, }; diff --git a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js b/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js index 744d3ecc..f9f2c158 100644 --- a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js +++ b/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js @@ -10,7 +10,7 @@ import htmlSyntax from 'markup-it/syntaxes/html'; import reInline from 'markup-it/syntaxes/markdown/re/inline'; import MarkupItReactRenderer from '../'; -function getMedia(path) { +function getAsset(path) { return path; } @@ -21,7 +21,7 @@ describe('MarkitupReactRenderer', () => { ); const tree1 = component.html(); @@ -38,7 +38,7 @@ describe('MarkitupReactRenderer', () => { ); const syntax1 = component.instance().props.syntax; @@ -83,7 +83,7 @@ Text with **bold** & _em_ elements ); expect(component.html()).toMatchSnapshot(); @@ -98,7 +98,7 @@ Text with **bold** & _em_ elements ); expect(component.html()).toMatchSnapshot(); @@ -123,7 +123,7 @@ Text with **bold** & _em_ elements ); expect(component.html()).toMatchSnapshot(); @@ -143,7 +143,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] ); expect(component.html()).toMatchSnapshot(); @@ -157,7 +157,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] ); expect(component.html()).toMatchSnapshot(); @@ -169,7 +169,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] ); expect(component.html()).toMatchSnapshot(); @@ -197,7 +197,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] ); expect(component.html()).toMatchSnapshot(); @@ -241,7 +241,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] value={value} syntax={myMarkdownSyntax} schema={myCustomSchema} - getMedia={getMedia} + getAsset={getAsset} /> ); expect(component.html()).toMatchSnapshot(); @@ -255,7 +255,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] ); expect(component.html()).toMatchSnapshot(); diff --git a/src/components/MarkupItReactRenderer/index.js b/src/components/MarkupItReactRenderer/index.js index 3513c1fd..79a2bc98 100644 --- a/src/components/MarkupItReactRenderer/index.js +++ b/src/components/MarkupItReactRenderer/index.js @@ -13,9 +13,7 @@ const defaultSchema = { [BLOCKS.BLOCKQUOTE]: 'blockquote', [BLOCKS.PARAGRAPH]: 'p', [BLOCKS.FOOTNOTE]: 'footnote', - [BLOCKS.HTML]: ({ token }) => { - return
; - }, + [BLOCKS.HTML]: ({ token }) =>
, [BLOCKS.HR]: 'hr', [BLOCKS.HEADING_1]: 'h1', [BLOCKS.HEADING_2]: 'h2', @@ -63,10 +61,10 @@ export default class MarkupItReactRenderer extends React.Component { } sanitizeProps(props) { - const { getMedia } = this.props; + const { getAsset } = this.props; if (props.image) { - props = Object.assign({}, props, { src: getMedia(props.image).toString() }); + props = Object.assign({}, props, { src: getAsset(props.image).toString() }); } return omit(props, notAllowedAttributes); @@ -115,7 +113,7 @@ export default class MarkupItReactRenderer extends React.Component { render() { - const { value, schema, getMedia } = this.props; + const { value, schema, getAsset } = this.props; const content = this.parser.toContent(value); return this.renderToken({ ...defaultSchema, ...schema }, content.get('token')); } @@ -128,5 +126,5 @@ MarkupItReactRenderer.propTypes = { PropTypes.string, PropTypes.func, ])), - getMedia: PropTypes.func.isRequired, + getAsset: PropTypes.func.isRequired, }; diff --git a/src/components/PreviewPane/Preview.js b/src/components/PreviewPane/Preview.js index 6f2aac02..1875d1b1 100644 --- a/src/components/PreviewPane/Preview.js +++ b/src/components/PreviewPane/Preview.js @@ -24,6 +24,6 @@ Preview.propTypes = { collection: ImmutablePropTypes.map.isRequired, entry: ImmutablePropTypes.map.isRequired, fields: ImmutablePropTypes.list.isRequired, - getMedia: PropTypes.func.isRequired, + getAsset: PropTypes.func.isRequired, widgetFor: PropTypes.func.isRequired, }; diff --git a/src/components/PreviewPane/PreviewPane.js b/src/components/PreviewPane/PreviewPane.js index 6d82ab54..07903ab1 100644 --- a/src/components/PreviewPane/PreviewPane.js +++ b/src/components/PreviewPane/PreviewPane.js @@ -29,7 +29,7 @@ export default class PreviewPane extends React.Component { } widgetFor = (name) => { - const { fields, entry, fieldsMetaData, getMedia } = this.props; + const { fields, entry, fieldsMetaData, getAsset } = this.props; const field = fields.find(f => f.get('name') === name); let value = entry.getIn(['data', field.get('name')]); const metadata = fieldsMetaData.get(field.get('name')); @@ -46,7 +46,7 @@ export default class PreviewPane extends React.Component { value, field, metadata, - getMedia, + getAsset, }); }; @@ -72,6 +72,7 @@ export default class PreviewPane extends React.Component { renderPreview() { const { entry, collection } = this.props; + if (!entry || !entry.get('data')) return; const component = registry.getPreviewTemplate(selectTemplateName(collection, entry.get('slug'))) || Preview; this.inferFields(); @@ -104,5 +105,5 @@ PreviewPane.propTypes = { fields: ImmutablePropTypes.list.isRequired, entry: ImmutablePropTypes.map.isRequired, fieldsMetaData: ImmutablePropTypes.map.isRequired, - getMedia: PropTypes.func.isRequired, + getAsset: PropTypes.func.isRequired, }; diff --git a/src/components/Widgets.js b/src/components/Widgets.js index 7d4b89b4..4eabc067 100644 --- a/src/components/Widgets.js +++ b/src/components/Widgets.js @@ -13,6 +13,8 @@ import MarkdownControl from './Widgets/MarkdownControl'; import MarkdownPreview from './Widgets/MarkdownPreview'; import ImageControl from './Widgets/ImageControl'; import ImagePreview from './Widgets/ImagePreview'; +import FileControl from './Widgets/FileControl'; +import FilePreview from './Widgets/FilePreview'; import DateControl from './Widgets/DateControl'; import DatePreview from './Widgets/DatePreview'; import DateTimeControl from './Widgets/DateTimeControl'; @@ -31,6 +33,7 @@ registry.registerWidget('number', NumberControl, NumberPreview); registry.registerWidget('list', ListControl, ListPreview); registry.registerWidget('markdown', MarkdownControl, MarkdownPreview); registry.registerWidget('image', ImageControl, ImagePreview); +registry.registerWidget('file', FileControl, FilePreview); registry.registerWidget('date', DateControl, DatePreview); registry.registerWidget('datetime', DateTimeControl, DateTimePreview); registry.registerWidget('select', SelectControl, SelectPreview); diff --git a/src/components/Widgets/FileControl.js b/src/components/Widgets/FileControl.js new file mode 100644 index 00000000..46643e43 --- /dev/null +++ b/src/components/Widgets/FileControl.js @@ -0,0 +1,122 @@ +import React, { PropTypes } from 'react'; +import { truncateMiddle } from '../../lib/textHelper'; +import { Loader } from '../UI'; +import AssetProxy, { createAssetProxy } from '../../valueObjects/AssetProxy'; + +const MAX_DISPLAY_LENGTH = 50; + +export default class FileControl extends React.Component { + state = { + processing: false, + }; + + handleFileInputRef = (el) => { + this._fileInput = el; + }; + + handleClick = (e) => { + this._fileInput.click(); + }; + + handleDragEnter = (e) => { + e.stopPropagation(); + e.preventDefault(); + }; + + handleDragOver = (e) => { + e.stopPropagation(); + e.preventDefault(); + }; + + handleChange = (e) => { + e.stopPropagation(); + e.preventDefault(); + + const fileList = e.dataTransfer ? e.dataTransfer.files : e.target.files; + const files = [...fileList]; + const imageType = /^image\//; + + // Return the first file on the list + const file = files[0]; + + this.props.onRemoveAsset(this.props.value); + if (file) { + this.setState({ processing: true }); + createAssetProxy(file.name, file, false, this.props.field.get('private', false)) + .then((assetProxy) => { + this.setState({ processing: false }); + this.props.onAddAsset(assetProxy); + this.props.onChange(assetProxy.public_path); + }); + } else { + this.props.onChange(null); + } + }; + + renderFileName = () => { + if (!this.props.value) return null; + if (this.value instanceof AssetProxy) { + return truncateMiddle(this.props.value.path, MAX_DISPLAY_LENGTH); + } else { + return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH); + } + }; + + render() { + const { processing } = this.state; + const fileName = this.renderFileName(); + if (processing) { + return ( +
+ + + +
+ ); + } + return ( +
+ + {fileName ? fileName : 'Tip: Click here to select a file to upload, or drag an image directly into this box from your desktop'} + + +
+ ); + } +} + +const styles = { + input: { + display: 'none', + }, + message: { + padding: '20px', + display: 'block', + fontSize: '12px', + }, + imageUpload: { + backgroundColor: '#fff', + textAlign: 'center', + color: '#999', + border: '1px dashed #eee', + cursor: 'pointer', + }, +}; + +FileControl.propTypes = { + onAddAsset: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onRemoveAsset: PropTypes.func.isRequired, + value: PropTypes.node, + field: PropTypes.object, +}; diff --git a/src/components/Widgets/FilePreview.js b/src/components/Widgets/FilePreview.js new file mode 100644 index 00000000..d9ed8801 --- /dev/null +++ b/src/components/Widgets/FilePreview.js @@ -0,0 +1,15 @@ +import React, { PropTypes } from 'react'; +import previewStyle from './defaultPreviewStyle'; + +export default function FilePreview({ value, getAsset }) { + return (
+ { value ? + { value } + : null} +
); +} + +FilePreview.propTypes = { + getAsset: PropTypes.func.isRequired, + value: PropTypes.node, +}; diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index 890adf90..0ccc46ee 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -1,10 +1,15 @@ import React, { PropTypes } from 'react'; import { truncateMiddle } from '../../lib/textHelper'; -import MediaProxy from '../../valueObjects/MediaProxy'; +import { Loader } from '../UI'; +import AssetProxy, { createAssetProxy } from '../../valueObjects/AssetProxy'; const MAX_DISPLAY_LENGTH = 50; export default class ImageControl extends React.Component { + state = { + processing: false, + }; + handleFileInputRef = (el) => { this._fileInput = el; }; @@ -38,11 +43,15 @@ export default class ImageControl extends React.Component { } }); - this.props.onRemoveMedia(this.props.value); + this.props.onRemoveAsset(this.props.value); if (file) { - const mediaProxy = new MediaProxy(file.name, file); - this.props.onAddMedia(mediaProxy); - this.props.onChange(mediaProxy.public_path); + this.setState({ processing: true }); + createAssetProxy(file.name, file) + .then((assetProxy) => { + this.setState({ processing: false }); + this.props.onAddAsset(assetProxy); + this.props.onChange(assetProxy.public_path); + }); } else { this.props.onChange(null); } @@ -50,7 +59,7 @@ export default class ImageControl extends React.Component { renderImageName = () => { if (!this.props.value) return null; - if (this.value instanceof MediaProxy) { + if (this.value instanceof AssetProxy) { return truncateMiddle(this.props.value.path, MAX_DISPLAY_LENGTH); } else { return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH); @@ -58,14 +67,25 @@ export default class ImageControl extends React.Component { }; render() { + const { processing } = this.state; const imageName = this.renderImageName(); + if (processing) { + return ( +
+ + + +
+ ); + } return (
- + {imageName ? imageName : 'Tip: Click here to upload an image from your file browser, or drag an image directly into this box from your desktop'} { value ? @@ -14,6 +14,6 @@ export default function ImagePreview({ value, getMedia }) { } ImagePreview.propTypes = { - getMedia: PropTypes.func.isRequired, + getAsset: PropTypes.func.isRequired, value: PropTypes.node, }; diff --git a/src/components/Widgets/ListControl.js b/src/components/Widgets/ListControl.js index 2b3e71bd..345d6d4c 100644 --- a/src/components/Widgets/ListControl.js +++ b/src/components/Widgets/ListControl.js @@ -29,9 +29,9 @@ export default class ListControl extends Component { onChange: PropTypes.func.isRequired, value: PropTypes.node, field: PropTypes.node, - getMedia: PropTypes.func.isRequired, - onAddMedia: PropTypes.func.isRequired, - onRemoveMedia: PropTypes.func.isRequired, + getAsset: PropTypes.func.isRequired, + onAddAsset: PropTypes.func.isRequired, + onRemoveAsset: PropTypes.func.isRequired, }; constructor(props) { @@ -125,7 +125,7 @@ export default class ListControl extends Component { }; renderItem(item, index) { - const { value, field, getMedia, onAddMedia, onRemoveMedia } = this.props; + const { value, field, getAsset, onAddAsset, onRemoveAsset } = this.props; const { itemStates } = this.state; const collapsed = itemStates.getIn([index, 'collapsed']); const classNames = [styles.item, collapsed ? styles.collapsed : styles.expanded]; @@ -145,9 +145,9 @@ export default class ListControl extends Component { value={item} field={field} onChange={this.handleChangeFor(index)} - getMedia={getMedia} - onAddMedia={onAddMedia} - onRemoveMedia={onRemoveMedia} + getAsset={getAsset} + onAddAsset={onAddAsset} + onRemoveAsset={onRemoveAsset} />
diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js index ad9c2cf3..21782571 100644 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js @@ -4,7 +4,7 @@ import markdownSyntax from 'markup-it/syntaxes/markdown'; import htmlSyntax from 'markup-it/syntaxes/html'; import CaretPosition from 'textarea-caret-position'; import registry from '../../../../lib/registry'; -import MediaProxy from '../../../../valueObjects/MediaProxy'; +import { createAssetProxy } from '../../../../valueObjects/AssetProxy'; import Toolbar from '../Toolbar'; import BlockMenu from '../BlockMenu'; import styles from './index.css'; @@ -271,12 +271,16 @@ export default class RawEditor extends React.Component { if (e.dataTransfer.files && e.dataTransfer.files.length) { data = Array.from(e.dataTransfer.files).map((file) => { - const mediaProxy = new MediaProxy(file.name, file); - this.props.onAddMedia(mediaProxy); - const link = `[${ file.name }](${ mediaProxy.public_path })`; + const link = `[Uploading ${ file.name }...]()`; if (file.type.split('/')[0] === 'image') { return `!${ link }`; } + + createAssetProxy(file.name, file) + .then((assetProxy) => { + this.props.onAddAsset(assetProxy); + // TODO: Change the link text + }); return link; }).join('\n\n'); } else { @@ -304,7 +308,7 @@ export default class RawEditor extends React.Component { }; render() { - const { onAddMedia, onRemoveMedia, getMedia } = this.props; + const { onAddAsset, onRemoveAsset, getAsset } = this.props; const { showToolbar, showBlockMenu, plugins, selectionPosition, dragging } = this.state; const classNames = [styles.root]; if (dragging) { @@ -333,9 +337,9 @@ export default class RawEditor extends React.Component { selectionPosition={selectionPosition} plugins={plugins} onBlock={this.handleBlock} - onAddMedia={onAddMedia} - onRemoveMedia={onRemoveMedia} - getMedia={getMedia} + onAddAsset={onAddAsset} + onRemoveAsset={onRemoveAsset} + getAsset={getAsset} />