@ -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));
|
||||
};
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
@ -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 })
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -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>;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
|
122
src/components/Widgets/FileControl.js
Normal file
122
src/components/Widgets/FileControl.js
Normal 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,
|
||||
};
|
15
src/components/Widgets/FilePreview.js
Normal file
15
src/components/Widgets/FilePreview.js
Normal 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,
|
||||
};
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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)}>
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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 ``;
|
||||
}
|
||||
});
|
||||
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 };
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
));
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
})();
|
||||
}());
|
||||
|
110
src/integrations/providers/assetStore/implementation.js
Normal file
110
src/integrations/providers/assetStore/implementation.js
Normal 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 };
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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}>
|
||||
|
53
src/valueObjects/AssetProxy.js
Normal file
53
src/valueObjects/AssetProxy.js
Normal 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));
|
||||
}
|
@ -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);
|
||||
});
|
||||
};
|
Reference in New Issue
Block a user