Optimistic Updates (#114)

* Optimistic Updates structure
* Optimistic update for Editorial Workflow
This commit is contained in:
Cássio Souza 2016-10-18 12:32:39 -02:00 committed by GitHub
parent 009c881290
commit 45c7e8b08b
28 changed files with 528 additions and 243 deletions

View File

@ -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"
}
}

View File

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

View File

@ -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 = {};

View File

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

View File

@ -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,
@ -25,7 +25,7 @@ class FindBar extends Component {
search: true,
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,7 +84,7 @@ 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 => {
let command = this._compiledCommands.find((command) => {
match = string.match(RegExp(`^${ command.regexp }`, 'i'));
return match;
});
@ -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;
};
@ -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}

View File

@ -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'];
@ -101,6 +101,6 @@ MarkupItReactRenderer.propTypes = {
syntax: PropTypes.instanceOf(Syntax).isRequired,
schema: PropTypes.objectOf(PropTypes.oneOfType([
PropTypes.string,
PropTypes.func
]))
PropTypes.func,
])),
};

View File

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

View File

@ -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 */

View File

@ -5,7 +5,7 @@ export default class ScrollSyncPane extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
attachTo: PropTypes.any
attachTo: PropTypes.any,
};
static contextTypes = {

View File

@ -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>);
}
};
@ -59,6 +58,5 @@ export default class Loader extends React.Component {
}
return <div className={classNames} style={style}>{this.renderChild()}</div>;
}
}

View File

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

View File

@ -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 = {

View File

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

View File

@ -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();
@ -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,7 +86,7 @@ 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 = [
@ -97,8 +97,8 @@ export default class RawEditor extends React.Component {
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 });
};

View File

@ -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,7 +62,6 @@ class BlockTypesMenu extends Component {
const mediaProxy = new MediaProxy(file.name, file);
this.props.onClickImage(mediaProxy);
}
};
renderBlockTypeButton = (type, icon) => {
@ -72,7 +71,7 @@ class BlockTypesMenu extends Component {
);
};
renderPluginButton = plugin => {
renderPluginButton = (plugin) => {
const onClick = e => this.handlePluginClick(e, plugin);
return (
<Icon key={plugin.id} type={plugin.icon} onClick={onClick} className={styles.icon} />
@ -92,7 +91,7 @@ class BlockTypesMenu extends Component {
accept="image/*"
onChange={this.handleFileUploadChange}
className={styles.input}
ref={el => {
ref={(el) => {
this._fileInput = el;
}}
/>

View File

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

View File

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

View File

@ -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();
@ -38,14 +38,14 @@ export default function withPortalAtCursorPosition(WrappedComponent) {
menu.style.opacity = 1;
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;

View File

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

View File

@ -13,7 +13,7 @@ const MarkdownPreview = ({ value, getMedia }) => {
src={getMedia(token.getIn(['data', 'src']))}
alt={token.getIn(['data', 'alt'])}
/>
)
),
};
const { markdown } = getSyntaxes();

View File

@ -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) => (
@ -66,36 +66,36 @@ 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', '');
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);
@ -113,7 +113,7 @@ function getPlugins() {
return processedPlugins.map(plugin => ({
id: plugin.id,
icon: plugin.icon,
fields: plugin.fields
fields: plugin.fields,
})).toArray();
}

View File

@ -27,7 +27,7 @@ const commands = [
const style = {
width: 800,
margin: 20
margin: 20,
};
storiesOf('FindBar', module)

View File

@ -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 {
*,
*:before,
*:after {
box-sizing: inherit;
}
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;
padding: 1rem;
}
& .heading {
& .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;
}
.main {
padding-top: 54px;
& .rdtPicker button:hover {
background-color: #eee;
}
.notifsContainer {
position: fixed;
top: 60px;
right: 0;
bottom: 60px;
z-index: var(--topmostZindex);
width: 360px;
pointer-events: none;
& .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;
}
}

View File

@ -63,6 +63,6 @@ export default function CollectionPageHOC(CollectionPage) {
return connect(mapStateToProps, {
updateUnpublishedEntryStatus,
publishUnpublishedEntry
publishUnpublishedEntry,
})(CollectionPageHOC);
}

View File

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

View File

@ -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';
@ -36,16 +36,17 @@ const unpublishedEntries = (state = null, action) => {
case UNPUBLISHED_ENTRIES_SUCCESS:
const { entries, pages } = action.payload;
return state.withMutations((map) => {
entries.forEach((entry) => (
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 }`]);
entry = entry.setIn(['metaData', 'status'], action.payload.newStatus);
@ -58,7 +59,8 @@ const unpublishedEntries = (state = null, action) => {
map.set('entities', entities);
});
case UNPUBLISHED_ENTRY_PUBLISH_SUCCESS:
case UNPUBLISHED_ENTRY_PUBLISH_REQUEST:
// Update Optimistically
return state.deleteIn(['entities', `${ action.payload.status }.${ action.payload.slug }`]);
default:

View File

@ -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) =>