parent
37f690fc44
commit
a4d7622ade
@ -182,7 +182,7 @@ Registers a template for a collection.
|
||||
* react_component: A React component that renders the collection data. Three props will be passed to your component during render:
|
||||
* entry: Immutable collection containing the entry data.
|
||||
* widgetFor: Returns the appropriate widget preview component for a given field.
|
||||
* getMedia: Returns the correct filePath or in-memory preview for uploaded images.
|
||||
* getAsset: Returns the correct filePath or in-memory preview for uploaded images.
|
||||
|
||||
**Example:**
|
||||
|
||||
@ -192,7 +192,7 @@ var PostPreview = createClass({
|
||||
render: function() {
|
||||
var entry = this.props.entry;
|
||||
var image = entry.getIn(['data', 'image']);
|
||||
var bg = this.props.getMedia(image);
|
||||
var bg = this.props.getAsset(image);
|
||||
return h('div', {},
|
||||
h('h1', {}, entry.getIn(['data', 'title'])),
|
||||
h('img', {src: bg.toString()}),
|
||||
|
@ -32,14 +32,14 @@ Selectors are functions defined within reducers used to compute derived data fro
|
||||
|
||||
**selectEntries:** Selects all entries for a given collection.
|
||||
|
||||
**getMedia:** Selects a single MediaProxy object for the given URI:
|
||||
**getAsset:** Selects a single AssetProxy object for the given URI:
|
||||
|
||||
## Value Objects:
|
||||
**MediaProxy:** MediaProxy is a Value Object that holds information regarding a media file (such as an image, for example), whether it's persisted online or hold locally in cache.
|
||||
**AssetProxy:** AssetProxy is a Value Object that holds information regarding an asset file (such as an image, for example), whether it's persisted online or hold locally in cache.
|
||||
|
||||
For files persisted online, the MediaProxy only keeps information about it's URI. For local files, the MediaProxy will keep a reference to the actual File object while generating the expected final URIs and on-demand blobs for local preview.
|
||||
For files persisted online, the AssetProxy only keeps information about it's URI. For local files, the AssetProxy will keep a reference to the actual File object while generating the expected final URIs and on-demand blobs for local preview.
|
||||
|
||||
The MediaProxy object can be used directly inside a media tag (such as `<img>`), as it will always return something that can be used by the media tag to render correctly (either the URI for the online file or a single-use blob).
|
||||
The AssetProxy object can be used directly inside a media tag (such as `<img>`), as it will always return something that can be used by the media tag to render correctly (either the URI for the online file or a single-use blob).
|
||||
|
||||
## Components structure and Workflows
|
||||
Components are separated into two main categories: Container components and presentational components.
|
||||
@ -52,10 +52,10 @@ For either updating an existing entry or creating a new one, the `EntryEditor` i
|
||||
- Widgets are used for editing entry fields. There are different widgets for different field types, and they are always defined in a pair containing a `control` and a `preview` components. The control component is responsible for presenting the user with the appropriate interface for manipulating the current field value, while the preview component is responsible for displaying value with the appropriate styling.
|
||||
|
||||
#### Widget components implementation:
|
||||
The control component receives 3 callbacks as props: onChange, onAddMedia & onRemoveMedia.
|
||||
The control component receives 3 callbacks as props: onChange, onAddAsset & onRemoveAsset.
|
||||
- onChange (Required): Should be called when the users changes the current value. It will ultimately end up updating the EntryDraft object in the Redux Store, thus updating the preview component.
|
||||
- onAddMedia & onRemoveMedia (optionals): If the field accepts file uploads for media (images, for example), these callbacks should be invoked with a `MediaProxy` value object. `onAddMedia` will get the current media stored in the Redux state tree while `onRemoveMedia` will remove it. MediaProxy objects are stored in the `Medias` object and referenced in the `EntryDraft` object on the state tree.
|
||||
- onAddAsset & onRemoveAsset (optionals): If the field accepts file uploads for media (images, for example), these callbacks should be invoked with a `AssetProxy` value object. `onAddAsset` will get the current media stored in the Redux state tree while `onRemoveAsset` will remove it. AssetProxy objects are stored in the `Medias` object and referenced in the `EntryDraft` object on the state tree.
|
||||
|
||||
Both control and preview widgets receive a `getMedia` selector via props. Displaying the media (or its uri) for the user should always be done via `getMedia`, as it returns a MediaProxy that can return the correct value for both medias already persisted on server and cached media not yet uploaded.
|
||||
Both control and preview widgets receive a `getAsset` selector via props. Displaying the media (or its uri) for the user should always be done via `getAsset`, as it returns a AssetProxy that can return the correct value for both medias already persisted on server and cached media not yet uploaded.
|
||||
|
||||
The actual persistence of the content and medias inserted into the control component are delegated to the backend implementation. The backend will be called with the updated values and a a list of mediaProxy objects for each field of the entry, and should return a promise that can resolve into the persisted entry object and the list of the persisted media URIs.
|
||||
The actual persistence of the content and medias inserted into the control component are delegated to the backend implementation. The backend will be called with the updated values and a a list of assetProxy objects for each field of the entry, and should return a promise that can resolve into the persisted entry object and the list of the persisted media URIs.
|
||||
|
@ -73,7 +73,7 @@
|
||||
render: function() {
|
||||
var entry = this.props.entry;
|
||||
var image = entry.getIn(['data', 'image']);
|
||||
var bg = image && this.props.getMedia(image);
|
||||
var bg = image && this.props.getAsset(image);
|
||||
return h('div', {},
|
||||
h('div', {className: "cover"},
|
||||
h('h1', {}, entry.getIn(['data', 'title'])),
|
||||
@ -104,7 +104,7 @@
|
||||
h('dd', {}, posts && posts.get('author') || 'None'),
|
||||
|
||||
h('dt', {}, 'Default Thumbnail'),
|
||||
h('dd', {}, thumb && h('img', {src: this.props.getMedia(thumb).toString()}))
|
||||
h('dd', {}, thumb && h('img', {src: this.props.getAsset(thumb).toString()}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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 `![${ 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 };
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user