Asset API (#204)

Asset API
This commit is contained in:
Cássio Souza 2017-01-10 22:23:22 -02:00 committed by GitHub
parent 37f690fc44
commit a4d7622ade
52 changed files with 706 additions and 687 deletions

View File

@ -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: * 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. * entry: Immutable collection containing the entry data.
* widgetFor: Returns the appropriate widget preview component for a given field. * 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:** **Example:**
@ -192,7 +192,7 @@ var PostPreview = createClass({
render: function() { render: function() {
var entry = this.props.entry; var entry = this.props.entry;
var image = entry.getIn(['data', 'image']); var image = entry.getIn(['data', 'image']);
var bg = this.props.getMedia(image); var bg = this.props.getAsset(image);
return h('div', {}, return h('div', {},
h('h1', {}, entry.getIn(['data', 'title'])), h('h1', {}, entry.getIn(['data', 'title'])),
h('img', {src: bg.toString()}), h('img', {src: bg.toString()}),

View File

@ -32,14 +32,14 @@ Selectors are functions defined within reducers used to compute derived data fro
**selectEntries:** Selects all entries for a given collection. **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: ## 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 `<img>`), 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 `<img>`), 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 structure and Workflows
Components are separated into two main categories: Container components and presentational components. 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. - 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: #### 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. - 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.

View File

@ -73,7 +73,7 @@
render: function() { render: function() {
var entry = this.props.entry; var entry = this.props.entry;
var image = entry.getIn(['data', 'image']); var image = entry.getIn(['data', 'image']);
var bg = image && this.props.getMedia(image); var bg = image && this.props.getAsset(image);
return h('div', {}, return h('div', {},
h('div', {className: "cover"}, h('div', {className: "cover"},
h('h1', {}, entry.getIn(['data', 'title'])), h('h1', {}, entry.getIn(['data', 'title'])),
@ -104,7 +104,7 @@
h('dd', {}, posts && posts.get('author') || 'None'), h('dd', {}, posts && posts.get('author') || 'None'),
h('dt', {}, 'Default Thumbnail'), 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()}))
) )
); );
} }

View File

@ -2,7 +2,6 @@ import yaml from "js-yaml";
import { set, defaultsDeep } from "lodash"; import { set, defaultsDeep } from "lodash";
import { currentBackend } from "../backends/backend"; import { currentBackend } from "../backends/backend";
import { authenticate } from "../actions/auth"; import { authenticate } from "../actions/auth";
import * as MediaProxy from "../valueObjects/MediaProxy";
import * as publishModes from "../constants/publishModes"; import * as publishModes from "../constants/publishModes";
export const CONFIG_REQUEST = "CONFIG_REQUEST"; export const CONFIG_REQUEST = "CONFIG_REQUEST";
@ -64,7 +63,6 @@ export function configFailed(err) {
export function configDidLoad(config) { export function configDidLoad(config) {
return (dispatch) => { return (dispatch) => {
MediaProxy.setConfig(config);
dispatch(configLoaded(config)); dispatch(configLoaded(config));
}; };
} }

View File

@ -2,7 +2,7 @@ import uuid from 'uuid';
import { actions as notifActions } from 'redux-notifications'; import { actions as notifActions } from 'redux-notifications';
import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
import { currentBackend } from '../backends/backend'; import { currentBackend } from '../backends/backend';
import { getMedia } from '../reducers'; import { getAsset } from '../reducers';
import { status, EDITORIAL_WORKFLOW } from '../constants/publishModes'; import { status, EDITORIAL_WORKFLOW } from '../constants/publishModes';
const { notifSend } = notifActions; const { notifSend } = notifActions;
@ -175,13 +175,13 @@ export function persistUnpublishedEntry(collection, entryDraft, existingUnpublis
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const backend = currentBackend(state.config); 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 entry = entryDraft.get('entry');
const transactionID = uuid.v4(); const transactionID = uuid.v4();
dispatch(unpublishedEntryPersisting(collection, entry, transactionID)); dispatch(unpublishedEntryPersisting(collection, entry, transactionID));
const persistAction = existingUnpublishedEntry ? backend.persistUnpublishedEntry : backend.persistEntry; 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(() => { .then(() => {
dispatch(notifSend({ dispatch(notifSend({
message: 'Entry saved', message: 'Entry saved',

View File

@ -2,7 +2,7 @@ import { List } from 'immutable';
import { actions as notifActions } from 'redux-notifications'; import { actions as notifActions } from 'redux-notifications';
import { currentBackend } from '../backends/backend'; import { currentBackend } from '../backends/backend';
import { getIntegrationProvider } from '../integrations'; import { getIntegrationProvider } from '../integrations';
import { getMedia, selectIntegration } from '../reducers'; import { getAsset, selectIntegration } from '../reducers';
import { createEntry } from '../valueObjects/Entry'; import { createEntry } from '../valueObjects/Entry';
const { notifSend } = notifActions; 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) { export function entriesLoading(collection) {
return { return {
type: ENTRIES_REQUEST, type: ENTRIES_REQUEST,
@ -161,7 +172,15 @@ export function loadEntry(entry, collection, slug) {
return backend.getEntry(collection, slug) return backend.getEntry(collection, slug)
.then(loadedEntry => ( .then(loadedEntry => (
dispatch(entryLoaded(collection, 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; return;
} }
const state = getState(); const state = getState();
const backend = currentBackend(state.config);
const integration = selectIntegration(state, collection.get('name'), 'listEntries'); 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)); dispatch(entriesLoading(collection));
provider.listEntries(collection, page).then( provider.listEntries(collection, page).then(
response => dispatch(entriesLoaded(collection, response.entries, response.pagination)), response => dispatch(entriesLoaded(collection, response.entries, response.pagination)),
@ -196,11 +216,11 @@ export function persistEntry(collection, entryDraft) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const backend = currentBackend(state.config); 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 entry = entryDraft.get('entry');
dispatch(entryPersisting(collection, entry)); dispatch(entryPersisting(collection, entry));
backend backend
.persistEntry(state.config, collection, entryDraft, mediaProxies.toJS()) .persistEntry(state.config, collection, entryDraft, assetProxies.toJS())
.then(() => { .then(() => {
dispatch(notifSend({ dispatch(notifSend({
message: 'Entry saved', message: 'Entry saved',

View File

@ -1,10 +1,10 @@
export const ADD_MEDIA = 'ADD_MEDIA'; export const ADD_ASSET = 'ADD_ASSET';
export const REMOVE_MEDIA = 'REMOVE_MEDIA'; export const REMOVE_ASSET = 'REMOVE_ASSET';
export function addMedia(mediaProxy) { export function addAsset(assetProxy) {
return { type: ADD_MEDIA, payload: mediaProxy }; return { type: ADD_ASSET, payload: assetProxy };
} }
export function removeMedia(path) { export function removeAsset(path) {
return { type: REMOVE_MEDIA, payload: path }; return { type: REMOVE_ASSET, payload: path };
} }

View File

@ -109,7 +109,7 @@ export function searchEntries(searchTerm, page = 0) {
dispatch(searchFailure(searchTerm, 'Search integration is not configured.')); dispatch(searchFailure(searchTerm, 'Search integration is not configured.'));
} }
const provider = integration ? const provider = integration ?
getIntegrationProvider(state.integrations, integration) getIntegrationProvider(state.integrations, currentBackend(state.config).getToken, integration)
: currentBackend(state.config); : currentBackend(state.config);
dispatch(searchingEntries(searchTerm)); dispatch(searchingEntries(searchTerm));
provider.search(collections, searchTerm, page).then( 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.')); dispatch(searchFailure(namespace, searchTerm, 'Search integration is not configured.'));
} }
const provider = integration ? const provider = integration ?
getIntegrationProvider(state.integrations, integration) getIntegrationProvider(state.integrations, currentBackend(state.config).getToken, integration)
: currentBackend(state.config); : currentBackend(state.config);
dispatch(querying(namespace, collection, searchFields, searchTerm)); dispatch(querying(namespace, collection, searchFields, searchTerm));
provider.searchBy(searchFields, collection, searchTerm).then( provider.searchBy(searchFields, collection, searchTerm).then(

View File

@ -1,6 +1,5 @@
import TestRepoBackend from "./test-repo/implementation"; import TestRepoBackend from "./test-repo/implementation";
import GitHubBackend from "./github/implementation"; import GitHubBackend from "./github/implementation";
import NetlifyGitBackend from "./netlify-git/implementation";
import NetlifyAuthBackend from "./netlify-auth/implementation"; import NetlifyAuthBackend from "./netlify-auth/implementation";
import { resolveFormat } from "../formats/formats"; import { resolveFormat } from "../formats/formats";
import { selectListMethod, selectEntrySlug, selectEntryPath, selectAllowNewEntries } from "../reducers/collections"; import { selectListMethod, selectEntrySlug, selectEntryPath, selectAllowNewEntries } from "../reducers/collections";
@ -79,6 +78,8 @@ class Backend {
} }
} }
getToken = () => this.implementation.getToken();
listEntries(collection) { listEntries(collection) {
const listMethod = this.implementation[selectListMethod(collection)]; const listMethod = this.implementation[selectListMethod(collection)];
return listMethod.call(this.implementation, collection) return listMethod.call(this.implementation, collection)
@ -218,8 +219,6 @@ export function resolveBackend(config) {
return new Backend(new TestRepoBackend(config), authStore); return new Backend(new TestRepoBackend(config), authStore);
case "github": case "github":
return new Backend(new GitHubBackend(config), authStore); return new Backend(new GitHubBackend(config), authStore);
case "netlify-git":
return new Backend(new NetlifyGitBackend(config), authStore);
case "netlify-auth": case "netlify-auth":
return new Backend(new NetlifyAuthBackend(config), authStore); return new Backend(new NetlifyAuthBackend(config), authStore);
default: default:

View File

@ -1,7 +1,7 @@
import LocalForage from "localforage"; import LocalForage from "localforage";
import { Base64 } from "js-base64"; import { Base64 } from "js-base64";
import _ from "lodash"; import _ from "lodash";
import MediaProxy from "../../valueObjects/MediaProxy"; import AssetProxy from "../../valueObjects/AssetProxy";
import { SIMPLE, EDITORIAL_WORKFLOW, status } from "../../constants/publishModes"; import { SIMPLE, EDITORIAL_WORKFLOW, status } from "../../constants/publishModes";
export default class API { export default class API {
@ -356,7 +356,7 @@ export default class API {
} }
uploadBlob(item) { 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`, { return content.then(contentBase64 => this.request(`${ this.repoURL }/git/blobs`, {
method: "POST", method: "POST",

View File

@ -14,6 +14,7 @@ export default class GitHub {
this.repo = config.getIn(["backend", "repo"], ""); this.repo = config.getIn(["backend", "repo"], "");
this.branch = config.getIn(["backend", "branch"], "master"); this.branch = config.getIn(["backend", "branch"], "master");
this.token = '';
} }
authComponent() { authComponent() {
@ -21,17 +22,23 @@ export default class GitHub {
} }
setUser(user) { 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) { 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) => { return this.api.user().then((user) => {
user.token = state.token; user.token = state.token;
return user; return user;
}); });
} }
getToken() {
return Promise.resolve(this.token);
}
entriesByFolder(collection) { entriesByFolder(collection) {
return this.api.listFiles(collection.get("folder")) return this.api.listFiles(collection.get("folder"))
.then(this.fetchFiles); .then(this.fetchFiles);

View File

@ -4,20 +4,23 @@ export default class API extends GithubAPI {
constructor(config) { constructor(config) {
super(config); super(config);
this.api_root = config.api_root; this.api_root = config.api_root;
this.jwtToken = config.jwtToken; this.tokenPromise = config.tokenPromise;
this.commitAuthor = config.commitAuthor; this.commitAuthor = config.commitAuthor;
this.repoURL = ""; this.repoURL = "";
} }
requestHeaders(headers = {}) { getRequestHeaders(headers = {}) {
const baseHeader = { return this.tokenPromise()
Authorization: `Bearer ${ this.jwtToken }`, .then((jwtToken) => {
"Content-Type": "application/json", const baseHeader = {
...headers, "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 = {}) { request(path, options = {}) {
const headers = this.requestHeaders(options.headers || {});
const url = this.urlFor(path, options); 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"); const contentType = response.headers.get("Content-Type");
if (contentType && contentType.match(/json/)) { if (contentType && contentType.match(/json/)) {
return this.parseJsonResponse(response); return this.parseJsonResponse(response);

View File

@ -1,5 +1,5 @@
import NetlifyAuthClient from "netlify-auth-js"; import NetlifyAuthClient from "netlify-auth-js";
import { omit } from "lodash"; import { pick } from "lodash";
import GitHubBackend from "../github/implementation"; import GitHubBackend from "../github/implementation";
import API from "./API"; import API from "./API";
import AuthenticationPage from "./AuthenticationPage"; import AuthenticationPage from "./AuthenticationPage";
@ -25,20 +25,26 @@ export default class NetlifyAuth extends GitHubBackend {
setUser() { setUser() {
const user = this.authClient.currentUser(); const user = this.authClient.currentUser();
if (!user) return Promise.reject(); if (!user) return Promise.reject();
return this.authenticate(user); return this.authenticate(user);
} }
authenticate(user) { authenticate(user) {
return user.jwt().then((token) => { this.tokenPromise = user.jwt.bind(user);
const userData = { const userData = {
name: `${ user.user_metadata.firstname } ${ user.user_metadata.lastname }`, name: `${ user.user_metadata.firstname } ${ user.user_metadata.lastname }`,
email: user.email, email: user.email,
metadata: user.user_metadata, metadata: user.user_metadata,
}; };
this.api = new API({ api_root: this.github_proxy_url, jwtToken: token, commitAuthor: omit(userData, ["metadata"]) }); this.api = new API({
return userData; api_root: this.github_proxy_url,
tokenPromise: this.tokenPromise,
commitAuthor: pick(userData, ["name", "email"]),
}); });
return Promise.resolve(userData);
}
getToken() {
return this.tokenPromise();
} }
authComponent() { authComponent() {

View File

@ -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 })
});
}
}

View File

@ -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 <form onSubmit={this.handleLogin}>
{loginError && <p>{loginError}</p>}
<p>
<label>Your Email: <input type="email" onChange={this.handleChange('email')}/></label>
</p>
<p>
<label>Your Password: <input type="password" onChange={this.handleChange('password')}/></label>
</p>
<p>
<button>Login</button>
</p>
</form>;
}
}

View File

@ -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);
}
}

View File

@ -36,6 +36,10 @@ export default class TestRepo {
return Promise.resolve({ email: state.email, name: nameFromEmail(state.email) }); return Promise.resolve({ email: state.email, name: nameFromEmail(state.email) });
} }
getToken() {
return Promise.resolve('');
}
entriesByFolder(collection) { entriesByFolder(collection) {
const entries = []; const entries = [];
const folder = collection.get('folder'); const folder = collection.get('folder');
@ -67,7 +71,7 @@ export default class TestRepo {
getEntry(collection, slug, path) { getEntry(collection, slug, path) {
return Promise.resolve({ return Promise.resolve({
file: { path }, file: { path },
data: getFile(path).content data: getFile(path).content,
}); });
} }

View File

@ -10,7 +10,7 @@ function isHidden(field) {
export default class ControlPane extends Component { export default class ControlPane extends Component {
controlFor(field) { 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 widget = resolveWidget(field.get('widget'));
const fieldName = field.get('name'); const fieldName = field.get('name');
const value = entry.getIn(['data', fieldName]); const value = entry.getIn(['data', fieldName]);
@ -25,9 +25,9 @@ export default class ControlPane extends Component {
value, value,
metadata, metadata,
onChange: (newValue, newMetadata) => onChange(fieldName, newValue, newMetadata), onChange: (newValue, newMetadata) => onChange(fieldName, newValue, newMetadata),
onAddMedia, onAddAsset,
onRemoveMedia, onRemoveAsset,
getMedia, getAsset,
}) })
} }
</div> </div>
@ -60,8 +60,8 @@ ControlPane.propTypes = {
entry: ImmutablePropTypes.map.isRequired, entry: ImmutablePropTypes.map.isRequired,
fields: ImmutablePropTypes.list.isRequired, fields: ImmutablePropTypes.list.isRequired,
fieldsMetaData: ImmutablePropTypes.map.isRequired, fieldsMetaData: ImmutablePropTypes.map.isRequired,
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onRemoveMedia: PropTypes.func.isRequired, onRemoveAsset: PropTypes.func.isRequired,
}; };

View File

@ -26,10 +26,10 @@ class EntryEditor extends Component {
entry, entry,
fields, fields,
fieldsMetaData, fieldsMetaData,
getMedia, getAsset,
onChange, onChange,
onAddMedia, onAddAsset,
onRemoveMedia, onRemoveAsset,
onPersist, onPersist,
onCancelEdit, onCancelEdit,
} = this.props; } = this.props;
@ -53,10 +53,10 @@ class EntryEditor extends Component {
entry={entry} entry={entry}
fields={fields} fields={fields}
fieldsMetaData={fieldsMetaData} fieldsMetaData={fieldsMetaData}
getMedia={getMedia} getAsset={getAsset}
onChange={onChange} onChange={onChange}
onAddMedia={onAddMedia} onAddAsset={onAddAsset}
onRemoveMedia={onRemoveMedia} onRemoveAsset={onRemoveAsset}
/> />
</div> </div>
@ -67,7 +67,7 @@ class EntryEditor extends Component {
entry={entry} entry={entry}
fields={fields} fields={fields}
fieldsMetaData={fieldsMetaData} fieldsMetaData={fieldsMetaData}
getMedia={getMedia} getAsset={getAsset}
/> />
</div> </div>
</SplitPane> </SplitPane>
@ -91,11 +91,11 @@ EntryEditor.propTypes = {
entry: ImmutablePropTypes.map.isRequired, entry: ImmutablePropTypes.map.isRequired,
fields: ImmutablePropTypes.list.isRequired, fields: ImmutablePropTypes.list.isRequired,
fieldsMetaData: ImmutablePropTypes.map.isRequired, fieldsMetaData: ImmutablePropTypes.map.isRequired,
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onPersist: PropTypes.func.isRequired, onPersist: PropTypes.func.isRequired,
onRemoveMedia: PropTypes.func.isRequired, onRemoveAsset: PropTypes.func.isRequired,
onCancelEdit: PropTypes.func.isRequired, onCancelEdit: PropTypes.func.isRequired,
}; };

View File

@ -10,7 +10,7 @@ import htmlSyntax from 'markup-it/syntaxes/html';
import reInline from 'markup-it/syntaxes/markdown/re/inline'; import reInline from 'markup-it/syntaxes/markdown/re/inline';
import MarkupItReactRenderer from '../'; import MarkupItReactRenderer from '../';
function getMedia(path) { function getAsset(path) {
return path; return path;
} }
@ -21,7 +21,7 @@ describe('MarkitupReactRenderer', () => {
<MarkupItReactRenderer <MarkupItReactRenderer
value="# Title" value="# Title"
syntax={markdownSyntax} syntax={markdownSyntax}
getMedia={getMedia} getAsset={getAsset}
/> />
); );
const tree1 = component.html(); const tree1 = component.html();
@ -38,7 +38,7 @@ describe('MarkitupReactRenderer', () => {
<MarkupItReactRenderer <MarkupItReactRenderer
value="# Title" value="# Title"
syntax={markdownSyntax} syntax={markdownSyntax}
getMedia={getMedia} getAsset={getAsset}
/> />
); );
const syntax1 = component.instance().props.syntax; const syntax1 = component.instance().props.syntax;
@ -83,7 +83,7 @@ Text with **bold** & _em_ elements
<MarkupItReactRenderer <MarkupItReactRenderer
value={value} value={value}
syntax={markdownSyntax} syntax={markdownSyntax}
getMedia={getMedia} getAsset={getAsset}
/> />
); );
expect(component.html()).toMatchSnapshot(); expect(component.html()).toMatchSnapshot();
@ -98,7 +98,7 @@ Text with **bold** & _em_ elements
<MarkupItReactRenderer <MarkupItReactRenderer
value={value} value={value}
syntax={markdownSyntax} syntax={markdownSyntax}
getMedia={getMedia} getAsset={getAsset}
/> />
); );
expect(component.html()).toMatchSnapshot(); expect(component.html()).toMatchSnapshot();
@ -123,7 +123,7 @@ Text with **bold** & _em_ elements
<MarkupItReactRenderer <MarkupItReactRenderer
value={value} value={value}
syntax={markdownSyntax} syntax={markdownSyntax}
getMedia={getMedia} getAsset={getAsset}
/> />
); );
expect(component.html()).toMatchSnapshot(); expect(component.html()).toMatchSnapshot();
@ -143,7 +143,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
<MarkupItReactRenderer <MarkupItReactRenderer
value={value} value={value}
syntax={markdownSyntax} syntax={markdownSyntax}
getMedia={getMedia} getAsset={getAsset}
/> />
); );
expect(component.html()).toMatchSnapshot(); expect(component.html()).toMatchSnapshot();
@ -157,7 +157,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
<MarkupItReactRenderer <MarkupItReactRenderer
value={value} value={value}
syntax={markdownSyntax} syntax={markdownSyntax}
getMedia={getMedia} getAsset={getAsset}
/> />
); );
expect(component.html()).toMatchSnapshot(); expect(component.html()).toMatchSnapshot();
@ -169,7 +169,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
<MarkupItReactRenderer <MarkupItReactRenderer
value={value} value={value}
syntax={markdownSyntax} syntax={markdownSyntax}
getMedia={getMedia} getAsset={getAsset}
/> />
); );
expect(component.html()).toMatchSnapshot(); expect(component.html()).toMatchSnapshot();
@ -197,7 +197,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
<MarkupItReactRenderer <MarkupItReactRenderer
value={value} value={value}
syntax={markdownSyntax} syntax={markdownSyntax}
getMedia={getMedia} getAsset={getAsset}
/> />
); );
expect(component.html()).toMatchSnapshot(); 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} value={value}
syntax={myMarkdownSyntax} syntax={myMarkdownSyntax}
schema={myCustomSchema} schema={myCustomSchema}
getMedia={getMedia} getAsset={getAsset}
/> />
); );
expect(component.html()).toMatchSnapshot(); expect(component.html()).toMatchSnapshot();
@ -255,7 +255,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
<MarkupItReactRenderer <MarkupItReactRenderer
value={value} value={value}
syntax={htmlSyntax} syntax={htmlSyntax}
getMedia={getMedia} getAsset={getAsset}
/> />
); );
expect(component.html()).toMatchSnapshot(); expect(component.html()).toMatchSnapshot();

View File

@ -13,9 +13,7 @@ const defaultSchema = {
[BLOCKS.BLOCKQUOTE]: 'blockquote', [BLOCKS.BLOCKQUOTE]: 'blockquote',
[BLOCKS.PARAGRAPH]: 'p', [BLOCKS.PARAGRAPH]: 'p',
[BLOCKS.FOOTNOTE]: 'footnote', [BLOCKS.FOOTNOTE]: 'footnote',
[BLOCKS.HTML]: ({ token }) => { [BLOCKS.HTML]: ({ token }) => <div dangerouslySetInnerHTML={{ __html: token.get('raw') }} />,
return <div dangerouslySetInnerHTML={{ __html: token.get('raw') }} />;
},
[BLOCKS.HR]: 'hr', [BLOCKS.HR]: 'hr',
[BLOCKS.HEADING_1]: 'h1', [BLOCKS.HEADING_1]: 'h1',
[BLOCKS.HEADING_2]: 'h2', [BLOCKS.HEADING_2]: 'h2',
@ -63,10 +61,10 @@ export default class MarkupItReactRenderer extends React.Component {
} }
sanitizeProps(props) { sanitizeProps(props) {
const { getMedia } = this.props; const { getAsset } = this.props;
if (props.image) { 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); return omit(props, notAllowedAttributes);
@ -115,7 +113,7 @@ export default class MarkupItReactRenderer extends React.Component {
render() { render() {
const { value, schema, getMedia } = this.props; const { value, schema, getAsset } = this.props;
const content = this.parser.toContent(value); const content = this.parser.toContent(value);
return this.renderToken({ ...defaultSchema, ...schema }, content.get('token')); return this.renderToken({ ...defaultSchema, ...schema }, content.get('token'));
} }
@ -128,5 +126,5 @@ MarkupItReactRenderer.propTypes = {
PropTypes.string, PropTypes.string,
PropTypes.func, PropTypes.func,
])), ])),
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
}; };

View File

@ -24,6 +24,6 @@ Preview.propTypes = {
collection: ImmutablePropTypes.map.isRequired, collection: ImmutablePropTypes.map.isRequired,
entry: ImmutablePropTypes.map.isRequired, entry: ImmutablePropTypes.map.isRequired,
fields: ImmutablePropTypes.list.isRequired, fields: ImmutablePropTypes.list.isRequired,
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
widgetFor: PropTypes.func.isRequired, widgetFor: PropTypes.func.isRequired,
}; };

View File

@ -29,7 +29,7 @@ export default class PreviewPane extends React.Component {
} }
widgetFor = (name) => { 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); const field = fields.find(f => f.get('name') === name);
let value = entry.getIn(['data', field.get('name')]); let value = entry.getIn(['data', field.get('name')]);
const metadata = fieldsMetaData.get(field.get('name')); const metadata = fieldsMetaData.get(field.get('name'));
@ -46,7 +46,7 @@ export default class PreviewPane extends React.Component {
value, value,
field, field,
metadata, metadata,
getMedia, getAsset,
}); });
}; };
@ -72,6 +72,7 @@ export default class PreviewPane extends React.Component {
renderPreview() { renderPreview() {
const { entry, collection } = this.props; const { entry, collection } = this.props;
if (!entry || !entry.get('data')) return;
const component = registry.getPreviewTemplate(selectTemplateName(collection, entry.get('slug'))) || Preview; const component = registry.getPreviewTemplate(selectTemplateName(collection, entry.get('slug'))) || Preview;
this.inferFields(); this.inferFields();
@ -104,5 +105,5 @@ PreviewPane.propTypes = {
fields: ImmutablePropTypes.list.isRequired, fields: ImmutablePropTypes.list.isRequired,
entry: ImmutablePropTypes.map.isRequired, entry: ImmutablePropTypes.map.isRequired,
fieldsMetaData: ImmutablePropTypes.map.isRequired, fieldsMetaData: ImmutablePropTypes.map.isRequired,
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
}; };

View File

@ -13,6 +13,8 @@ import MarkdownControl from './Widgets/MarkdownControl';
import MarkdownPreview from './Widgets/MarkdownPreview'; import MarkdownPreview from './Widgets/MarkdownPreview';
import ImageControl from './Widgets/ImageControl'; import ImageControl from './Widgets/ImageControl';
import ImagePreview from './Widgets/ImagePreview'; import ImagePreview from './Widgets/ImagePreview';
import FileControl from './Widgets/FileControl';
import FilePreview from './Widgets/FilePreview';
import DateControl from './Widgets/DateControl'; import DateControl from './Widgets/DateControl';
import DatePreview from './Widgets/DatePreview'; import DatePreview from './Widgets/DatePreview';
import DateTimeControl from './Widgets/DateTimeControl'; import DateTimeControl from './Widgets/DateTimeControl';
@ -31,6 +33,7 @@ registry.registerWidget('number', NumberControl, NumberPreview);
registry.registerWidget('list', ListControl, ListPreview); registry.registerWidget('list', ListControl, ListPreview);
registry.registerWidget('markdown', MarkdownControl, MarkdownPreview); registry.registerWidget('markdown', MarkdownControl, MarkdownPreview);
registry.registerWidget('image', ImageControl, ImagePreview); registry.registerWidget('image', ImageControl, ImagePreview);
registry.registerWidget('file', FileControl, FilePreview);
registry.registerWidget('date', DateControl, DatePreview); registry.registerWidget('date', DateControl, DatePreview);
registry.registerWidget('datetime', DateTimeControl, DateTimePreview); registry.registerWidget('datetime', DateTimeControl, DateTimePreview);
registry.registerWidget('select', SelectControl, SelectPreview); registry.registerWidget('select', SelectControl, SelectPreview);

View File

@ -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 (
<div style={styles.imageUpload}>
<span style={styles.message}>
<Loader active />
</span>
</div>
);
}
return (
<div
style={styles.imageUpload}
onDragEnter={this.handleDragEnter}
onDragOver={this.handleDragOver}
onDrop={this.handleChange}
>
<span style={styles.message} onClick={this.handleClick}>
{fileName ? fileName : 'Tip: Click here to select a file to upload, or drag an image directly into this box from your desktop'}
</span>
<input
type="file"
onChange={this.handleChange}
style={styles.input}
ref={this.handleFileInputRef}
/>
</div>
);
}
}
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,
};

View File

@ -0,0 +1,15 @@
import React, { PropTypes } from 'react';
import previewStyle from './defaultPreviewStyle';
export default function FilePreview({ value, getAsset }) {
return (<div style={previewStyle}>
{ value ?
<a href={getAsset(value)}>{ value }</a>
: null}
</div>);
}
FilePreview.propTypes = {
getAsset: PropTypes.func.isRequired,
value: PropTypes.node,
};

View File

@ -1,10 +1,15 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { truncateMiddle } from '../../lib/textHelper'; 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; const MAX_DISPLAY_LENGTH = 50;
export default class ImageControl extends React.Component { export default class ImageControl extends React.Component {
state = {
processing: false,
};
handleFileInputRef = (el) => { handleFileInputRef = (el) => {
this._fileInput = 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) { if (file) {
const mediaProxy = new MediaProxy(file.name, file); this.setState({ processing: true });
this.props.onAddMedia(mediaProxy); createAssetProxy(file.name, file)
this.props.onChange(mediaProxy.public_path); .then((assetProxy) => {
this.setState({ processing: false });
this.props.onAddAsset(assetProxy);
this.props.onChange(assetProxy.public_path);
});
} else { } else {
this.props.onChange(null); this.props.onChange(null);
} }
@ -50,7 +59,7 @@ export default class ImageControl extends React.Component {
renderImageName = () => { renderImageName = () => {
if (!this.props.value) return null; 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); return truncateMiddle(this.props.value.path, MAX_DISPLAY_LENGTH);
} else { } else {
return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH); return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH);
@ -58,14 +67,25 @@ export default class ImageControl extends React.Component {
}; };
render() { render() {
const { processing } = this.state;
const imageName = this.renderImageName(); const imageName = this.renderImageName();
if (processing) {
return (
<div style={styles.imageUpload}>
<span style={styles.message}>
<Loader active />
</span>
</div>
);
}
return ( return (
<div <div
style={styles.imageUpload}
onDragEnter={this.handleDragEnter} onDragEnter={this.handleDragEnter}
onDragOver={this.handleDragOver} onDragOver={this.handleDragOver}
onDrop={this.handleChange} onDrop={this.handleChange}
> >
<span style={styles.imageUpload} onClick={this.handleClick}> <span style={styles.message} onClick={this.handleClick}>
{imageName ? imageName : 'Tip: Click here to upload an image from your file browser, or drag an image directly into this box from your desktop'} {imageName ? imageName : 'Tip: Click here to upload an image from your file browser, or drag an image directly into this box from your desktop'}
</span> </span>
<input <input
@ -84,21 +104,23 @@ const styles = {
input: { input: {
display: 'none', display: 'none',
}, },
message: {
padding: '20px',
display: 'block',
fontSize: '12px',
},
imageUpload: { imageUpload: {
backgroundColor: '#fff', backgroundColor: '#fff',
textAlign: 'center', textAlign: 'center',
color: '#999', color: '#999',
padding: '20px',
display: 'block',
border: '1px dashed #eee', border: '1px dashed #eee',
cursor: 'pointer', cursor: 'pointer',
fontSize: '12px',
}, },
}; };
ImageControl.propTypes = { ImageControl.propTypes = {
onAddMedia: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onRemoveMedia: PropTypes.func.isRequired, onRemoveAsset: PropTypes.func.isRequired,
value: PropTypes.node, value: PropTypes.node,
}; };

View File

@ -1,11 +1,11 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import previewStyle, { imagePreviewStyle } from './defaultPreviewStyle'; import previewStyle, { imagePreviewStyle } from './defaultPreviewStyle';
export default function ImagePreview({ value, getMedia }) { export default function ImagePreview({ value, getAsset }) {
return (<div style={previewStyle}> return (<div style={previewStyle}>
{ value ? { value ?
<img <img
src={getMedia(value)} src={getAsset(value)}
style={imagePreviewStyle} style={imagePreviewStyle}
role="presentation" role="presentation"
/> />
@ -14,6 +14,6 @@ export default function ImagePreview({ value, getMedia }) {
} }
ImagePreview.propTypes = { ImagePreview.propTypes = {
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
value: PropTypes.node, value: PropTypes.node,
}; };

View File

@ -29,9 +29,9 @@ export default class ListControl extends Component {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
value: PropTypes.node, value: PropTypes.node,
field: PropTypes.node, field: PropTypes.node,
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired,
onRemoveMedia: PropTypes.func.isRequired, onRemoveAsset: PropTypes.func.isRequired,
}; };
constructor(props) { constructor(props) {
@ -125,7 +125,7 @@ export default class ListControl extends Component {
}; };
renderItem(item, index) { renderItem(item, index) {
const { value, field, getMedia, onAddMedia, onRemoveMedia } = this.props; const { value, field, getAsset, onAddAsset, onRemoveAsset } = this.props;
const { itemStates } = this.state; const { itemStates } = this.state;
const collapsed = itemStates.getIn([index, 'collapsed']); const collapsed = itemStates.getIn([index, 'collapsed']);
const classNames = [styles.item, collapsed ? styles.collapsed : styles.expanded]; const classNames = [styles.item, collapsed ? styles.collapsed : styles.expanded];
@ -145,9 +145,9 @@ export default class ListControl extends Component {
value={item} value={item}
field={field} field={field}
onChange={this.handleChangeFor(index)} onChange={this.handleChangeFor(index)}
getMedia={getMedia} getAsset={getAsset}
onAddMedia={onAddMedia} onAddAsset={onAddAsset}
onRemoveMedia={onRemoveMedia} onRemoveAsset={onRemoveAsset}
/> />
</div> </div>
<button className={styles.toggleButton} onClick={this.handleToggle(index)}> <button className={styles.toggleButton} onClick={this.handleToggle(index)}>

View File

@ -4,13 +4,13 @@ import previewStyle from './defaultPreviewStyle';
export default class ListPreview extends Component { export default class ListPreview extends Component {
widgetFor = (field, value) => { widgetFor = (field, value) => {
const { getMedia } = this.props; const { getAsset } = this.props;
const widget = resolveWidget(field.get('widget')); const widget = resolveWidget(field.get('widget'));
return (<div key={field.get('name')}>{React.createElement(widget.preview, { return (<div key={field.get('name')}>{React.createElement(widget.preview, {
key: field.get('name'), key: field.get('name'),
value: value && value.get(field.get('name')), value: value && value.get(field.get('name')),
field, field,
getMedia, getAsset,
})}</div>); })}</div>);
}; };
@ -32,5 +32,5 @@ export default class ListPreview extends Component {
ListPreview.propTypes = { ListPreview.propTypes = {
value: PropTypes.node, value: PropTypes.node,
field: PropTypes.node, field: PropTypes.node,
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
}; };

View File

@ -12,8 +12,8 @@ class MarkdownControl extends React.Component {
static propTypes = { static propTypes = {
editor: PropTypes.object.isRequired, editor: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
switchVisualMode: PropTypes.func.isRequired, switchVisualMode: PropTypes.func.isRequired,
value: PropTypes.node, value: PropTypes.node,
}; };
@ -33,17 +33,17 @@ class MarkdownControl extends React.Component {
}; };
render() { render() {
const { onChange, onAddMedia, onRemoveMedia, getMedia, value } = this.props; const { onChange, onAddAsset, onRemoveAsset, getAsset, value } = this.props;
const { mode } = this.state; const { mode } = this.state;
if (mode === 'visual') { if (mode === 'visual') {
return ( return (
<div className="cms-editor-visual"> <div className="cms-editor-visual">
<VisualEditor <VisualEditor
onChange={onChange} onChange={onChange}
onAddMedia={onAddMedia} onAddAsset={onAddAsset}
onRemoveMedia={onRemoveMedia} onRemoveAsset={onRemoveAsset}
onMode={this.handleMode} onMode={this.handleMode}
getMedia={getMedia} getAsset={getAsset}
value={value} value={value}
/> />
</div> </div>
@ -54,10 +54,10 @@ class MarkdownControl extends React.Component {
<div className="cms-editor-raw"> <div className="cms-editor-raw">
<RawEditor <RawEditor
onChange={onChange} onChange={onChange}
onAddMedia={onAddMedia} onAddAsset={onAddAsset}
onRemoveMedia={onRemoveMedia} onRemoveAsset={onRemoveAsset}
onMode={this.handleMode} onMode={this.handleMode}
getMedia={getMedia} getAsset={getAsset}
value={value} value={value}
/> />
</div> </div>

View File

@ -10,9 +10,9 @@ export default class BlockMenu extends Component {
selectionPosition: PropTypes.object, selectionPosition: PropTypes.object,
plugins: PropTypes.object.isRequired, plugins: PropTypes.object.isRequired,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired,
onRemoveMedia: PropTypes.func.isRequired, onRemoveAsset: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
}; };
constructor(props) { constructor(props) {
@ -67,7 +67,7 @@ export default class BlockMenu extends Component {
}; };
controlFor(field) { controlFor(field) {
const { onAddMedia, onRemoveMedia, getMedia } = this.props; const { onAddAsset, onRemoveAsset, getAsset } = this.props;
const { pluginData } = this.state; const { pluginData } = this.state;
const widget = resolveWidget(field.get('widget') || 'string'); const widget = resolveWidget(field.get('widget') || 'string');
const value = pluginData.get(field.get('name')); const value = pluginData.get(field.get('name'));
@ -84,9 +84,9 @@ export default class BlockMenu extends Component {
pluginData: pluginData.set(field.get('name'), val), pluginData: pluginData.set(field.get('name'), val),
}); });
}, },
onAddMedia, onAddAsset,
onRemoveMedia, onRemoveAsset,
getMedia, getAsset,
}) })
} }
</div> </div>

View File

@ -4,7 +4,7 @@ import markdownSyntax from 'markup-it/syntaxes/markdown';
import htmlSyntax from 'markup-it/syntaxes/html'; import htmlSyntax from 'markup-it/syntaxes/html';
import CaretPosition from 'textarea-caret-position'; import CaretPosition from 'textarea-caret-position';
import registry from '../../../../lib/registry'; import registry from '../../../../lib/registry';
import MediaProxy from '../../../../valueObjects/MediaProxy'; import { createAssetProxy } from '../../../../valueObjects/AssetProxy';
import Toolbar from '../Toolbar'; import Toolbar from '../Toolbar';
import BlockMenu from '../BlockMenu'; import BlockMenu from '../BlockMenu';
import styles from './index.css'; import styles from './index.css';
@ -271,12 +271,16 @@ export default class RawEditor extends React.Component {
if (e.dataTransfer.files && e.dataTransfer.files.length) { if (e.dataTransfer.files && e.dataTransfer.files.length) {
data = Array.from(e.dataTransfer.files).map((file) => { data = Array.from(e.dataTransfer.files).map((file) => {
const mediaProxy = new MediaProxy(file.name, file); const link = `[Uploading ${ file.name }...]()`;
this.props.onAddMedia(mediaProxy);
const link = `[${ file.name }](${ mediaProxy.public_path })`;
if (file.type.split('/')[0] === 'image') { if (file.type.split('/')[0] === 'image') {
return `!${ link }`; return `!${ link }`;
} }
createAssetProxy(file.name, file)
.then((assetProxy) => {
this.props.onAddAsset(assetProxy);
// TODO: Change the link text
});
return link; return link;
}).join('\n\n'); }).join('\n\n');
} else { } else {
@ -304,7 +308,7 @@ export default class RawEditor extends React.Component {
}; };
render() { render() {
const { onAddMedia, onRemoveMedia, getMedia } = this.props; const { onAddAsset, onRemoveAsset, getAsset } = this.props;
const { showToolbar, showBlockMenu, plugins, selectionPosition, dragging } = this.state; const { showToolbar, showBlockMenu, plugins, selectionPosition, dragging } = this.state;
const classNames = [styles.root]; const classNames = [styles.root];
if (dragging) { if (dragging) {
@ -333,9 +337,9 @@ export default class RawEditor extends React.Component {
selectionPosition={selectionPosition} selectionPosition={selectionPosition}
plugins={plugins} plugins={plugins}
onBlock={this.handleBlock} onBlock={this.handleBlock}
onAddMedia={onAddMedia} onAddAsset={onAddAsset}
onRemoveMedia={onRemoveMedia} onRemoveAsset={onRemoveAsset}
getMedia={getMedia} getAsset={getAsset}
/> />
<textarea <textarea
ref={this.handleRef} ref={this.handleRef}
@ -344,15 +348,15 @@ export default class RawEditor extends React.Component {
onChange={this.handleChange} onChange={this.handleChange}
onSelect={this.handleSelection} onSelect={this.handleSelection}
/> />
<div className={styles.shim}/> <div className={styles.shim} />
</div>); </div>);
} }
} }
RawEditor.propTypes = { RawEditor.propTypes = {
onAddMedia: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired,
onRemoveMedia: PropTypes.func.isRequired, onRemoveAsset: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onMode: PropTypes.func.isRequired, onMode: PropTypes.func.isRequired,
value: PropTypes.node, value: PropTypes.node,

View File

@ -11,7 +11,7 @@ import { keymap } from 'prosemirror-keymap';
import { schema, defaultMarkdownSerializer } from 'prosemirror-markdown'; import { schema, defaultMarkdownSerializer } from 'prosemirror-markdown';
import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands'; import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands';
import registry from '../../../../lib/registry'; import registry from '../../../../lib/registry';
import MediaProxy from '../../../../valueObjects/MediaProxy'; import { createAssetProxy } from '../../../../valueObjects/AssetProxy';
import { buildKeymap } from './keymap'; import { buildKeymap } from './keymap';
import createMarkdownParser from './parser'; import createMarkdownParser from './parser';
import Toolbar from '../Toolbar'; import Toolbar from '../Toolbar';
@ -89,7 +89,7 @@ function createSerializer(schema, plugins) {
plugins.forEach((plugin) => { plugins.forEach((plugin) => {
serializer.nodes[`plugin_${ plugin.get('id') }`] = (state, node) => { serializer.nodes[`plugin_${ plugin.get('id') }`] = (state, node) => {
const toBlock = plugin.get('toBlock'); const toBlock = plugin.get('toBlock');
state.write(toBlock.call(plugin, node.attrs) + '\n\n'); state.write(`${ toBlock.call(plugin, node.attrs) }\n\n`);
}; };
}); });
return serializer; return serializer;
@ -239,17 +239,19 @@ export default class Editor extends Component {
if (e.dataTransfer.files && e.dataTransfer.files.length) { if (e.dataTransfer.files && e.dataTransfer.files.length) {
Array.from(e.dataTransfer.files).forEach((file) => { Array.from(e.dataTransfer.files).forEach((file) => {
const mediaProxy = new MediaProxy(file.name, file); createAssetProxy(file.name, file)
this.props.onAddMedia(mediaProxy); .then((assetProxy) => {
if (file.type.split('/')[0] === 'image') { this.props.onAddAsset(assetProxy);
nodes.push( if (file.type.split('/')[0] === 'image') {
schema.nodes.image.create({ src: mediaProxy.public_path, alt: file.name }) nodes.push(
); schema.nodes.image.create({ src: assetProxy.public_path, alt: file.name })
} else { );
nodes.push( } else {
schema.marks.link.create({ href: mediaProxy.public_path, title: file.name }) nodes.push(
); schema.marks.link.create({ href: assetProxy.public_path, title: file.name })
} );
}
});
}); });
} else { } else {
nodes.push(schema.nodes.paragraph.create({}, e.dataTransfer.getData('text/plain'))); nodes.push(schema.nodes.paragraph.create({}, e.dataTransfer.getData('text/plain')));
@ -265,7 +267,7 @@ export default class Editor extends Component {
}; };
render() { render() {
const { onAddMedia, onRemoveMedia, getMedia } = this.props; const { onAddAsset, onRemoveAsset, getAsset } = this.props;
const { plugins, showToolbar, showBlockMenu, selectionPosition, dragging } = this.state; const { plugins, showToolbar, showBlockMenu, selectionPosition, dragging } = this.state;
const classNames = [styles.editor]; const classNames = [styles.editor];
if (dragging) { if (dragging) {
@ -294,9 +296,9 @@ export default class Editor extends Component {
selectionPosition={selectionPosition} selectionPosition={selectionPosition}
plugins={plugins} plugins={plugins}
onBlock={this.handleBlock} onBlock={this.handleBlock}
onAddMedia={onAddMedia} onAddAsset={onAddAsset}
onRemoveMedia={onRemoveMedia} onRemoveAsset={onRemoveAsset}
getMedia={getMedia} getAsset={getAsset}
/> />
<div ref={this.handleRef} /> <div ref={this.handleRef} />
<div className={styles.shim} /> <div className={styles.shim} />
@ -305,9 +307,9 @@ export default class Editor extends Component {
} }
Editor.propTypes = { Editor.propTypes = {
onAddMedia: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired,
onRemoveMedia: PropTypes.func.isRequired, onRemoveAsset: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onMode: PropTypes.func.isRequired, onMode: PropTypes.func.isRequired,
value: PropTypes.node, value: PropTypes.node,

View File

@ -3,7 +3,7 @@ import { getSyntaxes } from './richText';
import MarkupItReactRenderer from '../MarkupItReactRenderer/index'; import MarkupItReactRenderer from '../MarkupItReactRenderer/index';
import previewStyle from './defaultPreviewStyle'; import previewStyle from './defaultPreviewStyle';
const MarkdownPreview = ({ value, getMedia }) => { const MarkdownPreview = ({ value, getAsset }) => {
if (value == null) { if (value == null) {
return null; return null;
} }
@ -11,7 +11,7 @@ const MarkdownPreview = ({ value, getMedia }) => {
const schema = { const schema = {
'mediaproxy': ({ token }) => ( // eslint-disable-line 'mediaproxy': ({ token }) => ( // eslint-disable-line
<img <img
src={getMedia(token.getIn(['data', 'src']))} src={getAsset(token.getIn(['data', 'src']))}
alt={token.getIn(['data', 'alt'])} alt={token.getIn(['data', 'alt'])}
/> />
), ),
@ -24,14 +24,14 @@ const MarkdownPreview = ({ value, getMedia }) => {
value={value} value={value}
syntax={markdown} syntax={markdown}
schema={schema} schema={schema}
getMedia={getMedia} getAsset={getAsset}
/> />
</div> </div>
); );
}; };
MarkdownPreview.propTypes = { MarkdownPreview.propTypes = {
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
value: PropTypes.string, value: PropTypes.string,
}; };

View File

@ -7,14 +7,14 @@ import styles from './ObjectControl.css';
export default class ObjectControl extends Component { export default class ObjectControl extends Component {
static propTypes = { static propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
value: PropTypes.node, value: PropTypes.node,
field: PropTypes.node, field: PropTypes.node,
}; };
controlFor(field) { controlFor(field) {
const { onAddMedia, onRemoveMedia, getMedia, value, onChange } = this.props; const { onAddAsset, onRemoveAsset, getAsset, value, onChange } = this.props;
const widget = resolveWidget(field.get('widget') || 'string'); const widget = resolveWidget(field.get('widget') || 'string');
const fieldValue = value && Map.isMap(value) ? value.get(field.get('name')) : value; const fieldValue = value && Map.isMap(value) ? value.get(field.get('name')) : value;
@ -28,9 +28,9 @@ export default class ObjectControl extends Component {
onChange: (val, metadata) => { onChange: (val, metadata) => {
onChange((value || Map()).set(field.get('name'), val), metadata); onChange((value || Map()).set(field.get('name'), val), metadata);
}, },
onAddMedia, onAddAsset,
onRemoveMedia, onRemoveAsset,
getMedia, getAsset,
}) })
} }
</div> </div>

View File

@ -4,7 +4,7 @@ import previewStyle from './defaultPreviewStyle';
export default class ObjectPreview extends Component { export default class ObjectPreview extends Component {
widgetFor = (field) => { widgetFor = (field) => {
const { value, getMedia } = this.props; const { value, getAsset } = this.props;
const widget = resolveWidget(field.get('widget')); const widget = resolveWidget(field.get('widget'));
return ( return (
<div key={field.get('name')}> <div key={field.get('name')}>
@ -12,7 +12,7 @@ export default class ObjectPreview extends Component {
key: field.get('name'), key: field.get('name'),
value: value && value.get(field.get('name')), value: value && value.get(field.get('name')),
field, field,
getMedia, getAsset,
})} })}
</div> </div>
); );
@ -29,5 +29,5 @@ export default class ObjectPreview extends Component {
ObjectPreview.propTypes = { ObjectPreview.propTypes = {
value: PropTypes.node, value: PropTypes.node,
field: PropTypes.node, field: PropTypes.node,
getMedia: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
}; };

View File

@ -59,8 +59,8 @@ function processEditorPlugins(plugins) {
processedPlugins = plugins; processedPlugins = plugins;
} }
function processMediaProxyPlugins(getMedia) { function processAssetProxyPlugins(getAsset) {
const mediaProxyRule = MarkupIt.Rule('mediaproxy').regExp(reInline.link, (state, match) => { const assetProxyRule = MarkupIt.Rule('assetproxy').regExp(reInline.link, (state, match) => {
if (match[0].charAt(0) !== '!') { if (match[0].charAt(0) !== '!') {
// Return if this is not an image // Return if this is not an image
return; return;
@ -76,7 +76,7 @@ function processMediaProxyPlugins(getMedia) {
data: imgData, data: imgData,
}; };
}); });
const mediaProxyMarkdownRule = mediaProxyRule.toText((state, token) => { const assetProxyMarkdownRule = assetProxyRule.toText((state, token) => {
const data = token.getData(); const data = token.getData();
const alt = data.get('alt', ''); const alt = data.get('alt', '');
const src = data.get('src', ''); const src = data.get('src', '');
@ -88,25 +88,25 @@ function processMediaProxyPlugins(getMedia) {
return `![${ alt }](${ src })`; return `![${ alt }](${ src })`;
} }
}); });
const mediaProxyHTMLRule = mediaProxyRule.toText((state, token) => { const assetProxyHTMLRule = assetProxyRule.toText((state, token) => {
const data = token.getData(); const data = token.getData();
const alt = data.get('alt', ''); const alt = data.get('alt', '');
const src = data.get('src', ''); const src = data.get('src', '');
return `<img src=${ getMedia(src) } alt=${ alt } />`; return `<img src=${ getAsset(src) } alt=${ alt } />`;
}); });
nodes.mediaproxy = (props) => { nodes.assetproxy = (props) => {
/* eslint react/prop-types: 0 */ /* eslint react/prop-types: 0 */
const { node, state } = props; const { node, state } = props;
const isFocused = state.selection.hasEdgeIn(node); const isFocused = state.selection.hasEdgeIn(node);
const className = isFocused ? 'active' : null; const className = isFocused ? 'active' : null;
const src = node.data.get('src'); const src = node.data.get('src');
return ( return (
<img {...props.attributes} src={getMedia(src)} className={className} /> <img {...props.attributes} src={getAsset(src)} className={className} />
); );
}; };
augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(mediaProxyMarkdownRule); augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(assetProxyMarkdownRule);
augmentedHTMLSyntax = augmentedHTMLSyntax.addInlineRules(mediaProxyHTMLRule); augmentedHTMLSyntax = augmentedHTMLSyntax.addInlineRules(assetProxyHTMLRule);
} }
function getPlugins() { function getPlugins() {
@ -121,9 +121,9 @@ function getNodes() {
return nodes; return nodes;
} }
function getSyntaxes(getMedia) { function getSyntaxes(getAsset) {
if (getMedia) { if (getAsset) {
processMediaProxyPlugins(getMedia); processAssetProxyPlugins(getAsset);
} }
return { markdown: augmentedMarkdownSyntax, html: augmentedHTMLSyntax }; return { markdown: augmentedMarkdownSyntax, html: augmentedHTMLSyntax };
} }

View File

@ -19,7 +19,7 @@ const htmlContent = `
</ol> </ol>
`; `;
function getMedia(path) { function getAsset(path) {
return path; return path;
} }
@ -28,13 +28,13 @@ storiesOf('MarkupItReactRenderer', module)
<MarkupItReactRenderer <MarkupItReactRenderer
value={mdContent} value={mdContent}
syntax={markdownSyntax} syntax={markdownSyntax}
getMedia={getMedia} getAsset={getAsset}
/> />
)).add('HTML', () => ( )).add('HTML', () => (
<MarkupItReactRenderer <MarkupItReactRenderer
value={htmlContent} value={htmlContent}
syntax={htmlSyntax} syntax={htmlSyntax}
getMedia={getMedia} getAsset={getAsset}
/> />
)); ));

View File

@ -10,9 +10,9 @@ import {
persistEntry, persistEntry,
} from '../actions/entries'; } from '../actions/entries';
import { cancelEdit } from '../actions/editor'; import { cancelEdit } from '../actions/editor';
import { addMedia, removeMedia } from '../actions/media'; import { addAsset, removeAsset } from '../actions/media';
import { openSidebar } from '../actions/globalUI'; import { openSidebar } from '../actions/globalUI';
import { selectEntry, getMedia } from '../reducers'; import { selectEntry, getAsset } from '../reducers';
import { selectFields } from '../reducers/collections'; import { selectFields } from '../reducers/collections';
import EntryEditor from '../components/EntryEditor/EntryEditor'; import EntryEditor from '../components/EntryEditor/EntryEditor';
import entryPageHOC from './editorialWorkflow/EntryPageHOC'; import entryPageHOC from './editorialWorkflow/EntryPageHOC';
@ -20,8 +20,8 @@ import { Loader } from '../components/UI';
class EntryPage extends React.Component { class EntryPage extends React.Component {
static propTypes = { static propTypes = {
addMedia: PropTypes.func.isRequired, addAsset: PropTypes.func.isRequired,
boundGetMedia: PropTypes.func.isRequired, boundGetAsset: PropTypes.func.isRequired,
changeDraftField: PropTypes.func.isRequired, changeDraftField: PropTypes.func.isRequired,
collection: ImmutablePropTypes.map.isRequired, collection: ImmutablePropTypes.map.isRequired,
createDraftFromEntry: PropTypes.func.isRequired, createDraftFromEntry: PropTypes.func.isRequired,
@ -31,7 +31,7 @@ class EntryPage extends React.Component {
entryDraft: ImmutablePropTypes.map.isRequired, entryDraft: ImmutablePropTypes.map.isRequired,
loadEntry: PropTypes.func.isRequired, loadEntry: PropTypes.func.isRequired,
persistEntry: PropTypes.func.isRequired, persistEntry: PropTypes.func.isRequired,
removeMedia: PropTypes.func.isRequired, removeAsset: PropTypes.func.isRequired,
cancelEdit: PropTypes.func.isRequired, cancelEdit: PropTypes.func.isRequired,
openSidebar: PropTypes.func.isRequired, openSidebar: PropTypes.func.isRequired,
fields: ImmutablePropTypes.list.isRequired, fields: ImmutablePropTypes.list.isRequired,
@ -52,7 +52,7 @@ class EntryPage extends React.Component {
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (this.props.entry === nextProps.entry) return; if (this.props.entry === nextProps.entry) return;
if (nextProps.entry && !nextProps.entry.get('isFetching')) { if (nextProps.entry && !nextProps.entry.get('isFetching') && !nextProps.entry.get('error')) {
this.createDraft(nextProps.entry); this.createDraft(nextProps.entry);
} else if (nextProps.newEntry) { } else if (nextProps.newEntry) {
this.props.createEmptyDraft(nextProps.collection); this.props.createEmptyDraft(nextProps.collection);
@ -77,30 +77,32 @@ class EntryPage extends React.Component {
entry, entry,
entryDraft, entryDraft,
fields, fields,
boundGetMedia, boundGetAsset,
collection, collection,
changeDraftField, changeDraftField,
addMedia, addAsset,
removeMedia, removeAsset,
cancelEdit, cancelEdit,
} = this.props; } = this.props;
if (entry && entry.get('error')) {
if (entryDraft == null return <div><h3>{ entry.get('error') }</h3></div>;
} else if (entryDraft == null
|| entryDraft.get('entry') === undefined || entryDraft.get('entry') === undefined
|| (entry && entry.get('isFetching'))) { || (entry && entry.get('isFetching'))) {
return <Loader active>Loading entry...</Loader>; return <Loader active>Loading entry...</Loader>;
} }
return ( return (
<EntryEditor <EntryEditor
entry={entryDraft.get('entry')} entry={entryDraft.get('entry')}
getMedia={boundGetMedia} getAsset={boundGetAsset}
collection={collection} collection={collection}
fields={fields} fields={fields}
fieldsMetaData={entryDraft.get('fieldsMetaData')} fieldsMetaData={entryDraft.get('fieldsMetaData')}
onChange={changeDraftField} onChange={changeDraftField}
onAddMedia={addMedia} onAddAsset={addAsset}
onRemoveMedia={removeMedia} onRemoveAsset={removeAsset}
onPersist={this.handlePersistEntry} onPersist={this.handlePersistEntry}
onCancelEdit={cancelEdit} onCancelEdit={cancelEdit}
/> />
@ -115,13 +117,13 @@ function mapStateToProps(state, ownProps) {
const newEntry = ownProps.route && ownProps.route.newRecord === true; const newEntry = ownProps.route && ownProps.route.newRecord === true;
const fields = selectFields(collection, slug); const fields = selectFields(collection, slug);
const entry = newEntry ? null : selectEntry(state, collection.get('name'), slug); const entry = newEntry ? null : selectEntry(state, collection.get('name'), slug);
const boundGetMedia = getMedia.bind(null, state); const boundGetAsset = getAsset.bind(null, state);
return { return {
collection, collection,
collections, collections,
newEntry, newEntry,
entryDraft, entryDraft,
boundGetMedia, boundGetAsset,
fields, fields,
slug, slug,
entry, entry,
@ -132,8 +134,8 @@ export default connect(
mapStateToProps, mapStateToProps,
{ {
changeDraftField, changeDraftField,
addMedia, addAsset,
removeMedia, removeAsset,
loadEntry, loadEntry,
createDraftFromEntry, createDraftFromEntry,
createEmptyDraft, createEmptyDraft,

View File

@ -1,38 +1,38 @@
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import moment from 'moment'; import moment from 'moment';
import MediaProxy from '../valueObjects/MediaProxy'; import AssetProxy from '../valueObjects/AssetProxy';
const MomentType = new yaml.Type('date', { const MomentType = new yaml.Type('date', {
kind: 'scalar', kind: 'scalar',
predicate: function(value) { predicate(value) {
return moment.isMoment(value); return moment.isMoment(value);
}, },
represent: function(value) { represent(value) {
return value.format(value._f); return value.format(value._f);
}, },
resolve: function(value) { resolve(value) {
return moment.isMoment(value) && value._f; return moment.isMoment(value) && value._f;
} },
}); });
const ImageType = new yaml.Type('image', { const ImageType = new yaml.Type('image', {
kind: 'scalar', kind: 'scalar',
instanceOf: MediaProxy, instanceOf: AssetProxy,
represent: function(value) { represent(value) {
return `${value.path}`; return `${ value.path }`;
}, },
resolve: function(value) { resolve(value) {
if (value === null) return false; if (value === null) return false;
if (value instanceof MediaProxy) return true; if (value instanceof AssetProxy) return true;
return false; return false;
} },
}); });
const OutputSchema = new yaml.Schema({ const OutputSchema = new yaml.Schema({
include: yaml.DEFAULT_SAFE_SCHEMA.include, include: yaml.DEFAULT_SAFE_SCHEMA.include,
implicit: [MomentType, ImageType].concat(yaml.DEFAULT_SAFE_SCHEMA.implicit), implicit: [MomentType, ImageType].concat(yaml.DEFAULT_SAFE_SCHEMA.implicit),
explicit: yaml.DEFAULT_SAFE_SCHEMA.explicit explicit: yaml.DEFAULT_SAFE_SCHEMA.explicit,
}); });
export default class YAML { export default class YAML {

View File

@ -1,13 +1,17 @@
import Algolia from './providers/algolia/implementation'; import Algolia from './providers/algolia/implementation';
import AssetStore from './providers/assetStore/implementation';
import { Map } from 'immutable'; import { Map } from 'immutable';
export function resolveIntegrations(interationsConfig) { export function resolveIntegrations(interationsConfig, getToken) {
let integrationInstances = Map({}); let integrationInstances = Map({});
interationsConfig.get('providers').forEach((providerData, providerName) => { interationsConfig.get('providers').forEach((providerData, providerName) => {
switch (providerName) { switch (providerName) {
case 'algolia': case 'algolia':
integrationInstances = integrationInstances.set('algolia', new Algolia(providerData)); integrationInstances = integrationInstances.set('algolia', new Algolia(providerData));
break; break;
case 'assetStore':
integrationInstances = integrationInstances.set('assetStore', new AssetStore(providerData, getToken));
break;
} }
}); });
return integrationInstances; return integrationInstances;
@ -17,12 +21,12 @@ export function resolveIntegrations(interationsConfig) {
export const getIntegrationProvider = (function() { export const getIntegrationProvider = (function() {
let integrations = null; let integrations = null;
return (interationsConfig, provider) => { return (interationsConfig, getToken, provider) => {
if (integrations) { if (integrations) {
return integrations.get(provider); return integrations.get(provider);
} else { } else {
integrations = resolveIntegrations(interationsConfig); integrations = resolveIntegrations(interationsConfig, getToken);
return integrations.get(provider); return integrations.get(provider);
} }
}; };
})(); }());

View File

@ -0,0 +1,110 @@
export default class AssetStore {
constructor(config, getToken) {
this.config = config;
if (config.get('getSignedFormURL') == null) {
throw 'The AssetStore integration needs the getSignedFormURL in the integration configuration.';
}
this.getToken = getToken;
this.shouldConfirmUpload = config.get('shouldConfirmUpload', false);
this.getSignedFormURL = config.get('getSignedFormURL');
}
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 path;
}
requestHeaders(headers = {}) {
return {
...headers,
};
}
confirmRequest(assetID) {
this.getToken()
.then(token => this.request(`${ this.getSignedFormURL }/${ assetID }`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ token }`,
},
body: JSON.stringify({ state: 'uploaded' }),
}));
}
request(path, options = {}) {
const headers = this.requestHeaders(options.headers || {});
const url = this.urlFor(path, options);
return fetch(url, { ...options, headers }).then((response) => {
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.match(/json/)) {
return this.parseJsonResponse(response);
}
return response.text();
});
}
upload(file, privateUpload = false) {
const fileData = {
name: file.name,
size: file.size,
content_type: file.type,
};
if (privateUpload) {
fileData.visibility = 'private';
}
return this.getToken()
.then(token => this.request(this.getSignedFormURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ token }`,
},
body: JSON.stringify(fileData),
}))
.then((response) => {
const formURL = response.form.url;
const formFields = response.form.fields;
const assetID = response.asset.id;
const assetURL = response.asset.url;
const formData = new FormData();
Object.keys(formFields).forEach(key => formData.append(key, formFields[key]));
formData.append('file', file, file.name);
return this.request(formURL, {
method: 'POST',
body: formData,
})
.then(() => {
if (this.shouldConfirmUpload) this.confirmRequest(assetID);
return { success: true, assetURL };
});
});
}
}

View File

@ -16,3 +16,66 @@ export function resolvePath(path, basePath) { // eslint-disable-line
// It's a relative path. Prepend a forward slash. // It's a relative path. Prepend a forward slash.
return normalizePath(`/${ path }`); return normalizePath(`/${ path }`);
} }
/**
* Return the last portion of a path. Similar to the Unix basename command.
* @example Usage example
* path.basename('/foo/bar/baz/asdf/quux.html')
* // returns
* 'quux.html'
*
* path.basename('/foo/bar/baz/asdf/quux.html', '.html')
* // returns
* 'quux'
*/
export function basename(p, ext = "") {
// Special case: Normalize will modify this to '.'
if (p === '') {
return p;
}
// Normalize the string first to remove any weirdness.
p = normalizePath(p);
// Get the last part of the string.
const sections = p.split('/');
const lastPart = sections[sections.length - 1];
// Special case: If it's empty, then we have a string like so: foo/
// Meaning, 'foo' is guaranteed to be a directory.
if (lastPart === '' && sections.length > 1) {
return sections[sections.length - 2];
}
// Remove the extension, if need be.
if (ext.length > 0) {
const lastPartExt = lastPart.substr(lastPart.length - ext.length);
if (lastPartExt === ext) {
return lastPart.substr(0, lastPart.length - ext.length);
}
}
return lastPart;
}
/**
* Return the extension of the path, from the last '.' to end of string in the
* last portion of the path. If there is no '.' in the last portion of the path
* or the first character of it is '.', then it returns an empty string.
* @example Usage example
* path.extname('index.html')
* // returns
* '.html'
*/
export function extname(p) {
p = normalizePath(p);
const sections = p.split('/');
p = sections.pop();
// Special case: foo/file.ext/ should return '.ext'
if (p === '' && sections.length > 0) {
p = sections.pop();
}
if (p === '..') {
return '';
}
const i = p.lastIndexOf('.');
if (i === -1 || i === 0) {
return '';
}
return p.substr(i);
}

View File

@ -2,6 +2,7 @@ import { Map, List, fromJS } from 'immutable';
import { import {
ENTRY_REQUEST, ENTRY_REQUEST,
ENTRY_SUCCESS, ENTRY_SUCCESS,
ENTRY_FAILURE,
ENTRIES_REQUEST, ENTRIES_REQUEST,
ENTRIES_SUCCESS, ENTRIES_SUCCESS,
} from '../actions/entries'; } from '../actions/entries';
@ -41,6 +42,12 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
ids: (!page || page === 0) ? ids : map.getIn(['pages', collection, 'ids'], List()).concat(ids), ids: (!page || page === 0) ? ids : map.getIn(['pages', collection, 'ids'], List()).concat(ids),
})); }));
}); });
case ENTRY_FAILURE:
return state.withMutations((map) => {
map.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'isFetching'], false);
map.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'error'], action.payload.error.message);
});
case SEARCH_ENTRIES_SUCCESS: case SEARCH_ENTRIES_SUCCESS:
loadedEntries = action.payload.entries; loadedEntries = action.payload.entries;

View File

@ -9,8 +9,8 @@ import {
ENTRY_PERSIST_FAILURE, ENTRY_PERSIST_FAILURE,
} from '../actions/entries'; } from '../actions/entries';
import { import {
ADD_MEDIA, ADD_ASSET,
REMOVE_MEDIA, REMOVE_ASSET,
} from '../actions/media'; } from '../actions/media';
const initialState = Map({ entry: Map(), mediaFiles: List(), fieldsMetaData: Map() }); const initialState = Map({ entry: Map(), mediaFiles: List(), fieldsMetaData: Map() });
@ -49,9 +49,9 @@ const entryDraftReducer = (state = Map(), action) => {
return state.deleteIn(['entry', 'isPersisting']); return state.deleteIn(['entry', 'isPersisting']);
} }
case ADD_MEDIA: case ADD_ASSET:
return state.update('mediaFiles', list => list.push(action.payload.public_path)); return state.update('mediaFiles', list => list.push(action.payload.public_path));
case REMOVE_MEDIA: case REMOVE_ASSET:
return state.update('mediaFiles', list => list.filterNot(path => path === action.payload)); return state.update('mediaFiles', list => list.filterNot(path => path === action.payload));
default: default:

View File

@ -49,5 +49,5 @@ export const selectUnpublishedEntries = (state, status) =>
export const selectIntegration = (state, collection, hook) => export const selectIntegration = (state, collection, hook) =>
fromIntegrations.selectIntegration(state.integrations, collection, hook); fromIntegrations.selectIntegration(state.integrations, collection, hook);
export const getMedia = (state, path) => export const getAsset = (state, path) =>
fromMedias.getMedia(state.config.get('public_folder'), state.medias, path); fromMedias.getAsset(state.config.get('public_folder'), state.medias, path);

View File

@ -8,8 +8,15 @@ const integrations = (state = null, action) => {
const newState = integrations.reduce((acc, integration) => { const newState = integrations.reduce((acc, integration) => {
const { hooks, collections, provider, ...providerData } = integration; const { hooks, collections, provider, ...providerData } = integration;
acc.providers[provider] = { ...providerData }; acc.providers[provider] = { ...providerData };
collections.forEach(collection => { if (!collections) {
hooks.forEach(hook => { hooks.forEach((hook) => {
acc.hooks[hook] = provider;
});
return acc;
}
const integrationCollections = collections === "*" ? action.payload.collections.map(collection => collection.name) : collections;
integrationCollections.forEach((collection) => {
hooks.forEach((hook) => {
acc.hooks[collection] ? acc.hooks[collection][hook] = provider : acc.hooks[collection] = { [hook]: provider }; acc.hooks[collection] ? acc.hooks[collection][hook] = provider : acc.hooks[collection] = { [hook]: provider };
}); });
}); });
@ -21,9 +28,9 @@ const integrations = (state = null, action) => {
} }
}; };
export const selectIntegration = (state, collection, hook) => { export const selectIntegration = (state, collection, hook) => (
return state.getIn(['hooks', collection, hook], false); collection? state.getIn(['hooks', collection, hook], false) : state.getIn(['hooks', hook], false)
}; );
export default integrations; export default integrations;

View File

@ -1,13 +1,13 @@
import { Map } from 'immutable'; import { Map } from 'immutable';
import { resolvePath } from '../lib/pathHelper'; import { resolvePath } from '../lib/pathHelper';
import { ADD_MEDIA, REMOVE_MEDIA } from '../actions/media'; import { ADD_ASSET, REMOVE_ASSET } from '../actions/media';
import MediaProxy from '../valueObjects/MediaProxy'; import AssetProxy from '../valueObjects/AssetProxy';
const medias = (state = Map(), action) => { const medias = (state = Map(), action) => {
switch (action.type) { switch (action.type) {
case ADD_MEDIA: case ADD_ASSET:
return state.set(action.payload.public_path, action.payload); return state.set(action.payload.public_path, action.payload);
case REMOVE_MEDIA: case REMOVE_ASSET:
return state.delete(action.payload); return state.delete(action.payload);
default: default:
@ -18,17 +18,17 @@ const medias = (state = Map(), action) => {
export default medias; export default medias;
const memoizedProxies = {}; const memoizedProxies = {};
export const getMedia = (publicFolder, state, path) => { export const getAsset = (publicFolder, state, path) => {
// No path provided, skip // No path provided, skip
if (!path) return null; if (!path) return null;
let proxy = state.get(path) || memoizedProxies[path]; let proxy = state.get(path) || memoizedProxies[path];
if (proxy) { if (proxy) {
// There is already a MediaProxy in memmory for this path. Use it. // There is already an AssetProxy in memmory for this path. Use it.
return proxy; return proxy;
} }
// Create a new MediaProxy (for consistency) and return it. // Create a new AssetProxy (for consistency) and return it.
proxy = memoizedProxies[path] = new MediaProxy(resolvePath(path, publicFolder), null, true); proxy = memoizedProxies[path] = new AssetProxy(resolvePath(path, publicFolder), null, true);
return proxy; return proxy;
}; };

View File

@ -4,12 +4,15 @@ import { Router } from 'react-router';
import routes from './routing/routes'; import routes from './routing/routes';
import history, { syncHistory } from './routing/history'; import history, { syncHistory } from './routing/history';
import configureStore from './store/configureStore'; import configureStore from './store/configureStore';
import { setStore } from './valueObjects/AssetProxy';
const store = configureStore(); const store = configureStore();
// Create an enhanced history that syncs navigation events with the store // Create an enhanced history that syncs navigation events with the store
syncHistory(store); syncHistory(store);
setStore(store);
const Root = () => ( const Root = () => (
<Provider store={store}> <Provider store={store}>
<Router history={history}> <Router history={history}>

View File

@ -0,0 +1,53 @@
import { resolvePath } from '../lib/pathHelper';
import { currentBackend } from "../backends/backend";
import { getIntegrationProvider } from '../integrations';
import { selectIntegration } from '../reducers';
let store;
export const setStore = (storeObj) => {
store = storeObj;
};
export default function AssetProxy(value, file, uploaded = false) {
const config = store.getState().config;
this.value = value;
this.file = file;
this.uploaded = uploaded;
this.sha = null;
this.path = config.media_folder && !uploaded ? `${ config.get('media_folder') }/${ value }` : value;
this.public_path = !uploaded ? resolvePath(value, config.get('public_folder')) : value;
}
AssetProxy.prototype.toString = function () {
return this.uploaded ? this.public_path : window.URL.createObjectURL(this.file, { oneTimeOnly: true });
};
AssetProxy.prototype.toBase64 = function () {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = (readerEvt) => {
const binaryString = readerEvt.target.result;
resolve(binaryString.split('base64,')[1]);
};
fr.readAsDataURL(this.file);
});
};
export function createAssetProxy(value, file, uploaded = false, privateUpload = false) {
const state = store.getState();
const integration = selectIntegration(state, null, 'assetStore');
if (integration && !uploaded) {
const provider = integration && getIntegrationProvider(state.integrations, currentBackend(state.config).getToken, integration);
return provider.upload(file, privateUpload).then(
response => (
new AssetProxy(response.assetURL.replace(/^(https?):/, ''), null, true)
),
error => new AssetProxy(value, file, false)
);
} else if (privateUpload) {
throw new Error('The Private Upload option is only avaible for Asset Store Integration');
}
return Promise.resolve(new AssetProxy(value, file, uploaded));
}

View File

@ -1,31 +0,0 @@
import { resolvePath } from '../lib/pathHelper';
let config;
export const setConfig = (configObj) => {
config = configObj;
};
export default function MediaProxy(value, file, uploaded = false) {
this.value = value;
this.file = file;
this.uploaded = uploaded;
this.sha = null;
this.path = config.media_folder && !uploaded ? `${ config.media_folder }/${ value }` : value;
this.public_path = !uploaded ? resolvePath(value, config.public_folder) : value;
}
MediaProxy.prototype.toString = function () {
return this.uploaded ? this.public_path : window.URL.createObjectURL(this.file, { oneTimeOnly: true });
};
MediaProxy.prototype.toBase64 = function () {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = (readerEvt) => {
const binaryString = readerEvt.target.result;
resolve(binaryString.split('base64,')[1]);
};
fr.readAsDataURL(this.file);
});
};