Optimistic Updates (#114)
* Optimistic Updates structure * Optimistic update for Editorial Workflow
This commit is contained in:
parent
009c881290
commit
45c7e8b08b
@ -124,12 +124,14 @@
|
||||
"react-toolbox": "^1.2.1",
|
||||
"react-waypoint": "^3.1.3",
|
||||
"redux": "^3.3.1",
|
||||
"redux-optimist": "^0.0.2",
|
||||
"redux-notifications": "^2.1.1",
|
||||
"redux-thunk": "^1.0.3",
|
||||
"selection-position": "^1.0.0",
|
||||
"semaphore": "^1.0.5",
|
||||
"slate": "^0.14.14",
|
||||
"slate-drop-or-paste-images": "^0.2.0",
|
||||
"uuid": "^2.0.3",
|
||||
"whatwg-fetch": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import uuid from 'uuid';
|
||||
import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
|
||||
import { currentBackend } from '../backends/backend';
|
||||
import { getMedia } from '../reducers';
|
||||
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
|
||||
@ -16,9 +18,12 @@ export const UNPUBLISHED_ENTRY_PERSIST_SUCCESS = 'UNPUBLISHED_ENTRY_PERSIST_SUCC
|
||||
|
||||
export const UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE';
|
||||
|
||||
|
||||
export const UNPUBLISHED_ENTRY_PUBLISH_REQUEST = 'UNPUBLISHED_ENTRY_PUBLISH_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_PUBLISH_SUCCESS = 'UNPUBLISHED_ENTRY_PUBLISH_SUCCESS';
|
||||
export const UNPUBLISHED_ENTRY_PUBLISH_FAILURE = 'UNPUBLISHED_ENTRY_PUBLISH_FAILURE';
|
||||
|
||||
/*
|
||||
* Simple Action Creators (Internal)
|
||||
@ -27,20 +32,20 @@ export const UNPUBLISHED_ENTRY_PUBLISH_SUCCESS = 'UNPUBLISHED_ENTRY_PUBLISH_SUCC
|
||||
function unpublishedEntryLoading(status, slug) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_REQUEST,
|
||||
payload: { status, slug }
|
||||
payload: { status, slug },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryLoaded(status, entry) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_SUCCESS,
|
||||
payload: { status, entry }
|
||||
payload: { status, entry },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntriesLoading() {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRIES_REQUEST
|
||||
type: UNPUBLISHED_ENTRIES_REQUEST,
|
||||
};
|
||||
}
|
||||
|
||||
@ -48,9 +53,9 @@ function unpublishedEntriesLoaded(entries, pagination) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRIES_SUCCESS,
|
||||
payload: {
|
||||
entries: entries,
|
||||
pages: pagination
|
||||
}
|
||||
entries,
|
||||
pages: pagination,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -66,49 +71,69 @@ function unpublishedEntriesFailed(error) {
|
||||
function unpublishedEntryPersisting(entry) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PERSIST_REQUEST,
|
||||
payload: { entry }
|
||||
payload: { entry },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPersisted(entry) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
|
||||
payload: { entry }
|
||||
payload: { entry },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPersistedFail(error) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
|
||||
payload: { error }
|
||||
payload: { error },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newStatus) {
|
||||
function unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newStatus, transactionID) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST,
|
||||
payload: { collection, slug, oldStatus, newStatus }
|
||||
payload: { collection, slug, oldStatus, newStatus },
|
||||
optimist: { type: BEGIN, id: transactionID },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryStatusChangePersisted(collection, slug, oldStatus, newStatus) {
|
||||
function unpublishedEntryStatusChangePersisted(collection, slug, oldStatus, newStatus, transactionID) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS,
|
||||
payload: { collection, slug, oldStatus, newStatus }
|
||||
payload: { collection, slug, oldStatus, newStatus },
|
||||
optimist: { type: COMMIT, id: transactionID },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPublishRequest(collection, slug, status) {
|
||||
function unpublishedEntryStatusChangeError(collection, slug, transactionID) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE,
|
||||
payload: { collection, slug },
|
||||
optimist: { type: REVERT, id: transactionID },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPublishRequest(collection, slug, status, transactionID) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PUBLISH_REQUEST,
|
||||
payload: { collection, slug, status }
|
||||
payload: { collection, slug, status },
|
||||
optimist: { type: BEGIN, id: transactionID },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPublished(collection, slug, status) {
|
||||
function unpublishedEntryPublished(collection, slug, status, transactionID) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PUBLISH_SUCCESS,
|
||||
payload: { collection, slug, status }
|
||||
payload: { collection, slug, status },
|
||||
optimist: { type: COMMIT, id: transactionID },
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPublishError(collection, slug, transactionID) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PUBLISH_FAILURE,
|
||||
payload: { collection, slug },
|
||||
optimist: { type: REVERT, id: transactionID },
|
||||
};
|
||||
}
|
||||
|
||||
@ -122,7 +147,7 @@ export function loadUnpublishedEntry(collection, status, slug) {
|
||||
const backend = currentBackend(state.config);
|
||||
dispatch(unpublishedEntryLoading(status, slug));
|
||||
backend.unpublishedEntry(collection, slug)
|
||||
.then((entry) => dispatch(unpublishedEntryLoaded(status, entry)));
|
||||
.then(entry => dispatch(unpublishedEntryLoaded(status, entry)));
|
||||
};
|
||||
}
|
||||
|
||||
@ -133,8 +158,8 @@ export function loadUnpublishedEntries() {
|
||||
const backend = currentBackend(state.config);
|
||||
dispatch(unpublishedEntriesLoading());
|
||||
backend.unpublishedEntries().then(
|
||||
(response) => dispatch(unpublishedEntriesLoaded(response.entries, response.pagination)),
|
||||
(error) => dispatch(unpublishedEntriesFailed(error))
|
||||
response => dispatch(unpublishedEntriesLoaded(response.entries, response.pagination)),
|
||||
error => dispatch(unpublishedEntriesFailed(error))
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -149,7 +174,7 @@ export function persistUnpublishedEntry(collection, entry) {
|
||||
() => {
|
||||
dispatch(unpublishedEntryPersisted(entry));
|
||||
},
|
||||
(error) => dispatch(unpublishedEntryPersistedFail(error))
|
||||
error => dispatch(unpublishedEntryPersistedFail(error))
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -158,10 +183,14 @@ export function updateUnpublishedEntryStatus(collection, slug, oldStatus, newSta
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
dispatch(unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newStatus));
|
||||
const transactionID = uuid.v4();
|
||||
dispatch(unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newStatus, transactionID));
|
||||
backend.updateUnpublishedEntryStatus(collection, slug, newStatus)
|
||||
.then(() => {
|
||||
dispatch(unpublishedEntryStatusChangePersisted(collection, slug, oldStatus, newStatus));
|
||||
dispatch(unpublishedEntryStatusChangePersisted(collection, slug, oldStatus, newStatus, transactionID));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(unpublishedEntryStatusChangeError(collection, slug, transactionID));
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -170,10 +199,14 @@ export function publishUnpublishedEntry(collection, slug, status) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const transactionID = uuid.v4();
|
||||
dispatch(unpublishedEntryPublishRequest(collection, slug, status));
|
||||
backend.publishUnpublishedEntry(collection, slug, status)
|
||||
backend.publishUnpublishedEntry(collection, slug, status, transactionID)
|
||||
.then(() => {
|
||||
dispatch(unpublishedEntryPublished(collection, slug, status));
|
||||
dispatch(unpublishedEntryPublished(collection, slug, status, transactionID));
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(unpublishedEntryPublishError(collection, slug, transactionID));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -165,7 +165,10 @@ export default class API {
|
||||
}
|
||||
|
||||
persistFiles(entry, mediaFiles, options) {
|
||||
let filename, part, parts, subtree;
|
||||
let filename,
|
||||
part,
|
||||
parts,
|
||||
subtree;
|
||||
const fileTree = {};
|
||||
const uploadPromises = [];
|
||||
|
||||
@ -269,7 +272,7 @@ export default class API {
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection, slug, status) {
|
||||
const contentKey = collection ? `${ collection }-${ slug }` : slug;
|
||||
const contentKey = slug;
|
||||
return this.retrieveMetadata(contentKey)
|
||||
.then((metadata) => {
|
||||
return {
|
||||
@ -281,7 +284,7 @@ export default class API {
|
||||
}
|
||||
|
||||
publishUnpublishedEntry(collection, slug, status) {
|
||||
const contentKey = collection ? `${ collection }-${ slug }` : slug;
|
||||
const contentKey = slug;
|
||||
return this.retrieveMetadata(contentKey)
|
||||
.then((metadata) => {
|
||||
const headSha = metadata.pr && metadata.pr.head;
|
||||
@ -376,7 +379,9 @@ export default class API {
|
||||
updateTree(sha, path, fileTree) {
|
||||
return this.getTree(sha)
|
||||
.then((tree) => {
|
||||
let obj, filename, fileOrDir;
|
||||
let obj,
|
||||
filename,
|
||||
fileOrDir;
|
||||
const updates = [];
|
||||
const added = {};
|
||||
|
||||
|
@ -83,7 +83,7 @@ export default class GitHub {
|
||||
sem.leave();
|
||||
} else {
|
||||
const entryPath = data.metaData.objects.entry;
|
||||
const entry = createEntry('draft', entryPath.split('/').pop().replace(/\.[^\.]+$/, ''), entryPath, { raw: data.file });
|
||||
const entry = createEntry('draft', contentKey, entryPath, { raw: data.file });
|
||||
entry.metaData = data.metaData;
|
||||
resolve(entry);
|
||||
sem.leave();
|
||||
|
@ -12,7 +12,7 @@ class FindBar extends Component {
|
||||
commands: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
pattern: PropTypes.string.isRequired
|
||||
pattern: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
defaultCommands: PropTypes.arrayOf(PropTypes.string),
|
||||
runCommand: PropTypes.func.isRequired,
|
||||
@ -23,9 +23,9 @@ class FindBar extends Component {
|
||||
this._compiledCommands = [];
|
||||
this._searchCommand = {
|
||||
search: true,
|
||||
regexp: `(?:${SEARCH})?(.*)`,
|
||||
regexp: `(?:${ SEARCH })?(.*)`,
|
||||
param: { name: 'searchTerm', display: '' },
|
||||
token: SEARCH
|
||||
token: SEARCH,
|
||||
};
|
||||
this.state = {
|
||||
value: '',
|
||||
@ -56,7 +56,7 @@ class FindBar extends Component {
|
||||
}
|
||||
|
||||
// Generates a regexp and splits a token and param details for a command
|
||||
compileCommand = command => {
|
||||
compileCommand = (command) => {
|
||||
let regexp = '';
|
||||
let param = null;
|
||||
|
||||
@ -75,7 +75,7 @@ class FindBar extends Component {
|
||||
return Object.assign({}, command, {
|
||||
regexp,
|
||||
token,
|
||||
param
|
||||
param,
|
||||
});
|
||||
};
|
||||
|
||||
@ -84,15 +84,15 @@ class FindBar extends Component {
|
||||
matchCommand = () => {
|
||||
const string = this.state.activeScope ? this.state.activeScope + this.state.value : this.state.value;
|
||||
let match;
|
||||
let command = this._compiledCommands.find(command => {
|
||||
match = string.match(RegExp(`^${command.regexp}`, 'i'));
|
||||
let command = this._compiledCommands.find((command) => {
|
||||
match = string.match(RegExp(`^${ command.regexp }`, 'i'));
|
||||
return match;
|
||||
});
|
||||
|
||||
// If no command was found, trigger a search command
|
||||
if (!command) {
|
||||
command = this._searchCommand;
|
||||
match = string.match(RegExp(`^${this._searchCommand.regexp}`, 'i'));
|
||||
match = string.match(RegExp(`^${ this._searchCommand.regexp }`, 'i'));
|
||||
}
|
||||
|
||||
const paramName = command && command.param ? command.param.name : null;
|
||||
@ -101,7 +101,7 @@ class FindBar extends Component {
|
||||
if (command.search) {
|
||||
this.setState({
|
||||
activeScope: SEARCH,
|
||||
placeholder: ''
|
||||
placeholder: '',
|
||||
});
|
||||
|
||||
enteredParamValue && this.props.runCommand(SEARCH, { searchTerm: enteredParamValue });
|
||||
@ -112,7 +112,7 @@ class FindBar extends Component {
|
||||
this.setState({
|
||||
value: '',
|
||||
activeScope: command.token,
|
||||
placeholder: command.param.display
|
||||
placeholder: command.param.display,
|
||||
});
|
||||
} else {
|
||||
// Match
|
||||
@ -121,7 +121,7 @@ class FindBar extends Component {
|
||||
this.setState({
|
||||
value: '',
|
||||
placeholder: PLACEHOLDER,
|
||||
activeScope: null
|
||||
activeScope: null,
|
||||
}, () => {
|
||||
this._input.blur();
|
||||
});
|
||||
@ -137,7 +137,7 @@ class FindBar extends Component {
|
||||
if (this.state.value.length === 0 && this.state.activeScope) {
|
||||
this.setState({
|
||||
activeScope: null,
|
||||
placeholder: PLACEHOLDER
|
||||
placeholder: PLACEHOLDER,
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -160,7 +160,7 @@ class FindBar extends Component {
|
||||
const results = fuzzy.filter(value, commands, {
|
||||
pre: '<strong>',
|
||||
post: '</strong>',
|
||||
extract: el => el.token
|
||||
extract: el => el.token,
|
||||
});
|
||||
|
||||
const returnResults = results.slice(0, 4).map(result => (
|
||||
@ -171,8 +171,9 @@ class FindBar extends Component {
|
||||
return returnResults;
|
||||
}
|
||||
|
||||
handleKeyDown = event => {
|
||||
let highlightedIndex, index;
|
||||
handleKeyDown = (event) => {
|
||||
let highlightedIndex,
|
||||
index;
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
@ -202,7 +203,7 @@ class FindBar extends Component {
|
||||
const command = this.getSuggestions()[this.state.highlightedIndex];
|
||||
const newState = {
|
||||
isOpen: false,
|
||||
highlightedIndex: 0
|
||||
highlightedIndex: 0,
|
||||
};
|
||||
if (command && !command.search) {
|
||||
newState.value = command.token;
|
||||
@ -223,24 +224,24 @@ class FindBar extends Component {
|
||||
highlightedIndex: 0,
|
||||
isOpen: false,
|
||||
activeScope: null,
|
||||
placeholder: PLACEHOLDER
|
||||
placeholder: PLACEHOLDER,
|
||||
});
|
||||
break;
|
||||
case 'Backspace':
|
||||
this.setState({
|
||||
highlightedIndex: 0,
|
||||
isOpen: true
|
||||
isOpen: true,
|
||||
}, this.maybeRemoveActiveScope);
|
||||
break;
|
||||
default:
|
||||
this.setState({
|
||||
highlightedIndex: 0,
|
||||
isOpen: true
|
||||
isOpen: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleChange = event => {
|
||||
handleChange = (event) => {
|
||||
this.setState({
|
||||
value: event.target.value,
|
||||
});
|
||||
@ -250,7 +251,7 @@ class FindBar extends Component {
|
||||
if (this._ignoreBlur) return;
|
||||
this.setState({
|
||||
isOpen: false,
|
||||
highlightedIndex: 0
|
||||
highlightedIndex: 0,
|
||||
});
|
||||
};
|
||||
|
||||
@ -261,17 +262,17 @@ class FindBar extends Component {
|
||||
|
||||
handleInputClick = () => {
|
||||
if (this.state.isOpen === false)
|
||||
this.setState({ isOpen: true });
|
||||
{ this.setState({ isOpen: true }); }
|
||||
};
|
||||
|
||||
highlightCommandFromMouse = index => {
|
||||
highlightCommandFromMouse = (index) => {
|
||||
this.setState({ highlightedIndex: index });
|
||||
};
|
||||
|
||||
selectCommandFromMouse = command => {
|
||||
selectCommandFromMouse = (command) => {
|
||||
const newState = {
|
||||
isOpen: false,
|
||||
highlightedIndex: 0
|
||||
highlightedIndex: 0,
|
||||
};
|
||||
if (command && !command.search) {
|
||||
newState.value = command.token;
|
||||
@ -283,7 +284,7 @@ class FindBar extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
setIgnoreBlur = ignore => {
|
||||
setIgnoreBlur = (ignore) => {
|
||||
this._ignoreBlur = ignore;
|
||||
};
|
||||
|
||||
@ -292,14 +293,14 @@ class FindBar extends Component {
|
||||
let children;
|
||||
if (!command.search) {
|
||||
children = (
|
||||
<span><span dangerouslySetInnerHTML={{ __html: command.string }}/></span>
|
||||
<span><span dangerouslySetInnerHTML={{ __html: command.string }} /></span>
|
||||
);
|
||||
} else {
|
||||
children = (
|
||||
<span>
|
||||
{this.state.value.length === 0 ?
|
||||
<span><Icon type="search"/>Search... </span> :
|
||||
<span className={styles.faded}><Icon type="search"/>Search for: </span>
|
||||
<span><Icon type="search" />Search... </span> :
|
||||
<span className={styles.faded}><Icon type="search" />Search for: </span>
|
||||
}
|
||||
<strong>{this.state.value}</strong>
|
||||
</span>
|
||||
@ -331,7 +332,7 @@ class FindBar extends Component {
|
||||
|
||||
renderActiveScope() {
|
||||
if (this.state.activeScope === SEARCH) {
|
||||
return <div className={styles.inputScope}><Icon type="search"/></div>;
|
||||
return <div className={styles.inputScope}><Icon type="search" /></div>;
|
||||
} else {
|
||||
return <div className={styles.inputScope}>{this.state.activeScope}</div>;
|
||||
}
|
||||
@ -346,7 +347,7 @@ class FindBar extends Component {
|
||||
{scope}
|
||||
<input
|
||||
className={styles.inputField}
|
||||
ref={(c) => this._input = c}
|
||||
ref={c => this._input = c}
|
||||
onFocus={this.handleInputFocus}
|
||||
onBlur={this.handleInputBlur}
|
||||
onChange={this.handleChange}
|
||||
|
@ -10,7 +10,7 @@ const defaultSchema = {
|
||||
[BLOCKS.PARAGRAPH]: 'p',
|
||||
[BLOCKS.FOOTNOTE]: 'footnote',
|
||||
[BLOCKS.HTML]: ({ token }) => {
|
||||
return <div dangerouslySetInnerHTML={{ __html: token.get('raw') }}/>;
|
||||
return <div dangerouslySetInnerHTML={{ __html: token.get('raw') }} />;
|
||||
},
|
||||
[BLOCKS.HR]: 'hr',
|
||||
[BLOCKS.HEADING_1]: 'h1',
|
||||
@ -35,7 +35,7 @@ const defaultSchema = {
|
||||
[ENTITIES.LINK]: 'a',
|
||||
[ENTITIES.IMAGE]: 'img',
|
||||
[ENTITIES.FOOTNOTE_REF]: 'sup',
|
||||
[ENTITIES.HARD_BREAK]: 'br'
|
||||
[ENTITIES.HARD_BREAK]: 'br',
|
||||
};
|
||||
|
||||
const notAllowedAttributes = ['loose'];
|
||||
@ -50,7 +50,7 @@ function renderToken(schema, token, index = 0, key = '0') {
|
||||
const text = token.get('text');
|
||||
const tokens = token.get('tokens');
|
||||
const nodeType = schema[type];
|
||||
key = `${key}.${index}`;
|
||||
key = `${ key }.${ index }`;
|
||||
|
||||
// Only render if type is registered as renderer
|
||||
if (typeof nodeType !== 'undefined') {
|
||||
@ -101,6 +101,6 @@ MarkupItReactRenderer.propTypes = {
|
||||
syntax: PropTypes.instanceOf(Syntax).isRequired,
|
||||
schema: PropTypes.objectOf(PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.func
|
||||
]))
|
||||
PropTypes.func,
|
||||
])),
|
||||
};
|
||||
|
@ -13,9 +13,9 @@ export default class PreviewPane extends React.Component {
|
||||
this.renderPreview();
|
||||
}
|
||||
|
||||
widgetFor = name => {
|
||||
widgetFor = (name) => {
|
||||
const { collection, entry, getMedia } = this.props;
|
||||
const field = collection.get('fields').find((field) => field.get('name') === name);
|
||||
const field = collection.get('fields').find(field => field.get('name') === name);
|
||||
const widget = resolveWidget(field.get('widget'));
|
||||
return React.createElement(widget.preview, {
|
||||
key: field.get('name'),
|
||||
@ -29,7 +29,7 @@ export default class PreviewPane extends React.Component {
|
||||
const component = registry.getPreviewTemplate(this.props.collection.get('name')) || Preview;
|
||||
const previewProps = {
|
||||
...this.props,
|
||||
widgetFor: this.widgetFor
|
||||
widgetFor: this.widgetFor,
|
||||
};
|
||||
// We need to use this API in order to pass context to the iframe
|
||||
ReactDOM.unstable_renderSubtreeIntoContainer(
|
||||
@ -40,7 +40,7 @@ export default class PreviewPane extends React.Component {
|
||||
, this.previewEl);
|
||||
}
|
||||
|
||||
handleIframeRef = ref => {
|
||||
handleIframeRef = (ref) => {
|
||||
if (ref) {
|
||||
registry.getPreviewStyles().forEach((style) => {
|
||||
const linkEl = document.createElement('link');
|
||||
@ -61,7 +61,7 @@ export default class PreviewPane extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <iframe className={styles.frame} ref={this.handleIframeRef}></iframe>;
|
||||
return <iframe className={styles.frame} ref={this.handleIframeRef} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,44 +21,44 @@ export default class ScrollSync extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
registerPane = node => {
|
||||
registerPane = (node) => {
|
||||
if (!this.findPane(node)) {
|
||||
this.addEvents(node);
|
||||
this.panes.push(node);
|
||||
}
|
||||
};
|
||||
|
||||
unregisterPane = node => {
|
||||
unregisterPane = (node) => {
|
||||
if (this.findPane(node)) {
|
||||
this.removeEvents(node);
|
||||
this.panes = without(this.panes, node);
|
||||
}
|
||||
};
|
||||
|
||||
addEvents = node => {
|
||||
addEvents = (node) => {
|
||||
node.onscroll = this.handlePaneScroll.bind(this, node);
|
||||
// node.addEventListener('scroll', this.handlePaneScroll, false)
|
||||
};
|
||||
|
||||
removeEvents = node => {
|
||||
removeEvents = (node) => {
|
||||
node.onscroll = null;
|
||||
// node.removeEventListener('scroll', this.handlePaneScroll, false)
|
||||
};
|
||||
|
||||
findPane = node => {
|
||||
findPane = (node) => {
|
||||
return this.panes.find(p => p === node);
|
||||
};
|
||||
|
||||
handlePaneScroll = node => {
|
||||
handlePaneScroll = (node) => {
|
||||
// const node = evt.target
|
||||
window.requestAnimationFrame(() => {
|
||||
this.syncScrollPositions(node);
|
||||
});
|
||||
};
|
||||
|
||||
syncScrollPositions = scrolledPane => {
|
||||
syncScrollPositions = (scrolledPane) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrolledPane;
|
||||
this.panes.forEach(pane => {
|
||||
this.panes.forEach((pane) => {
|
||||
/* For all panes beside the currently scrolling one */
|
||||
if (scrolledPane !== pane) {
|
||||
/* Remove event listeners from the node that we'll manipulate */
|
||||
|
@ -5,7 +5,7 @@ export default class ScrollSyncPane extends Component {
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
attachTo: PropTypes.any
|
||||
attachTo: PropTypes.any,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -19,7 +19,6 @@ export default class Loader extends React.Component {
|
||||
const { children } = this.props;
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
|
||||
const nextItem = (this.state.currentItem === children.length - 1) ? 0 : this.state.currentItem + 1;
|
||||
this.setState({ currentItem: nextItem });
|
||||
}, 5000);
|
||||
@ -34,7 +33,7 @@ export default class Loader extends React.Component {
|
||||
return <div className={styles.text}>{children}</div>;
|
||||
} else if (Array.isArray(children)) {
|
||||
this.setAnimation();
|
||||
return <div className={styles.text}>
|
||||
return (<div className={styles.text}>
|
||||
<ReactCSSTransitionGroup
|
||||
transitionName={styles}
|
||||
transitionEnterTimeout={500}
|
||||
@ -42,7 +41,7 @@ export default class Loader extends React.Component {
|
||||
>
|
||||
<div key={currentItem} className={styles.animateItem}>{children[currentItem]}</div>
|
||||
</ReactCSSTransitionGroup>
|
||||
</div>;
|
||||
</div>);
|
||||
}
|
||||
};
|
||||
|
||||
@ -52,13 +51,12 @@ export default class Loader extends React.Component {
|
||||
// Class names
|
||||
let classNames = styles.loader;
|
||||
if (active) {
|
||||
classNames += ` ${styles.active}`;
|
||||
classNames += ` ${ styles.active }`;
|
||||
}
|
||||
if (className.length > 0) {
|
||||
classNames += ` ${className}`;
|
||||
classNames += ` ${ className }`;
|
||||
}
|
||||
|
||||
return <div className={classNames} style={style}>{this.renderChild()}</div>;
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,6 @@ class UnpublishedListing extends React.Component {
|
||||
};
|
||||
|
||||
requestPublish = (collection, slug, ownStatus) => {
|
||||
console.log('HERE');
|
||||
console.log(ownStatus);
|
||||
console.log(status.last());
|
||||
if (ownStatus !== status.last()) return;
|
||||
if (window.confirm('Are you sure you want to publish this entry?')) {
|
||||
this.props.handlePublish(collection, slug, ownStatus);
|
||||
|
@ -5,25 +5,25 @@ import MediaProxy from '../../valueObjects/MediaProxy';
|
||||
const MAX_DISPLAY_LENGTH = 50;
|
||||
|
||||
export default class ImageControl extends React.Component {
|
||||
handleFileInputRef = el => {
|
||||
handleFileInputRef = (el) => {
|
||||
this._fileInput = el;
|
||||
};
|
||||
|
||||
handleClick = e => {
|
||||
handleClick = (e) => {
|
||||
this._fileInput.click();
|
||||
};
|
||||
|
||||
handleDragEnter = e => {
|
||||
handleDragEnter = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
handleDragOver = e => {
|
||||
handleDragOver = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
handleChange = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
@ -46,7 +46,6 @@ export default class ImageControl extends React.Component {
|
||||
} else {
|
||||
this.props.onChange(null);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
renderImageName = () => {
|
||||
@ -56,7 +55,6 @@ export default class ImageControl extends React.Component {
|
||||
} else {
|
||||
return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -84,7 +82,7 @@ export default class ImageControl extends React.Component {
|
||||
|
||||
const styles = {
|
||||
input: {
|
||||
display: 'none'
|
||||
display: 'none',
|
||||
},
|
||||
imageUpload: {
|
||||
backgroundColor: '#fff',
|
||||
@ -94,8 +92,8 @@ const styles = {
|
||||
display: 'block',
|
||||
border: '1px dashed #eee',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}
|
||||
fontSize: '12px',
|
||||
},
|
||||
};
|
||||
|
||||
ImageControl.propTypes = {
|
||||
|
@ -33,7 +33,7 @@ class MarkdownControl extends React.Component {
|
||||
const { editor, onChange, onAddMedia, getMedia, value } = this.props;
|
||||
if (editor.get('useVisualMode')) {
|
||||
return (
|
||||
<div className='cms-editor-visual'>
|
||||
<div className="cms-editor-visual">
|
||||
{null && <button onClick={this.useRawEditor}>Switch to Raw Editor</button>}
|
||||
<VisualEditor
|
||||
onChange={onChange}
|
||||
@ -46,7 +46,7 @@ class MarkdownControl extends React.Component {
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className='cms-editor-raw'>
|
||||
<div className="cms-editor-raw">
|
||||
{null && <button onClick={this.useVisualEditor}>Switch to Visual Editor</button>}
|
||||
<RawEditor
|
||||
onChange={onChange}
|
||||
|
@ -8,10 +8,10 @@ import styles from './index.css';
|
||||
|
||||
Prism.languages.markdown = Prism.languages.extend('markup', {});
|
||||
Prism.languages.insertBefore('markdown', 'prolog', marks);
|
||||
Prism.languages.markdown['bold'].inside['url'] = Prism.util.clone(Prism.languages.markdown['url']);
|
||||
Prism.languages.markdown['italic'].inside['url'] = Prism.util.clone(Prism.languages.markdown['url']);
|
||||
Prism.languages.markdown['bold'].inside['italic'] = Prism.util.clone(Prism.languages.markdown['italic']);
|
||||
Prism.languages.markdown['italic'].inside['bold'] = Prism.util.clone(Prism.languages.markdown['bold']);
|
||||
Prism.languages.markdown.bold.inside.url = Prism.util.clone(Prism.languages.markdown.url);
|
||||
Prism.languages.markdown.italic.inside.url = Prism.util.clone(Prism.languages.markdown.url);
|
||||
Prism.languages.markdown.bold.inside.italic = Prism.util.clone(Prism.languages.markdown.italic);
|
||||
Prism.languages.markdown.italic.inside.bold = Prism.util.clone(Prism.languages.markdown.bold);
|
||||
|
||||
function renderDecorations(text, block) {
|
||||
let characters = text.characters.asMutable();
|
||||
@ -28,7 +28,7 @@ function renderDecorations(text, block) {
|
||||
|
||||
const length = offset + token.matchedStr.length;
|
||||
const name = token.alias || token.type;
|
||||
const type = `highlight-${name}`;
|
||||
const type = `highlight-${ name }`;
|
||||
|
||||
for (let i = offset; i < length; i++) {
|
||||
let char = characters.get(i);
|
||||
@ -47,13 +47,13 @@ function renderDecorations(text, block) {
|
||||
const SCHEMA = {
|
||||
rules: [
|
||||
{
|
||||
match: (object) => object.kind == 'block',
|
||||
decorate: renderDecorations
|
||||
}
|
||||
match: object => object.kind == 'block',
|
||||
decorate: renderDecorations,
|
||||
},
|
||||
],
|
||||
marks: {
|
||||
'highlight-comment': {
|
||||
opacity: '0.33'
|
||||
opacity: '0.33',
|
||||
},
|
||||
'highlight-important': {
|
||||
fontWeight: 'bold',
|
||||
@ -68,8 +68,8 @@ const SCHEMA = {
|
||||
},
|
||||
'highlight-punctuation': {
|
||||
color: '#006',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default class RawEditor extends React.Component {
|
||||
@ -86,19 +86,19 @@ export default class RawEditor extends React.Component {
|
||||
const content = props.value ? Plain.deserialize(props.value) : Plain.deserialize('');
|
||||
|
||||
this.state = {
|
||||
state: content
|
||||
state: content,
|
||||
};
|
||||
|
||||
this.plugins = [
|
||||
PluginDropImages({
|
||||
applyTransform: (transform, file) => {
|
||||
const mediaProxy = new MediaProxy(file.name, file);
|
||||
const state = Plain.deserialize(`\n\n![${file.name}](${mediaProxy.public_path})\n\n`);
|
||||
const state = Plain.deserialize(`\n\n![${ file.name }](${ mediaProxy.public_path })\n\n`);
|
||||
props.onAddMedia(mediaProxy);
|
||||
return transform
|
||||
.insertFragment(state.get('document'));
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@ -108,7 +108,7 @@ export default class RawEditor extends React.Component {
|
||||
* It also have an onDocumentChange, that get's dispatched only when the actual
|
||||
* content changes
|
||||
*/
|
||||
handleChange = state => {
|
||||
handleChange = (state) => {
|
||||
this.setState({ state });
|
||||
};
|
||||
|
||||
|
@ -10,11 +10,11 @@ class BlockTypesMenu extends Component {
|
||||
plugins: PropTypes.array.isRequired,
|
||||
onClickBlock: PropTypes.func.isRequired,
|
||||
onClickPlugin: PropTypes.func.isRequired,
|
||||
onClickImage: PropTypes.func.isRequired
|
||||
onClickImage: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
expanded: false
|
||||
expanded: false,
|
||||
};
|
||||
|
||||
componentWillUpdate() {
|
||||
@ -33,7 +33,7 @@ class BlockTypesMenu extends Component {
|
||||
|
||||
handlePluginClick = (e, plugin) => {
|
||||
const data = {};
|
||||
plugin.fields.forEach(field => {
|
||||
plugin.fields.forEach((field) => {
|
||||
data[field.name] = window.prompt(field.label); // eslint-disable-line
|
||||
});
|
||||
this.props.onClickPlugin(plugin.id, data);
|
||||
@ -43,7 +43,7 @@ class BlockTypesMenu extends Component {
|
||||
this._fileInput.click();
|
||||
};
|
||||
|
||||
handleFileUploadChange = e => {
|
||||
handleFileUploadChange = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
@ -62,20 +62,19 @@ class BlockTypesMenu extends Component {
|
||||
const mediaProxy = new MediaProxy(file.name, file);
|
||||
this.props.onClickImage(mediaProxy);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
renderBlockTypeButton = (type, icon) => {
|
||||
const onClick = e => this.handleBlockTypeClick(e, type);
|
||||
return (
|
||||
<Icon key={type} type={icon} onClick={onClick} className={styles.icon}/>
|
||||
<Icon key={type} type={icon} onClick={onClick} className={styles.icon} />
|
||||
);
|
||||
};
|
||||
|
||||
renderPluginButton = plugin => {
|
||||
renderPluginButton = (plugin) => {
|
||||
const onClick = e => this.handlePluginClick(e, plugin);
|
||||
return (
|
||||
<Icon key={plugin.id} type={plugin.icon} onClick={onClick} className={styles.icon}/>
|
||||
<Icon key={plugin.id} type={plugin.icon} onClick={onClick} className={styles.icon} />
|
||||
);
|
||||
};
|
||||
|
||||
@ -86,13 +85,13 @@ class BlockTypesMenu extends Component {
|
||||
<div className={styles.menu}>
|
||||
{this.renderBlockTypeButton('hr', 'dot-3')}
|
||||
{plugins.map(plugin => this.renderPluginButton(plugin))}
|
||||
<Icon type="picture" onClick={this.handleFileUploadClick} className={styles.icon}/>
|
||||
<Icon type="picture" onClick={this.handleFileUploadClick} className={styles.icon} />
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={this.handleFileUploadChange}
|
||||
className={styles.input}
|
||||
ref={el => {
|
||||
ref={(el) => {
|
||||
this._fileInput = el;
|
||||
}}
|
||||
/>
|
||||
@ -106,7 +105,7 @@ class BlockTypesMenu extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Icon type="plus-squared" className={styles.button} onClick={this.toggleMenu}/>
|
||||
<Icon type="plus-squared" className={styles.button} onClick={this.toggleMenu} />
|
||||
{this.renderMenu()}
|
||||
</div>
|
||||
);
|
||||
|
@ -11,23 +11,23 @@ class StylesMenu extends Component {
|
||||
inlines: PropTypes.object.isRequired,
|
||||
onClickBlock: PropTypes.func.isRequired,
|
||||
onClickMark: PropTypes.func.isRequired,
|
||||
onClickInline: PropTypes.func.isRequired
|
||||
onClickInline: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to set toolbar buttons to active state
|
||||
*/
|
||||
hasMark = type => {
|
||||
hasMark = (type) => {
|
||||
const { marks } = this.props;
|
||||
return marks.some(mark => mark.type == type);
|
||||
};
|
||||
|
||||
hasBlock = type => {
|
||||
hasBlock = (type) => {
|
||||
const { blocks } = this.props;
|
||||
return blocks.some(node => node.type == type);
|
||||
};
|
||||
|
||||
hasLinks = type => {
|
||||
hasLinks = (type) => {
|
||||
const { inlines } = this.props;
|
||||
return inlines.some(inline => inline.type == 'link');
|
||||
};
|
||||
@ -42,7 +42,7 @@ class StylesMenu extends Component {
|
||||
const onMouseDown = e => this.handleMarkClick(e, type);
|
||||
return (
|
||||
<span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}>
|
||||
<Icon type={icon}/>
|
||||
<Icon type={icon} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@ -57,7 +57,7 @@ class StylesMenu extends Component {
|
||||
const onMouseDown = e => this.handleInlineClick(e, 'link', isActive);
|
||||
return (
|
||||
<span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}>
|
||||
<Icon type="link"/>
|
||||
<Icon type="link" />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@ -75,14 +75,14 @@ class StylesMenu extends Component {
|
||||
const onMouseDown = e => this.handleBlockClick(e, type);
|
||||
return (
|
||||
<span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}>
|
||||
<Icon type={icon}/>
|
||||
<Icon type={icon} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={`${styles.menu} ${styles.hoverMenu}`}>
|
||||
<div className={`${ styles.menu } ${ styles.hoverMenu }`}>
|
||||
{this.renderMarkButton('BOLD', 'bold')}
|
||||
{this.renderMarkButton('ITALIC', 'italic')}
|
||||
{this.renderMarkButton('CODE', 'code')}
|
||||
|
@ -40,7 +40,7 @@ export default class VisualEditor extends React.Component {
|
||||
rawJson = emptyParagraphBlock;
|
||||
}
|
||||
this.state = {
|
||||
state: Raw.deserialize(rawJson, { terse: true })
|
||||
state: Raw.deserialize(rawJson, { terse: true }),
|
||||
};
|
||||
|
||||
this.plugins = [
|
||||
@ -50,12 +50,12 @@ export default class VisualEditor extends React.Component {
|
||||
props.onAddMedia(mediaProxy);
|
||||
return transform
|
||||
.insertBlock(mediaproxyBlock(mediaProxy));
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
getMedia = src => {
|
||||
getMedia = (src) => {
|
||||
return this.props.getMedia(src);
|
||||
};
|
||||
|
||||
@ -65,7 +65,7 @@ export default class VisualEditor extends React.Component {
|
||||
* It also have an onDocumentChange, that get's dispatched only when the actual
|
||||
* content changes
|
||||
*/
|
||||
handleChange = state => {
|
||||
handleChange = (state) => {
|
||||
if (this.blockEdit) {
|
||||
this.blockEdit = false;
|
||||
} else {
|
||||
@ -82,7 +82,7 @@ export default class VisualEditor extends React.Component {
|
||||
/**
|
||||
* Toggle marks / blocks when button is clicked
|
||||
*/
|
||||
handleMarkStyleClick = type => {
|
||||
handleMarkStyleClick = (type) => {
|
||||
let { state } = this.state;
|
||||
|
||||
state = state
|
||||
@ -100,7 +100,6 @@ export default class VisualEditor extends React.Component {
|
||||
|
||||
// Handle everything but list buttons.
|
||||
if (type != 'unordered_list' && type != 'ordered_list') {
|
||||
|
||||
if (isList) {
|
||||
transform = transform
|
||||
.setBlock(isActive ? DEFAULT_NODE : type)
|
||||
@ -165,7 +164,7 @@ export default class VisualEditor extends React.Component {
|
||||
.transform()
|
||||
.wrapInline({
|
||||
type: 'link',
|
||||
data: { href }
|
||||
data: { href },
|
||||
})
|
||||
.collapseToEnd()
|
||||
.apply();
|
||||
@ -174,14 +173,14 @@ export default class VisualEditor extends React.Component {
|
||||
this.setState({ state });
|
||||
};
|
||||
|
||||
handleBlockTypeClick = type => {
|
||||
handleBlockTypeClick = (type) => {
|
||||
let { state } = this.state;
|
||||
|
||||
state = state
|
||||
.transform()
|
||||
.insertBlock({
|
||||
type: type,
|
||||
isVoid: true
|
||||
type,
|
||||
isVoid: true,
|
||||
})
|
||||
.apply();
|
||||
|
||||
@ -194,9 +193,9 @@ export default class VisualEditor extends React.Component {
|
||||
state = state
|
||||
.transform()
|
||||
.insertInline({
|
||||
type: type,
|
||||
data: data,
|
||||
isVoid: true
|
||||
type,
|
||||
data,
|
||||
isVoid: true,
|
||||
})
|
||||
.collapseToEnd()
|
||||
.insertBlock(DEFAULT_NODE)
|
||||
@ -206,7 +205,7 @@ export default class VisualEditor extends React.Component {
|
||||
this.setState({ state });
|
||||
};
|
||||
|
||||
handleImageClick = mediaProxy => {
|
||||
handleImageClick = (mediaProxy) => {
|
||||
let { state } = this.state;
|
||||
this.props.onAddMedia(mediaProxy);
|
||||
|
||||
@ -229,12 +228,12 @@ export default class VisualEditor extends React.Component {
|
||||
.splitBlock()
|
||||
.setBlock(DEFAULT_NODE)
|
||||
.apply({
|
||||
snapshot: false
|
||||
snapshot: false,
|
||||
});
|
||||
this.setState({ state: normalized });
|
||||
};
|
||||
|
||||
handleKeyDown = evt => {
|
||||
handleKeyDown = (evt) => {
|
||||
if (evt.shiftKey && evt.key === 'Enter') {
|
||||
this.blockEdit = true;
|
||||
let { state } = this.state;
|
||||
|
@ -6,13 +6,13 @@ export default function withPortalAtCursorPosition(WrappedComponent) {
|
||||
return class extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
isOpen: React.PropTypes.bool.isRequired
|
||||
}
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
menu: null,
|
||||
cursorPosition: null
|
||||
}
|
||||
cursorPosition: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.adjustPosition();
|
||||
@ -36,22 +36,22 @@ export default function withPortalAtCursorPosition(WrappedComponent) {
|
||||
);
|
||||
const centerY = cursorPosition.top + window.scrollY;
|
||||
menu.style.opacity = 1;
|
||||
menu.style.top = `${centerY}px`;
|
||||
menu.style.left = `${centerX}px`;
|
||||
}
|
||||
menu.style.top = `${ centerY }px`;
|
||||
menu.style.left = `${ centerX }px`;
|
||||
};
|
||||
|
||||
/**
|
||||
* When the portal opens, cache the menu element.
|
||||
*/
|
||||
handleOpen = (portal) => {
|
||||
this.setState({ menu: portal.firstChild });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isOpen, ...rest } = this.props;
|
||||
return (
|
||||
<Portal isOpened={isOpen} onOpen={this.handleOpen}>
|
||||
<WrappedComponent {...rest}/>
|
||||
<WrappedComponent {...rest} />
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
@ -6,11 +6,11 @@ export const emptyParagraphBlock = {
|
||||
nodes: [{
|
||||
kind: 'text',
|
||||
ranges: [{
|
||||
text: ''
|
||||
}]
|
||||
}]
|
||||
}
|
||||
]
|
||||
text: '',
|
||||
}],
|
||||
}],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const mediaproxyBlock = mediaproxy => ({
|
||||
@ -22,7 +22,7 @@ export const mediaproxyBlock = mediaproxy => ({
|
||||
isVoid: true,
|
||||
data: {
|
||||
alt: mediaproxy.name,
|
||||
src: mediaproxy.public_path
|
||||
}
|
||||
}]
|
||||
src: mediaproxy.public_path,
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
@ -13,7 +13,7 @@ const MarkdownPreview = ({ value, getMedia }) => {
|
||||
src={getMedia(token.getIn(['data', 'src']))}
|
||||
alt={token.getIn(['data', 'alt'])}
|
||||
/>
|
||||
)
|
||||
),
|
||||
};
|
||||
|
||||
const { markdown } = getSyntaxes();
|
||||
|
@ -24,13 +24,13 @@ function processEditorPlugins(plugins) {
|
||||
// to determine whether we need to process again.
|
||||
if (plugins === processedPlugins) return;
|
||||
|
||||
plugins.forEach(plugin => {
|
||||
plugins.forEach((plugin) => {
|
||||
const basicRule = MarkupIt.Rule(plugin.id).regExp(plugin.pattern, (state, match) => (
|
||||
{ data: plugin.fromBlock(match) }
|
||||
));
|
||||
|
||||
const markdownRule = basicRule.toText((state, token) => (
|
||||
plugin.toBlock(token.getData().toObject()) + '\n\n'
|
||||
`${ plugin.toBlock(token.getData().toObject()) }\n\n`
|
||||
));
|
||||
|
||||
const htmlRule = basicRule.toText((state, token) => (
|
||||
@ -43,9 +43,9 @@ function processEditorPlugins(plugins) {
|
||||
const className = isFocused ? 'plugin active' : 'plugin';
|
||||
return (
|
||||
<div {...props.attributes} className={className}>
|
||||
<div className="plugin_icon" contentEditable={false}><Icon type={plugin.icon}/></div>
|
||||
<div className="plugin_icon" contentEditable={false}><Icon type={plugin.icon} /></div>
|
||||
<div className="plugin_fields" contentEditable={false}>
|
||||
{plugin.fields.map(field => `${field.label}: “${node.data.get(field.name)}”`)}
|
||||
{plugin.fields.map(field => `${ field.label }: “${ node.data.get(field.name) }”`)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -66,43 +66,43 @@ function processMediaProxyPlugins(getMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
var imgData = Map({
|
||||
const imgData = Map({
|
||||
alt: match[1],
|
||||
src: match[2],
|
||||
title: match[3]
|
||||
title: match[3],
|
||||
}).filter(Boolean);
|
||||
|
||||
return {
|
||||
data: imgData
|
||||
data: imgData,
|
||||
};
|
||||
});
|
||||
const mediaProxyMarkdownRule = mediaProxyRule.toText((state, token) => {
|
||||
var data = token.getData();
|
||||
var alt = data.get('alt', '');
|
||||
var src = data.get('src', '');
|
||||
var title = data.get('title', '');
|
||||
const data = token.getData();
|
||||
const alt = data.get('alt', '');
|
||||
const src = data.get('src', '');
|
||||
const title = data.get('title', '');
|
||||
|
||||
if (title) {
|
||||
return '![' + alt + '](' + src + ' "' + title + '")';
|
||||
return `![${ alt }](${ src } "${ title }")`;
|
||||
} else {
|
||||
return '![' + alt + '](' + src + ')';
|
||||
return `![${ alt }](${ src })`;
|
||||
}
|
||||
});
|
||||
const mediaProxyHTMLRule = mediaProxyRule.toText((state, token) => {
|
||||
var data = token.getData();
|
||||
var alt = data.get('alt', '');
|
||||
var src = data.get('src', '');
|
||||
return `<img src=${getMedia(src)} alt=${alt} />`;
|
||||
const data = token.getData();
|
||||
const alt = data.get('alt', '');
|
||||
const src = data.get('src', '');
|
||||
return `<img src=${ getMedia(src) } alt=${ alt } />`;
|
||||
});
|
||||
|
||||
nodes['mediaproxy'] = (props) => {
|
||||
nodes.mediaproxy = (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={getMedia(src)} className={className} />
|
||||
);
|
||||
};
|
||||
augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(mediaProxyMarkdownRule);
|
||||
@ -113,7 +113,7 @@ function getPlugins() {
|
||||
return processedPlugins.map(plugin => ({
|
||||
id: plugin.id,
|
||||
icon: plugin.icon,
|
||||
fields: plugin.fields
|
||||
fields: plugin.fields,
|
||||
})).toArray();
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ const commands = [
|
||||
|
||||
const style = {
|
||||
width: 800,
|
||||
margin: 20
|
||||
margin: 20,
|
||||
};
|
||||
|
||||
storiesOf('FindBar', module)
|
||||
|
@ -1,30 +1,280 @@
|
||||
@import '../components/UI/theme.css';
|
||||
@import "material-icons.css";
|
||||
|
||||
.layout .navDrawer .drawerContent {
|
||||
padding-top: 54px;
|
||||
max-width: 240px;
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-family: Roboto, 'Helvetica Neue', HelveticaNeue, Helvetica, Arial, sans-serif;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
& .heading {
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
background-color: #f2f5f4;
|
||||
color: #7c8382;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
:global #root,
|
||||
:global #root > * {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 30px auto 25px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #3ab7a5;
|
||||
color: #3ab7a5;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
:global {
|
||||
& .rdt {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& .rdtPicker {
|
||||
position: absolute;
|
||||
z-index: 99999 !important;
|
||||
display: none;
|
||||
margin-top: 1px;
|
||||
padding: 4px;
|
||||
width: 250px;
|
||||
border: 1px solid #f9f9f9;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
& .rdtOpen .rdtPicker {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& .rdtStatic .rdtPicker {
|
||||
position: static;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
& .rdtPicker .rdtTimeToggle {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& .rdtPicker table {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& .rdtPicker td,
|
||||
& .rdtPicker th {
|
||||
height: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& .rdtPicker td {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& .rdtPicker td.rdtDay:hover,
|
||||
& .rdtPicker td.rdtHour:hover,
|
||||
& .rdtPicker td.rdtMinute:hover,
|
||||
& .rdtPicker td.rdtSecond:hover,
|
||||
& .rdtPicker .rdtTimeToggle:hover {
|
||||
background: #eeeeee;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& .rdtPicker td.rdtOld,
|
||||
& .rdtPicker td.rdtNew {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
& .rdtPicker td.rdtToday {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& .rdtPicker td.rdtToday:before {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
display: inline-block;
|
||||
border-top-color: rgba(0, 0, 0, .2);
|
||||
border-bottom: 7px solid #428bca;
|
||||
border-left: 7px solid transparent;
|
||||
content: '';
|
||||
}
|
||||
|
||||
& .rdtPicker td.rdtActive,
|
||||
& .rdtPicker td.rdtActive:hover {
|
||||
background-color: #428bca;
|
||||
color: #fff;
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
|
||||
}
|
||||
|
||||
& .rdtPicker td.rdtActive.rdtToday:before {
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
|
||||
& .rdtPicker td.rdtDisabled,
|
||||
& .rdtPicker td.rdtDisabled:hover {
|
||||
background: none;
|
||||
color: #999999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
& .rdtPicker td span.rdtOld {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
& .rdtPicker td span.rdtDisabled,
|
||||
& .rdtPicker td span.rdtDisabled:hover {
|
||||
background: none;
|
||||
color: #999999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
& .rdtPicker th {
|
||||
border-bottom: 1px solid #f9f9f9;
|
||||
}
|
||||
|
||||
& .rdtPicker .dow {
|
||||
width: 14.2857%;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
& .rdtPicker th.rdtSwitch {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
& .rdtPicker th.rdtNext,
|
||||
& .rdtPicker th.rdtPrev {
|
||||
vertical-align: top;
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
& .rdtPrev span,
|
||||
& .rdtNext span {
|
||||
display: block;
|
||||
-webkit-user-select: none; /* Chrome/Safari/Opera */
|
||||
-khtml-user-select: none; /* Konqueror */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
}
|
||||
|
||||
& .rdtPicker th.rdtDisabled,
|
||||
& .rdtPicker th.rdtDisabled:hover {
|
||||
background: none;
|
||||
color: #999999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
& .rdtPicker thead tr:first-child th {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& .rdtPicker thead tr:first-child th:hover {
|
||||
background: #eeeeee;
|
||||
}
|
||||
|
||||
& .rdtPicker tfoot {
|
||||
border-top: 1px solid #f9f9f9;
|
||||
}
|
||||
|
||||
& .rdtPicker button {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& .rdtPicker button:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
& .rdtPicker thead button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
& td.rdtMonth,
|
||||
& td.rdtYear {
|
||||
width: 25%;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& td.rdtMonth:hover,
|
||||
& td.rdtYear:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
& .rdtCounters {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
& .rdtCounters > div {
|
||||
float: left;
|
||||
}
|
||||
|
||||
& .rdtCounter {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
& .rdtCounter {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
& .rdtCounterSeparator {
|
||||
line-height: 100px;
|
||||
}
|
||||
|
||||
& .rdtCounter .rdtBtn {
|
||||
display: block;
|
||||
height: 40%;
|
||||
line-height: 40px;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none; /* Chrome/Safari/Opera */
|
||||
-khtml-user-select: none; /* Konqueror */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
}
|
||||
|
||||
& .rdtCounter .rdtBtn:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
& .rdtCounter .rdtCount {
|
||||
height: 20%;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
& .rdtMilli {
|
||||
padding-left: 8px;
|
||||
width: 48px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
& .rdtMilli input {
|
||||
margin-top: 37px;
|
||||
width: 100%;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
padding-top: 54px;
|
||||
}
|
||||
|
||||
.notifsContainer {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 0;
|
||||
bottom: 60px;
|
||||
z-index: var(--topmostZindex);
|
||||
width: 360px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
@ -63,6 +63,6 @@ export default function CollectionPageHOC(CollectionPage) {
|
||||
|
||||
return connect(mapStateToProps, {
|
||||
updateUnpublishedEntryStatus,
|
||||
publishUnpublishedEntry
|
||||
publishUnpublishedEntry,
|
||||
})(CollectionPageHOC);
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { routerReducer } from 'react-router-redux';
|
||||
import { reducer as notifReducer } from 'redux-notifications';
|
||||
import optimist from 'redux-optimist';
|
||||
import reducers from '.';
|
||||
|
||||
export default combineReducers({
|
||||
export default optimist(combineReducers({
|
||||
...reducers,
|
||||
notifs: notifReducer,
|
||||
routing: routerReducer,
|
||||
});
|
||||
}));
|
||||
|
@ -5,8 +5,8 @@ import {
|
||||
UNPUBLISHED_ENTRY_SUCCESS,
|
||||
UNPUBLISHED_ENTRIES_REQUEST,
|
||||
UNPUBLISHED_ENTRIES_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_SUCCESS
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST,
|
||||
UNPUBLISHED_ENTRY_PUBLISH_REQUEST,
|
||||
} from '../actions/editorialWorkflow';
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
|
||||
@ -21,11 +21,11 @@ const unpublishedEntries = (state = null, action) => {
|
||||
return state;
|
||||
}
|
||||
case UNPUBLISHED_ENTRY_REQUEST:
|
||||
return state.setIn(['entities', `${action.payload.status}.${action.payload.slug}`, 'isFetching'], true);
|
||||
return state.setIn(['entities', `${ action.payload.status }.${ action.payload.slug }`, 'isFetching'], true);
|
||||
|
||||
case UNPUBLISHED_ENTRY_SUCCESS:
|
||||
return state.setIn(
|
||||
['entities', `${action.payload.status}.${action.payload.entry.slug}`],
|
||||
['entities', `${ action.payload.status }.${ action.payload.entry.slug }`],
|
||||
fromJS(action.payload.entry)
|
||||
);
|
||||
|
||||
@ -36,30 +36,32 @@ const unpublishedEntries = (state = null, action) => {
|
||||
case UNPUBLISHED_ENTRIES_SUCCESS:
|
||||
const { entries, pages } = action.payload;
|
||||
return state.withMutations((map) => {
|
||||
entries.forEach((entry) => (
|
||||
map.setIn(['entities', `${entry.metaData.status}.${entry.slug}`], fromJS(entry).set('isFetching', false))
|
||||
entries.forEach(entry => (
|
||||
map.setIn(['entities', `${ entry.metaData.status }.${ entry.slug }`], fromJS(entry).set('isFetching', false))
|
||||
));
|
||||
map.set('pages', Map({
|
||||
...pages,
|
||||
ids: List(entries.map((entry) => entry.slug))
|
||||
ids: List(entries.map(entry => entry.slug)),
|
||||
}));
|
||||
});
|
||||
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS:
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST:
|
||||
// Update Optimistically
|
||||
return state.withMutations((map) => {
|
||||
let entry = map.getIn(['entities', `${action.payload.oldStatus}.${action.payload.slug}`]);
|
||||
let entry = map.getIn(['entities', `${ action.payload.oldStatus }.${ action.payload.slug }`]);
|
||||
entry = entry.setIn(['metaData', 'status'], action.payload.newStatus);
|
||||
|
||||
let entities = map.get('entities').filter((val, key) => (
|
||||
key !== `${action.payload.oldStatus}.${action.payload.slug}`
|
||||
key !== `${ action.payload.oldStatus }.${ action.payload.slug }`
|
||||
));
|
||||
entities = entities.set(`${action.payload.newStatus}.${action.payload.slug}`, entry);
|
||||
entities = entities.set(`${ action.payload.newStatus }.${ action.payload.slug }`, entry);
|
||||
|
||||
map.set('entities', entities);
|
||||
});
|
||||
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_SUCCESS:
|
||||
return state.deleteIn(['entities', `${action.payload.status}.${action.payload.slug}`]);
|
||||
case UNPUBLISHED_ENTRY_PUBLISH_REQUEST:
|
||||
// Update Optimistically
|
||||
return state.deleteIn(['entities', `${ action.payload.status }.${ action.payload.slug }`]);
|
||||
|
||||
default:
|
||||
return state;
|
||||
@ -67,7 +69,7 @@ const unpublishedEntries = (state = null, action) => {
|
||||
};
|
||||
|
||||
export const selectUnpublishedEntry = (state, status, slug) => {
|
||||
return state && state.getIn(['entities', `${status}.${slug}`]);
|
||||
return state && state.getIn(['entities', `${ status }.${ slug }`]);
|
||||
};
|
||||
|
||||
export const selectUnpublishedEntries = (state, status) => {
|
||||
|
@ -17,7 +17,7 @@ const reducers = {
|
||||
entries,
|
||||
editorialWorkflow,
|
||||
entryDraft,
|
||||
medias
|
||||
medias,
|
||||
};
|
||||
|
||||
export default reducers;
|
||||
@ -31,7 +31,7 @@ export const selectEntry = (state, collection, slug) =>
|
||||
export const selectEntries = (state, collection) =>
|
||||
fromEntries.selectEntries(state.entries, collection);
|
||||
|
||||
export const selectSearchedEntries = (state) =>
|
||||
export const selectSearchedEntries = state =>
|
||||
fromEntries.selectSearchedEntries(state.entries);
|
||||
|
||||
export const selectUnpublishedEntry = (state, status, slug) =>
|
||||
|
Loading…
x
Reference in New Issue
Block a user