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

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

View File

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

View File

@ -2,7 +2,7 @@ import { List } from 'immutable';
import { actions as notifActions } from 'redux-notifications';
import { currentBackend } from '../backends/backend';
import { getIntegrationProvider } from '../integrations';
import { getMedia, selectIntegration } from '../reducers';
import { getAsset, selectIntegration } from '../reducers';
import { createEntry } from '../valueObjects/Entry';
const { notifSend } = notifActions;
@ -52,6 +52,17 @@ export function entryLoaded(collection, entry) {
};
}
export function entryLoadError(error, collection, slug) {
return {
type: ENTRY_FAILURE,
payload: {
error,
collection: collection.get('name'),
slug,
},
};
}
export function entriesLoading(collection) {
return {
type: ENTRIES_REQUEST,
@ -161,7 +172,15 @@ export function loadEntry(entry, collection, slug) {
return backend.getEntry(collection, slug)
.then(loadedEntry => (
dispatch(entryLoaded(collection, loadedEntry))
));
))
.catch((error) => {
dispatch(notifSend({
message: `Failed to load entry: ${ error.message }`,
kind: 'danger',
dismissAfter: 4000,
}));
dispatch(entryLoadError(error, collection, slug));
});
};
}
@ -171,8 +190,9 @@ export function loadEntries(collection, page = 0) {
return;
}
const state = getState();
const backend = currentBackend(state.config);
const integration = selectIntegration(state, collection.get('name'), 'listEntries');
const provider = integration ? getIntegrationProvider(state.integrations, integration) : currentBackend(state.config);
const provider = integration ? getIntegrationProvider(state.integrations, backend.getToken, integration) : backend;
dispatch(entriesLoading(collection));
provider.listEntries(collection, page).then(
response => dispatch(entriesLoaded(collection, response.entries, response.pagination)),
@ -196,11 +216,11 @@ export function persistEntry(collection, entryDraft) {
return (dispatch, getState) => {
const state = getState();
const backend = currentBackend(state.config);
const mediaProxies = entryDraft.get('mediaFiles').map(path => getMedia(state, path));
const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path));
const entry = entryDraft.get('entry');
dispatch(entryPersisting(collection, entry));
backend
.persistEntry(state.config, collection, entryDraft, mediaProxies.toJS())
.persistEntry(state.config, collection, entryDraft, assetProxies.toJS())
.then(() => {
dispatch(notifSend({
message: 'Entry saved',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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) });
}
getToken() {
return Promise.resolve('');
}
entriesByFolder(collection) {
const entries = [];
const folder = collection.get('folder');
@ -67,7 +71,7 @@ export default class TestRepo {
getEntry(collection, slug, path) {
return Promise.resolve({
file: { path },
data: getFile(path).content
data: getFile(path).content,
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { truncateMiddle } from '../../lib/textHelper';
import MediaProxy from '../../valueObjects/MediaProxy';
import { Loader } from '../UI';
import AssetProxy, { createAssetProxy } from '../../valueObjects/AssetProxy';
const MAX_DISPLAY_LENGTH = 50;
export default class ImageControl extends React.Component {
state = {
processing: false,
};
handleFileInputRef = (el) => {
this._fileInput = el;
};
@ -38,11 +43,15 @@ export default class ImageControl extends React.Component {
}
});
this.props.onRemoveMedia(this.props.value);
this.props.onRemoveAsset(this.props.value);
if (file) {
const mediaProxy = new MediaProxy(file.name, file);
this.props.onAddMedia(mediaProxy);
this.props.onChange(mediaProxy.public_path);
this.setState({ processing: true });
createAssetProxy(file.name, file)
.then((assetProxy) => {
this.setState({ processing: false });
this.props.onAddAsset(assetProxy);
this.props.onChange(assetProxy.public_path);
});
} else {
this.props.onChange(null);
}
@ -50,7 +59,7 @@ export default class ImageControl extends React.Component {
renderImageName = () => {
if (!this.props.value) return null;
if (this.value instanceof MediaProxy) {
if (this.value instanceof AssetProxy) {
return truncateMiddle(this.props.value.path, MAX_DISPLAY_LENGTH);
} else {
return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH);
@ -58,14 +67,25 @@ export default class ImageControl extends React.Component {
};
render() {
const { processing } = this.state;
const imageName = this.renderImageName();
if (processing) {
return (
<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.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'}
</span>
<input
@ -84,21 +104,23 @@ const styles = {
input: {
display: 'none',
},
message: {
padding: '20px',
display: 'block',
fontSize: '12px',
},
imageUpload: {
backgroundColor: '#fff',
textAlign: 'center',
color: '#999',
padding: '20px',
display: 'block',
border: '1px dashed #eee',
cursor: 'pointer',
fontSize: '12px',
},
};
ImageControl.propTypes = {
onAddMedia: PropTypes.func.isRequired,
onAddAsset: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onRemoveMedia: PropTypes.func.isRequired,
onRemoveAsset: PropTypes.func.isRequired,
value: PropTypes.node,
};

View File

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

View File

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

View File

@ -4,13 +4,13 @@ import previewStyle from './defaultPreviewStyle';
export default class ListPreview extends Component {
widgetFor = (field, value) => {
const { getMedia } = this.props;
const { getAsset } = this.props;
const widget = resolveWidget(field.get('widget'));
return (<div key={field.get('name')}>{React.createElement(widget.preview, {
key: field.get('name'),
value: value && value.get(field.get('name')),
field,
getMedia,
getAsset,
})}</div>);
};
@ -32,5 +32,5 @@ export default class ListPreview extends Component {
ListPreview.propTypes = {
value: 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 = {
editor: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired,
onAddAsset: PropTypes.func.isRequired,
getAsset: PropTypes.func.isRequired,
switchVisualMode: PropTypes.func.isRequired,
value: PropTypes.node,
};
@ -33,17 +33,17 @@ class MarkdownControl extends React.Component {
};
render() {
const { onChange, onAddMedia, onRemoveMedia, getMedia, value } = this.props;
const { onChange, onAddAsset, onRemoveAsset, getAsset, value } = this.props;
const { mode } = this.state;
if (mode === 'visual') {
return (
<div className="cms-editor-visual">
<VisualEditor
onChange={onChange}
onAddMedia={onAddMedia}
onRemoveMedia={onRemoveMedia}
onAddAsset={onAddAsset}
onRemoveAsset={onRemoveAsset}
onMode={this.handleMode}
getMedia={getMedia}
getAsset={getAsset}
value={value}
/>
</div>
@ -54,10 +54,10 @@ class MarkdownControl extends React.Component {
<div className="cms-editor-raw">
<RawEditor
onChange={onChange}
onAddMedia={onAddMedia}
onRemoveMedia={onRemoveMedia}
onAddAsset={onAddAsset}
onRemoveAsset={onRemoveAsset}
onMode={this.handleMode}
getMedia={getMedia}
getAsset={getAsset}
value={value}
/>
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -7,14 +7,14 @@ import styles from './ObjectControl.css';
export default class ObjectControl extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
onAddMedia: PropTypes.func.isRequired,
getMedia: PropTypes.func.isRequired,
onAddAsset: PropTypes.func.isRequired,
getAsset: PropTypes.func.isRequired,
value: PropTypes.node,
field: PropTypes.node,
};
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 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((value || Map()).set(field.get('name'), val), metadata);
},
onAddMedia,
onRemoveMedia,
getMedia,
onAddAsset,
onRemoveAsset,
getAsset,
})
}
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,38 +1,38 @@
import yaml from 'js-yaml';
import moment from 'moment';
import MediaProxy from '../valueObjects/MediaProxy';
import AssetProxy from '../valueObjects/AssetProxy';
const MomentType = new yaml.Type('date', {
kind: 'scalar',
predicate: function(value) {
predicate(value) {
return moment.isMoment(value);
},
represent: function(value) {
represent(value) {
return value.format(value._f);
},
resolve: function(value) {
resolve(value) {
return moment.isMoment(value) && value._f;
}
},
});
const ImageType = new yaml.Type('image', {
kind: 'scalar',
instanceOf: MediaProxy,
represent: function(value) {
return `${value.path}`;
instanceOf: AssetProxy,
represent(value) {
return `${ value.path }`;
},
resolve: function(value) {
resolve(value) {
if (value === null) return false;
if (value instanceof MediaProxy) return true;
if (value instanceof AssetProxy) return true;
return false;
}
},
});
const OutputSchema = new yaml.Schema({
include: yaml.DEFAULT_SAFE_SCHEMA.include,
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 {

View File

@ -1,13 +1,17 @@
import Algolia from './providers/algolia/implementation';
import AssetStore from './providers/assetStore/implementation';
import { Map } from 'immutable';
export function resolveIntegrations(interationsConfig) {
export function resolveIntegrations(interationsConfig, getToken) {
let integrationInstances = Map({});
interationsConfig.get('providers').forEach((providerData, providerName) => {
switch (providerName) {
case 'algolia':
integrationInstances = integrationInstances.set('algolia', new Algolia(providerData));
break;
case 'assetStore':
integrationInstances = integrationInstances.set('assetStore', new AssetStore(providerData, getToken));
break;
}
});
return integrationInstances;
@ -17,12 +21,12 @@ export function resolveIntegrations(interationsConfig) {
export const getIntegrationProvider = (function() {
let integrations = null;
return (interationsConfig, provider) => {
return (interationsConfig, getToken, provider) => {
if (integrations) {
return integrations.get(provider);
} else {
integrations = resolveIntegrations(interationsConfig);
integrations = resolveIntegrations(interationsConfig, getToken);
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.
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 {
ENTRY_REQUEST,
ENTRY_SUCCESS,
ENTRY_FAILURE,
ENTRIES_REQUEST,
ENTRIES_SUCCESS,
} 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),
}));
});
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:
loadedEntries = action.payload.entries;

View File

@ -9,8 +9,8 @@ import {
ENTRY_PERSIST_FAILURE,
} from '../actions/entries';
import {
ADD_MEDIA,
REMOVE_MEDIA,
ADD_ASSET,
REMOVE_ASSET,
} from '../actions/media';
const initialState = Map({ entry: Map(), mediaFiles: List(), fieldsMetaData: Map() });
@ -49,9 +49,9 @@ const entryDraftReducer = (state = Map(), action) => {
return state.deleteIn(['entry', 'isPersisting']);
}
case ADD_MEDIA:
case ADD_ASSET:
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));
default:

View File

@ -49,5 +49,5 @@ export const selectUnpublishedEntries = (state, status) =>
export const selectIntegration = (state, collection, hook) =>
fromIntegrations.selectIntegration(state.integrations, collection, hook);
export const getMedia = (state, path) =>
fromMedias.getMedia(state.config.get('public_folder'), state.medias, path);
export const getAsset = (state, 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 { hooks, collections, provider, ...providerData } = integration;
acc.providers[provider] = { ...providerData };
collections.forEach(collection => {
hooks.forEach(hook => {
if (!collections) {
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 };
});
});
@ -21,9 +28,9 @@ const integrations = (state = null, action) => {
}
};
export const selectIntegration = (state, collection, hook) => {
return state.getIn(['hooks', collection, hook], false);
};
export const selectIntegration = (state, collection, hook) => (
collection? state.getIn(['hooks', collection, hook], false) : state.getIn(['hooks', hook], false)
);
export default integrations;

View File

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

View File

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