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-toolbox": "^1.2.1",
"react-waypoint": "^3.1.3", "react-waypoint": "^3.1.3",
"redux": "^3.3.1", "redux": "^3.3.1",
"redux-optimist": "^0.0.2",
"redux-notifications": "^2.1.1", "redux-notifications": "^2.1.1",
"redux-thunk": "^1.0.3", "redux-thunk": "^1.0.3",
"selection-position": "^1.0.0", "selection-position": "^1.0.0",
"semaphore": "^1.0.5", "semaphore": "^1.0.5",
"slate": "^0.14.14", "slate": "^0.14.14",
"slate-drop-or-paste-images": "^0.2.0", "slate-drop-or-paste-images": "^0.2.0",
"uuid": "^2.0.3",
"whatwg-fetch": "^1.0.0" "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 { currentBackend } from '../backends/backend';
import { getMedia } from '../reducers'; import { getMedia } from '../reducers';
import { EDITORIAL_WORKFLOW } from '../constants/publishModes'; 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_REQUEST = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST';
export const UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS'; 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_REQUEST = 'UNPUBLISHED_ENTRY_PUBLISH_REQUEST';
export const UNPUBLISHED_ENTRY_PUBLISH_SUCCESS = 'UNPUBLISHED_ENTRY_PUBLISH_SUCCESS'; export const UNPUBLISHED_ENTRY_PUBLISH_SUCCESS = 'UNPUBLISHED_ENTRY_PUBLISH_SUCCESS';
export const UNPUBLISHED_ENTRY_PUBLISH_FAILURE = 'UNPUBLISHED_ENTRY_PUBLISH_FAILURE';
/* /*
* Simple Action Creators (Internal) * Simple Action Creators (Internal)
@ -27,20 +32,20 @@ export const UNPUBLISHED_ENTRY_PUBLISH_SUCCESS = 'UNPUBLISHED_ENTRY_PUBLISH_SUCC
function unpublishedEntryLoading(status, slug) { function unpublishedEntryLoading(status, slug) {
return { return {
type: UNPUBLISHED_ENTRY_REQUEST, type: UNPUBLISHED_ENTRY_REQUEST,
payload: { status, slug } payload: { status, slug },
}; };
} }
function unpublishedEntryLoaded(status, entry) { function unpublishedEntryLoaded(status, entry) {
return { return {
type: UNPUBLISHED_ENTRY_SUCCESS, type: UNPUBLISHED_ENTRY_SUCCESS,
payload: { status, entry } payload: { status, entry },
}; };
} }
function unpublishedEntriesLoading() { function unpublishedEntriesLoading() {
return { return {
type: UNPUBLISHED_ENTRIES_REQUEST type: UNPUBLISHED_ENTRIES_REQUEST,
}; };
} }
@ -48,9 +53,9 @@ function unpublishedEntriesLoaded(entries, pagination) {
return { return {
type: UNPUBLISHED_ENTRIES_SUCCESS, type: UNPUBLISHED_ENTRIES_SUCCESS,
payload: { payload: {
entries: entries, entries,
pages: pagination pages: pagination,
} },
}; };
} }
@ -66,49 +71,69 @@ function unpublishedEntriesFailed(error) {
function unpublishedEntryPersisting(entry) { function unpublishedEntryPersisting(entry) {
return { return {
type: UNPUBLISHED_ENTRY_PERSIST_REQUEST, type: UNPUBLISHED_ENTRY_PERSIST_REQUEST,
payload: { entry } payload: { entry },
}; };
} }
function unpublishedEntryPersisted(entry) { function unpublishedEntryPersisted(entry) {
return { return {
type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS, type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
payload: { entry } payload: { entry },
}; };
} }
function unpublishedEntryPersistedFail(error) { function unpublishedEntryPersistedFail(error) {
return { return {
type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS, type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
payload: { error } payload: { error },
}; };
} }
function unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newStatus) { function unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newStatus, transactionID) {
return { return {
type: UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST, 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 { return {
type: UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS, 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 { return {
type: UNPUBLISHED_ENTRY_PUBLISH_REQUEST, 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 { return {
type: UNPUBLISHED_ENTRY_PUBLISH_SUCCESS, 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); const backend = currentBackend(state.config);
dispatch(unpublishedEntryLoading(status, slug)); dispatch(unpublishedEntryLoading(status, slug));
backend.unpublishedEntry(collection, 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); const backend = currentBackend(state.config);
dispatch(unpublishedEntriesLoading()); dispatch(unpublishedEntriesLoading());
backend.unpublishedEntries().then( backend.unpublishedEntries().then(
(response) => dispatch(unpublishedEntriesLoaded(response.entries, response.pagination)), response => dispatch(unpublishedEntriesLoaded(response.entries, response.pagination)),
(error) => dispatch(unpublishedEntriesFailed(error)) error => dispatch(unpublishedEntriesFailed(error))
); );
}; };
} }
@ -149,7 +174,7 @@ export function persistUnpublishedEntry(collection, entry) {
() => { () => {
dispatch(unpublishedEntryPersisted(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) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const backend = currentBackend(state.config); 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) backend.updateUnpublishedEntryStatus(collection, slug, newStatus)
.then(() => { .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) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const backend = currentBackend(state.config); const backend = currentBackend(state.config);
const transactionID = uuid.v4();
dispatch(unpublishedEntryPublishRequest(collection, slug, status)); dispatch(unpublishedEntryPublishRequest(collection, slug, status));
backend.publishUnpublishedEntry(collection, slug, status) backend.publishUnpublishedEntry(collection, slug, status, transactionID)
.then(() => { .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) { persistFiles(entry, mediaFiles, options) {
let filename, part, parts, subtree; let filename,
part,
parts,
subtree;
const fileTree = {}; const fileTree = {};
const uploadPromises = []; const uploadPromises = [];
@ -269,7 +272,7 @@ export default class API {
} }
updateUnpublishedEntryStatus(collection, slug, status) { updateUnpublishedEntryStatus(collection, slug, status) {
const contentKey = collection ? `${ collection }-${ slug }` : slug; const contentKey = slug;
return this.retrieveMetadata(contentKey) return this.retrieveMetadata(contentKey)
.then((metadata) => { .then((metadata) => {
return { return {
@ -281,7 +284,7 @@ export default class API {
} }
publishUnpublishedEntry(collection, slug, status) { publishUnpublishedEntry(collection, slug, status) {
const contentKey = collection ? `${ collection }-${ slug }` : slug; const contentKey = slug;
return this.retrieveMetadata(contentKey) return this.retrieveMetadata(contentKey)
.then((metadata) => { .then((metadata) => {
const headSha = metadata.pr && metadata.pr.head; const headSha = metadata.pr && metadata.pr.head;
@ -376,7 +379,9 @@ export default class API {
updateTree(sha, path, fileTree) { updateTree(sha, path, fileTree) {
return this.getTree(sha) return this.getTree(sha)
.then((tree) => { .then((tree) => {
let obj, filename, fileOrDir; let obj,
filename,
fileOrDir;
const updates = []; const updates = [];
const added = {}; const added = {};

View File

@ -83,7 +83,7 @@ export default class GitHub {
sem.leave(); sem.leave();
} else { } else {
const entryPath = data.metaData.objects.entry; 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; entry.metaData = data.metaData;
resolve(entry); resolve(entry);
sem.leave(); sem.leave();

View File

@ -12,7 +12,7 @@ class FindBar extends Component {
commands: PropTypes.arrayOf(PropTypes.shape({ commands: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
pattern: PropTypes.string.isRequired pattern: PropTypes.string.isRequired,
})).isRequired, })).isRequired,
defaultCommands: PropTypes.arrayOf(PropTypes.string), defaultCommands: PropTypes.arrayOf(PropTypes.string),
runCommand: PropTypes.func.isRequired, runCommand: PropTypes.func.isRequired,
@ -23,9 +23,9 @@ class FindBar extends Component {
this._compiledCommands = []; this._compiledCommands = [];
this._searchCommand = { this._searchCommand = {
search: true, search: true,
regexp: `(?:${SEARCH})?(.*)`, regexp: `(?:${ SEARCH })?(.*)`,
param: { name: 'searchTerm', display: '' }, param: { name: 'searchTerm', display: '' },
token: SEARCH token: SEARCH,
}; };
this.state = { this.state = {
value: '', value: '',
@ -56,7 +56,7 @@ class FindBar extends Component {
} }
// Generates a regexp and splits a token and param details for a command // Generates a regexp and splits a token and param details for a command
compileCommand = command => { compileCommand = (command) => {
let regexp = ''; let regexp = '';
let param = null; let param = null;
@ -75,7 +75,7 @@ class FindBar extends Component {
return Object.assign({}, command, { return Object.assign({}, command, {
regexp, regexp,
token, token,
param param,
}); });
}; };
@ -84,15 +84,15 @@ class FindBar extends Component {
matchCommand = () => { matchCommand = () => {
const string = this.state.activeScope ? this.state.activeScope + this.state.value : this.state.value; const string = this.state.activeScope ? this.state.activeScope + this.state.value : this.state.value;
let match; let match;
let command = this._compiledCommands.find(command => { let command = this._compiledCommands.find((command) => {
match = string.match(RegExp(`^${command.regexp}`, 'i')); match = string.match(RegExp(`^${ command.regexp }`, 'i'));
return match; return match;
}); });
// If no command was found, trigger a search command // If no command was found, trigger a search command
if (!command) { if (!command) {
command = this._searchCommand; 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; const paramName = command && command.param ? command.param.name : null;
@ -101,7 +101,7 @@ class FindBar extends Component {
if (command.search) { if (command.search) {
this.setState({ this.setState({
activeScope: SEARCH, activeScope: SEARCH,
placeholder: '' placeholder: '',
}); });
enteredParamValue && this.props.runCommand(SEARCH, { searchTerm: enteredParamValue }); enteredParamValue && this.props.runCommand(SEARCH, { searchTerm: enteredParamValue });
@ -112,7 +112,7 @@ class FindBar extends Component {
this.setState({ this.setState({
value: '', value: '',
activeScope: command.token, activeScope: command.token,
placeholder: command.param.display placeholder: command.param.display,
}); });
} else { } else {
// Match // Match
@ -121,7 +121,7 @@ class FindBar extends Component {
this.setState({ this.setState({
value: '', value: '',
placeholder: PLACEHOLDER, placeholder: PLACEHOLDER,
activeScope: null activeScope: null,
}, () => { }, () => {
this._input.blur(); this._input.blur();
}); });
@ -137,7 +137,7 @@ class FindBar extends Component {
if (this.state.value.length === 0 && this.state.activeScope) { if (this.state.value.length === 0 && this.state.activeScope) {
this.setState({ this.setState({
activeScope: null, activeScope: null,
placeholder: PLACEHOLDER placeholder: PLACEHOLDER,
}); });
} }
}; };
@ -160,7 +160,7 @@ class FindBar extends Component {
const results = fuzzy.filter(value, commands, { const results = fuzzy.filter(value, commands, {
pre: '<strong>', pre: '<strong>',
post: '</strong>', post: '</strong>',
extract: el => el.token extract: el => el.token,
}); });
const returnResults = results.slice(0, 4).map(result => ( const returnResults = results.slice(0, 4).map(result => (
@ -171,8 +171,9 @@ class FindBar extends Component {
return returnResults; return returnResults;
} }
handleKeyDown = event => { handleKeyDown = (event) => {
let highlightedIndex, index; let highlightedIndex,
index;
switch (event.key) { switch (event.key) {
case 'ArrowDown': case 'ArrowDown':
event.preventDefault(); event.preventDefault();
@ -202,7 +203,7 @@ class FindBar extends Component {
const command = this.getSuggestions()[this.state.highlightedIndex]; const command = this.getSuggestions()[this.state.highlightedIndex];
const newState = { const newState = {
isOpen: false, isOpen: false,
highlightedIndex: 0 highlightedIndex: 0,
}; };
if (command && !command.search) { if (command && !command.search) {
newState.value = command.token; newState.value = command.token;
@ -223,24 +224,24 @@ class FindBar extends Component {
highlightedIndex: 0, highlightedIndex: 0,
isOpen: false, isOpen: false,
activeScope: null, activeScope: null,
placeholder: PLACEHOLDER placeholder: PLACEHOLDER,
}); });
break; break;
case 'Backspace': case 'Backspace':
this.setState({ this.setState({
highlightedIndex: 0, highlightedIndex: 0,
isOpen: true isOpen: true,
}, this.maybeRemoveActiveScope); }, this.maybeRemoveActiveScope);
break; break;
default: default:
this.setState({ this.setState({
highlightedIndex: 0, highlightedIndex: 0,
isOpen: true isOpen: true,
}); });
} }
}; };
handleChange = event => { handleChange = (event) => {
this.setState({ this.setState({
value: event.target.value, value: event.target.value,
}); });
@ -250,7 +251,7 @@ class FindBar extends Component {
if (this._ignoreBlur) return; if (this._ignoreBlur) return;
this.setState({ this.setState({
isOpen: false, isOpen: false,
highlightedIndex: 0 highlightedIndex: 0,
}); });
}; };
@ -261,17 +262,17 @@ class FindBar extends Component {
handleInputClick = () => { handleInputClick = () => {
if (this.state.isOpen === false) if (this.state.isOpen === false)
this.setState({ isOpen: true }); { this.setState({ isOpen: true }); }
}; };
highlightCommandFromMouse = index => { highlightCommandFromMouse = (index) => {
this.setState({ highlightedIndex: index }); this.setState({ highlightedIndex: index });
}; };
selectCommandFromMouse = command => { selectCommandFromMouse = (command) => {
const newState = { const newState = {
isOpen: false, isOpen: false,
highlightedIndex: 0 highlightedIndex: 0,
}; };
if (command && !command.search) { if (command && !command.search) {
newState.value = command.token; newState.value = command.token;
@ -283,7 +284,7 @@ class FindBar extends Component {
}); });
}; };
setIgnoreBlur = ignore => { setIgnoreBlur = (ignore) => {
this._ignoreBlur = ignore; this._ignoreBlur = ignore;
}; };
@ -292,14 +293,14 @@ class FindBar extends Component {
let children; let children;
if (!command.search) { if (!command.search) {
children = ( children = (
<span><span dangerouslySetInnerHTML={{ __html: command.string }}/></span> <span><span dangerouslySetInnerHTML={{ __html: command.string }} /></span>
); );
} else { } else {
children = ( children = (
<span> <span>
{this.state.value.length === 0 ? {this.state.value.length === 0 ?
<span><Icon type="search"/>Search... </span> : <span><Icon type="search" />Search... </span> :
<span className={styles.faded}><Icon type="search"/>Search for: </span> <span className={styles.faded}><Icon type="search" />Search for: </span>
} }
<strong>{this.state.value}</strong> <strong>{this.state.value}</strong>
</span> </span>
@ -331,7 +332,7 @@ class FindBar extends Component {
renderActiveScope() { renderActiveScope() {
if (this.state.activeScope === SEARCH) { if (this.state.activeScope === SEARCH) {
return <div className={styles.inputScope}><Icon type="search"/></div>; return <div className={styles.inputScope}><Icon type="search" /></div>;
} else { } else {
return <div className={styles.inputScope}>{this.state.activeScope}</div>; return <div className={styles.inputScope}>{this.state.activeScope}</div>;
} }
@ -346,7 +347,7 @@ class FindBar extends Component {
{scope} {scope}
<input <input
className={styles.inputField} className={styles.inputField}
ref={(c) => this._input = c} ref={c => this._input = c}
onFocus={this.handleInputFocus} onFocus={this.handleInputFocus}
onBlur={this.handleInputBlur} onBlur={this.handleInputBlur}
onChange={this.handleChange} onChange={this.handleChange}

View File

@ -10,7 +10,7 @@ const defaultSchema = {
[BLOCKS.PARAGRAPH]: 'p', [BLOCKS.PARAGRAPH]: 'p',
[BLOCKS.FOOTNOTE]: 'footnote', [BLOCKS.FOOTNOTE]: 'footnote',
[BLOCKS.HTML]: ({ token }) => { [BLOCKS.HTML]: ({ token }) => {
return <div dangerouslySetInnerHTML={{ __html: token.get('raw') }}/>; return <div dangerouslySetInnerHTML={{ __html: token.get('raw') }} />;
}, },
[BLOCKS.HR]: 'hr', [BLOCKS.HR]: 'hr',
[BLOCKS.HEADING_1]: 'h1', [BLOCKS.HEADING_1]: 'h1',
@ -35,7 +35,7 @@ const defaultSchema = {
[ENTITIES.LINK]: 'a', [ENTITIES.LINK]: 'a',
[ENTITIES.IMAGE]: 'img', [ENTITIES.IMAGE]: 'img',
[ENTITIES.FOOTNOTE_REF]: 'sup', [ENTITIES.FOOTNOTE_REF]: 'sup',
[ENTITIES.HARD_BREAK]: 'br' [ENTITIES.HARD_BREAK]: 'br',
}; };
const notAllowedAttributes = ['loose']; const notAllowedAttributes = ['loose'];
@ -50,7 +50,7 @@ function renderToken(schema, token, index = 0, key = '0') {
const text = token.get('text'); const text = token.get('text');
const tokens = token.get('tokens'); const tokens = token.get('tokens');
const nodeType = schema[type]; const nodeType = schema[type];
key = `${key}.${index}`; key = `${ key }.${ index }`;
// Only render if type is registered as renderer // Only render if type is registered as renderer
if (typeof nodeType !== 'undefined') { if (typeof nodeType !== 'undefined') {
@ -101,6 +101,6 @@ MarkupItReactRenderer.propTypes = {
syntax: PropTypes.instanceOf(Syntax).isRequired, syntax: PropTypes.instanceOf(Syntax).isRequired,
schema: PropTypes.objectOf(PropTypes.oneOfType([ schema: PropTypes.objectOf(PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.func PropTypes.func,
])) ])),
}; };

View File

@ -13,9 +13,9 @@ export default class PreviewPane extends React.Component {
this.renderPreview(); this.renderPreview();
} }
widgetFor = name => { widgetFor = (name) => {
const { collection, entry, getMedia } = this.props; 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')); const widget = resolveWidget(field.get('widget'));
return React.createElement(widget.preview, { return React.createElement(widget.preview, {
key: field.get('name'), 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 component = registry.getPreviewTemplate(this.props.collection.get('name')) || Preview;
const previewProps = { const previewProps = {
...this.props, ...this.props,
widgetFor: this.widgetFor widgetFor: this.widgetFor,
}; };
// We need to use this API in order to pass context to the iframe // We need to use this API in order to pass context to the iframe
ReactDOM.unstable_renderSubtreeIntoContainer( ReactDOM.unstable_renderSubtreeIntoContainer(
@ -40,7 +40,7 @@ export default class PreviewPane extends React.Component {
, this.previewEl); , this.previewEl);
} }
handleIframeRef = ref => { handleIframeRef = (ref) => {
if (ref) { if (ref) {
registry.getPreviewStyles().forEach((style) => { registry.getPreviewStyles().forEach((style) => {
const linkEl = document.createElement('link'); const linkEl = document.createElement('link');
@ -61,7 +61,7 @@ export default class PreviewPane extends React.Component {
return null; 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)) { if (!this.findPane(node)) {
this.addEvents(node); this.addEvents(node);
this.panes.push(node); this.panes.push(node);
} }
}; };
unregisterPane = node => { unregisterPane = (node) => {
if (this.findPane(node)) { if (this.findPane(node)) {
this.removeEvents(node); this.removeEvents(node);
this.panes = without(this.panes, node); this.panes = without(this.panes, node);
} }
}; };
addEvents = node => { addEvents = (node) => {
node.onscroll = this.handlePaneScroll.bind(this, node); node.onscroll = this.handlePaneScroll.bind(this, node);
// node.addEventListener('scroll', this.handlePaneScroll, false) // node.addEventListener('scroll', this.handlePaneScroll, false)
}; };
removeEvents = node => { removeEvents = (node) => {
node.onscroll = null; node.onscroll = null;
// node.removeEventListener('scroll', this.handlePaneScroll, false) // node.removeEventListener('scroll', this.handlePaneScroll, false)
}; };
findPane = node => { findPane = (node) => {
return this.panes.find(p => p === node); return this.panes.find(p => p === node);
}; };
handlePaneScroll = node => { handlePaneScroll = (node) => {
// const node = evt.target // const node = evt.target
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
this.syncScrollPositions(node); this.syncScrollPositions(node);
}); });
}; };
syncScrollPositions = scrolledPane => { syncScrollPositions = (scrolledPane) => {
const { scrollTop, scrollHeight, clientHeight } = scrolledPane; const { scrollTop, scrollHeight, clientHeight } = scrolledPane;
this.panes.forEach(pane => { this.panes.forEach((pane) => {
/* For all panes beside the currently scrolling one */ /* For all panes beside the currently scrolling one */
if (scrolledPane !== pane) { if (scrolledPane !== pane) {
/* Remove event listeners from the node that we'll manipulate */ /* Remove event listeners from the node that we'll manipulate */

View File

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

View File

@ -19,7 +19,6 @@ export default class Loader extends React.Component {
const { children } = this.props; const { children } = this.props;
this.interval = setInterval(() => { this.interval = setInterval(() => {
const nextItem = (this.state.currentItem === children.length - 1) ? 0 : this.state.currentItem + 1; const nextItem = (this.state.currentItem === children.length - 1) ? 0 : this.state.currentItem + 1;
this.setState({ currentItem: nextItem }); this.setState({ currentItem: nextItem });
}, 5000); }, 5000);
@ -34,7 +33,7 @@ export default class Loader extends React.Component {
return <div className={styles.text}>{children}</div>; return <div className={styles.text}>{children}</div>;
} else if (Array.isArray(children)) { } else if (Array.isArray(children)) {
this.setAnimation(); this.setAnimation();
return <div className={styles.text}> return (<div className={styles.text}>
<ReactCSSTransitionGroup <ReactCSSTransitionGroup
transitionName={styles} transitionName={styles}
transitionEnterTimeout={500} transitionEnterTimeout={500}
@ -42,7 +41,7 @@ export default class Loader extends React.Component {
> >
<div key={currentItem} className={styles.animateItem}>{children[currentItem]}</div> <div key={currentItem} className={styles.animateItem}>{children[currentItem]}</div>
</ReactCSSTransitionGroup> </ReactCSSTransitionGroup>
</div>; </div>);
} }
}; };
@ -52,13 +51,12 @@ export default class Loader extends React.Component {
// Class names // Class names
let classNames = styles.loader; let classNames = styles.loader;
if (active) { if (active) {
classNames += ` ${styles.active}`; classNames += ` ${ styles.active }`;
} }
if (className.length > 0) { if (className.length > 0) {
classNames += ` ${className}`; classNames += ` ${ className }`;
} }
return <div className={classNames} style={style}>{this.renderChild()}</div>; return <div className={classNames} style={style}>{this.renderChild()}</div>;
} }
} }

View File

@ -16,9 +16,6 @@ class UnpublishedListing extends React.Component {
}; };
requestPublish = (collection, slug, ownStatus) => { requestPublish = (collection, slug, ownStatus) => {
console.log('HERE');
console.log(ownStatus);
console.log(status.last());
if (ownStatus !== status.last()) return; if (ownStatus !== status.last()) return;
if (window.confirm('Are you sure you want to publish this entry?')) { if (window.confirm('Are you sure you want to publish this entry?')) {
this.props.handlePublish(collection, slug, ownStatus); this.props.handlePublish(collection, slug, ownStatus);

View File

@ -5,25 +5,25 @@ import MediaProxy from '../../valueObjects/MediaProxy';
const MAX_DISPLAY_LENGTH = 50; const MAX_DISPLAY_LENGTH = 50;
export default class ImageControl extends React.Component { export default class ImageControl extends React.Component {
handleFileInputRef = el => { handleFileInputRef = (el) => {
this._fileInput = el; this._fileInput = el;
}; };
handleClick = e => { handleClick = (e) => {
this._fileInput.click(); this._fileInput.click();
}; };
handleDragEnter = e => { handleDragEnter = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
}; };
handleDragOver = e => { handleDragOver = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
}; };
handleChange = e => { handleChange = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -46,7 +46,6 @@ export default class ImageControl extends React.Component {
} else { } else {
this.props.onChange(null); this.props.onChange(null);
} }
}; };
renderImageName = () => { renderImageName = () => {
@ -56,7 +55,6 @@ export default class ImageControl extends React.Component {
} else { } else {
return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH); return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH);
} }
}; };
render() { render() {
@ -84,7 +82,7 @@ export default class ImageControl extends React.Component {
const styles = { const styles = {
input: { input: {
display: 'none' display: 'none',
}, },
imageUpload: { imageUpload: {
backgroundColor: '#fff', backgroundColor: '#fff',
@ -94,8 +92,8 @@ const styles = {
display: 'block', display: 'block',
border: '1px dashed #eee', border: '1px dashed #eee',
cursor: 'pointer', cursor: 'pointer',
fontSize: '12px' fontSize: '12px',
} },
}; };
ImageControl.propTypes = { ImageControl.propTypes = {

View File

@ -33,7 +33,7 @@ class MarkdownControl extends React.Component {
const { editor, onChange, onAddMedia, getMedia, value } = this.props; const { editor, onChange, onAddMedia, getMedia, value } = this.props;
if (editor.get('useVisualMode')) { if (editor.get('useVisualMode')) {
return ( return (
<div className='cms-editor-visual'> <div className="cms-editor-visual">
{null && <button onClick={this.useRawEditor}>Switch to Raw Editor</button>} {null && <button onClick={this.useRawEditor}>Switch to Raw Editor</button>}
<VisualEditor <VisualEditor
onChange={onChange} onChange={onChange}
@ -46,7 +46,7 @@ class MarkdownControl extends React.Component {
); );
} else { } else {
return ( return (
<div className='cms-editor-raw'> <div className="cms-editor-raw">
{null && <button onClick={this.useVisualEditor}>Switch to Visual Editor</button>} {null && <button onClick={this.useVisualEditor}>Switch to Visual Editor</button>}
<RawEditor <RawEditor
onChange={onChange} onChange={onChange}

View File

@ -8,10 +8,10 @@ import styles from './index.css';
Prism.languages.markdown = Prism.languages.extend('markup', {}); Prism.languages.markdown = Prism.languages.extend('markup', {});
Prism.languages.insertBefore('markdown', 'prolog', marks); Prism.languages.insertBefore('markdown', 'prolog', marks);
Prism.languages.markdown['bold'].inside['url'] = Prism.util.clone(Prism.languages.markdown['url']); 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.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.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.italic.inside.bold = Prism.util.clone(Prism.languages.markdown.bold);
function renderDecorations(text, block) { function renderDecorations(text, block) {
let characters = text.characters.asMutable(); let characters = text.characters.asMutable();
@ -28,7 +28,7 @@ function renderDecorations(text, block) {
const length = offset + token.matchedStr.length; const length = offset + token.matchedStr.length;
const name = token.alias || token.type; const name = token.alias || token.type;
const type = `highlight-${name}`; const type = `highlight-${ name }`;
for (let i = offset; i < length; i++) { for (let i = offset; i < length; i++) {
let char = characters.get(i); let char = characters.get(i);
@ -47,13 +47,13 @@ function renderDecorations(text, block) {
const SCHEMA = { const SCHEMA = {
rules: [ rules: [
{ {
match: (object) => object.kind == 'block', match: object => object.kind == 'block',
decorate: renderDecorations decorate: renderDecorations,
} },
], ],
marks: { marks: {
'highlight-comment': { 'highlight-comment': {
opacity: '0.33' opacity: '0.33',
}, },
'highlight-important': { 'highlight-important': {
fontWeight: 'bold', fontWeight: 'bold',
@ -68,8 +68,8 @@ const SCHEMA = {
}, },
'highlight-punctuation': { 'highlight-punctuation': {
color: '#006', color: '#006',
} },
} },
}; };
export default class RawEditor extends React.Component { 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(''); const content = props.value ? Plain.deserialize(props.value) : Plain.deserialize('');
this.state = { this.state = {
state: content state: content,
}; };
this.plugins = [ this.plugins = [
PluginDropImages({ PluginDropImages({
applyTransform: (transform, file) => { applyTransform: (transform, file) => {
const mediaProxy = new MediaProxy(file.name, 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); props.onAddMedia(mediaProxy);
return transform return transform
.insertFragment(state.get('document')); .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 * It also have an onDocumentChange, that get's dispatched only when the actual
* content changes * content changes
*/ */
handleChange = state => { handleChange = (state) => {
this.setState({ state }); this.setState({ state });
}; };

View File

@ -10,11 +10,11 @@ class BlockTypesMenu extends Component {
plugins: PropTypes.array.isRequired, plugins: PropTypes.array.isRequired,
onClickBlock: PropTypes.func.isRequired, onClickBlock: PropTypes.func.isRequired,
onClickPlugin: PropTypes.func.isRequired, onClickPlugin: PropTypes.func.isRequired,
onClickImage: PropTypes.func.isRequired onClickImage: PropTypes.func.isRequired,
}; };
state = { state = {
expanded: false expanded: false,
}; };
componentWillUpdate() { componentWillUpdate() {
@ -33,7 +33,7 @@ class BlockTypesMenu extends Component {
handlePluginClick = (e, plugin) => { handlePluginClick = (e, plugin) => {
const data = {}; const data = {};
plugin.fields.forEach(field => { plugin.fields.forEach((field) => {
data[field.name] = window.prompt(field.label); // eslint-disable-line data[field.name] = window.prompt(field.label); // eslint-disable-line
}); });
this.props.onClickPlugin(plugin.id, data); this.props.onClickPlugin(plugin.id, data);
@ -43,7 +43,7 @@ class BlockTypesMenu extends Component {
this._fileInput.click(); this._fileInput.click();
}; };
handleFileUploadChange = e => { handleFileUploadChange = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -62,20 +62,19 @@ class BlockTypesMenu extends Component {
const mediaProxy = new MediaProxy(file.name, file); const mediaProxy = new MediaProxy(file.name, file);
this.props.onClickImage(mediaProxy); this.props.onClickImage(mediaProxy);
} }
}; };
renderBlockTypeButton = (type, icon) => { renderBlockTypeButton = (type, icon) => {
const onClick = e => this.handleBlockTypeClick(e, type); const onClick = e => this.handleBlockTypeClick(e, type);
return ( 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); const onClick = e => this.handlePluginClick(e, plugin);
return ( 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}> <div className={styles.menu}>
{this.renderBlockTypeButton('hr', 'dot-3')} {this.renderBlockTypeButton('hr', 'dot-3')}
{plugins.map(plugin => this.renderPluginButton(plugin))} {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 <input
type="file" type="file"
accept="image/*" accept="image/*"
onChange={this.handleFileUploadChange} onChange={this.handleFileUploadChange}
className={styles.input} className={styles.input}
ref={el => { ref={(el) => {
this._fileInput = el; this._fileInput = el;
}} }}
/> />
@ -106,7 +105,7 @@ class BlockTypesMenu extends Component {
render() { render() {
return ( return (
<div className={styles.root}> <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()} {this.renderMenu()}
</div> </div>
); );

View File

@ -11,23 +11,23 @@ class StylesMenu extends Component {
inlines: PropTypes.object.isRequired, inlines: PropTypes.object.isRequired,
onClickBlock: PropTypes.func.isRequired, onClickBlock: PropTypes.func.isRequired,
onClickMark: PropTypes.func.isRequired, onClickMark: PropTypes.func.isRequired,
onClickInline: PropTypes.func.isRequired onClickInline: PropTypes.func.isRequired,
}; };
/** /**
* Used to set toolbar buttons to active state * Used to set toolbar buttons to active state
*/ */
hasMark = type => { hasMark = (type) => {
const { marks } = this.props; const { marks } = this.props;
return marks.some(mark => mark.type == type); return marks.some(mark => mark.type == type);
}; };
hasBlock = type => { hasBlock = (type) => {
const { blocks } = this.props; const { blocks } = this.props;
return blocks.some(node => node.type == type); return blocks.some(node => node.type == type);
}; };
hasLinks = type => { hasLinks = (type) => {
const { inlines } = this.props; const { inlines } = this.props;
return inlines.some(inline => inline.type == 'link'); return inlines.some(inline => inline.type == 'link');
}; };
@ -42,7 +42,7 @@ class StylesMenu extends Component {
const onMouseDown = e => this.handleMarkClick(e, type); const onMouseDown = e => this.handleMarkClick(e, type);
return ( return (
<span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}> <span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}>
<Icon type={icon}/> <Icon type={icon} />
</span> </span>
); );
}; };
@ -57,7 +57,7 @@ class StylesMenu extends Component {
const onMouseDown = e => this.handleInlineClick(e, 'link', isActive); const onMouseDown = e => this.handleInlineClick(e, 'link', isActive);
return ( return (
<span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}> <span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}>
<Icon type="link"/> <Icon type="link" />
</span> </span>
); );
}; };
@ -75,14 +75,14 @@ class StylesMenu extends Component {
const onMouseDown = e => this.handleBlockClick(e, type); const onMouseDown = e => this.handleBlockClick(e, type);
return ( return (
<span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}> <span className={styles.button} onMouseDown={onMouseDown} data-active={isActive}>
<Icon type={icon}/> <Icon type={icon} />
</span> </span>
); );
}; };
render() { render() {
return ( return (
<div className={`${styles.menu} ${styles.hoverMenu}`}> <div className={`${ styles.menu } ${ styles.hoverMenu }`}>
{this.renderMarkButton('BOLD', 'bold')} {this.renderMarkButton('BOLD', 'bold')}
{this.renderMarkButton('ITALIC', 'italic')} {this.renderMarkButton('ITALIC', 'italic')}
{this.renderMarkButton('CODE', 'code')} {this.renderMarkButton('CODE', 'code')}

View File

@ -40,7 +40,7 @@ export default class VisualEditor extends React.Component {
rawJson = emptyParagraphBlock; rawJson = emptyParagraphBlock;
} }
this.state = { this.state = {
state: Raw.deserialize(rawJson, { terse: true }) state: Raw.deserialize(rawJson, { terse: true }),
}; };
this.plugins = [ this.plugins = [
@ -50,12 +50,12 @@ export default class VisualEditor extends React.Component {
props.onAddMedia(mediaProxy); props.onAddMedia(mediaProxy);
return transform return transform
.insertBlock(mediaproxyBlock(mediaProxy)); .insertBlock(mediaproxyBlock(mediaProxy));
} },
}) }),
]; ];
} }
getMedia = src => { getMedia = (src) => {
return this.props.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 * It also have an onDocumentChange, that get's dispatched only when the actual
* content changes * content changes
*/ */
handleChange = state => { handleChange = (state) => {
if (this.blockEdit) { if (this.blockEdit) {
this.blockEdit = false; this.blockEdit = false;
} else { } else {
@ -82,7 +82,7 @@ export default class VisualEditor extends React.Component {
/** /**
* Toggle marks / blocks when button is clicked * Toggle marks / blocks when button is clicked
*/ */
handleMarkStyleClick = type => { handleMarkStyleClick = (type) => {
let { state } = this.state; let { state } = this.state;
state = state state = state
@ -100,7 +100,6 @@ export default class VisualEditor extends React.Component {
// Handle everything but list buttons. // Handle everything but list buttons.
if (type != 'unordered_list' && type != 'ordered_list') { if (type != 'unordered_list' && type != 'ordered_list') {
if (isList) { if (isList) {
transform = transform transform = transform
.setBlock(isActive ? DEFAULT_NODE : type) .setBlock(isActive ? DEFAULT_NODE : type)
@ -165,7 +164,7 @@ export default class VisualEditor extends React.Component {
.transform() .transform()
.wrapInline({ .wrapInline({
type: 'link', type: 'link',
data: { href } data: { href },
}) })
.collapseToEnd() .collapseToEnd()
.apply(); .apply();
@ -174,14 +173,14 @@ export default class VisualEditor extends React.Component {
this.setState({ state }); this.setState({ state });
}; };
handleBlockTypeClick = type => { handleBlockTypeClick = (type) => {
let { state } = this.state; let { state } = this.state;
state = state state = state
.transform() .transform()
.insertBlock({ .insertBlock({
type: type, type,
isVoid: true isVoid: true,
}) })
.apply(); .apply();
@ -194,9 +193,9 @@ export default class VisualEditor extends React.Component {
state = state state = state
.transform() .transform()
.insertInline({ .insertInline({
type: type, type,
data: data, data,
isVoid: true isVoid: true,
}) })
.collapseToEnd() .collapseToEnd()
.insertBlock(DEFAULT_NODE) .insertBlock(DEFAULT_NODE)
@ -206,7 +205,7 @@ export default class VisualEditor extends React.Component {
this.setState({ state }); this.setState({ state });
}; };
handleImageClick = mediaProxy => { handleImageClick = (mediaProxy) => {
let { state } = this.state; let { state } = this.state;
this.props.onAddMedia(mediaProxy); this.props.onAddMedia(mediaProxy);
@ -229,12 +228,12 @@ export default class VisualEditor extends React.Component {
.splitBlock() .splitBlock()
.setBlock(DEFAULT_NODE) .setBlock(DEFAULT_NODE)
.apply({ .apply({
snapshot: false snapshot: false,
}); });
this.setState({ state: normalized }); this.setState({ state: normalized });
}; };
handleKeyDown = evt => { handleKeyDown = (evt) => {
if (evt.shiftKey && evt.key === 'Enter') { if (evt.shiftKey && evt.key === 'Enter') {
this.blockEdit = true; this.blockEdit = true;
let { state } = this.state; let { state } = this.state;

View File

@ -6,13 +6,13 @@ export default function withPortalAtCursorPosition(WrappedComponent) {
return class extends React.Component { return class extends React.Component {
static propTypes = { static propTypes = {
isOpen: React.PropTypes.bool.isRequired isOpen: React.PropTypes.bool.isRequired,
} };
state = { state = {
menu: null, menu: null,
cursorPosition: null cursorPosition: null,
} };
componentDidMount() { componentDidMount() {
this.adjustPosition(); this.adjustPosition();
@ -36,22 +36,22 @@ export default function withPortalAtCursorPosition(WrappedComponent) {
); );
const centerY = cursorPosition.top + window.scrollY; const centerY = cursorPosition.top + window.scrollY;
menu.style.opacity = 1; menu.style.opacity = 1;
menu.style.top = `${centerY}px`; menu.style.top = `${ centerY }px`;
menu.style.left = `${centerX}px`; menu.style.left = `${ centerX }px`;
} };
/** /**
* When the portal opens, cache the menu element. * When the portal opens, cache the menu element.
*/ */
handleOpen = (portal) => { handleOpen = (portal) => {
this.setState({ menu: portal.firstChild }); this.setState({ menu: portal.firstChild });
} };
render() { render() {
const { isOpen, ...rest } = this.props; const { isOpen, ...rest } = this.props;
return ( return (
<Portal isOpened={isOpen} onOpen={this.handleOpen}> <Portal isOpened={isOpen} onOpen={this.handleOpen}>
<WrappedComponent {...rest}/> <WrappedComponent {...rest} />
</Portal> </Portal>
); );
} }

View File

@ -6,11 +6,11 @@ export const emptyParagraphBlock = {
nodes: [{ nodes: [{
kind: 'text', kind: 'text',
ranges: [{ ranges: [{
text: '' text: '',
}] }],
}] }],
} },
] ],
}; };
export const mediaproxyBlock = mediaproxy => ({ export const mediaproxyBlock = mediaproxy => ({
@ -22,7 +22,7 @@ export const mediaproxyBlock = mediaproxy => ({
isVoid: true, isVoid: true,
data: { data: {
alt: mediaproxy.name, 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']))} src={getMedia(token.getIn(['data', 'src']))}
alt={token.getIn(['data', 'alt'])} alt={token.getIn(['data', 'alt'])}
/> />
) ),
}; };
const { markdown } = getSyntaxes(); const { markdown } = getSyntaxes();

View File

@ -24,13 +24,13 @@ function processEditorPlugins(plugins) {
// to determine whether we need to process again. // to determine whether we need to process again.
if (plugins === processedPlugins) return; if (plugins === processedPlugins) return;
plugins.forEach(plugin => { plugins.forEach((plugin) => {
const basicRule = MarkupIt.Rule(plugin.id).regExp(plugin.pattern, (state, match) => ( const basicRule = MarkupIt.Rule(plugin.id).regExp(plugin.pattern, (state, match) => (
{ data: plugin.fromBlock(match) } { data: plugin.fromBlock(match) }
)); ));
const markdownRule = basicRule.toText((state, token) => ( 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) => ( const htmlRule = basicRule.toText((state, token) => (
@ -43,9 +43,9 @@ function processEditorPlugins(plugins) {
const className = isFocused ? 'plugin active' : 'plugin'; const className = isFocused ? 'plugin active' : 'plugin';
return ( return (
<div {...props.attributes} className={className}> <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}> <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>
</div> </div>
); );
@ -66,43 +66,43 @@ function processMediaProxyPlugins(getMedia) {
return; return;
} }
var imgData = Map({ const imgData = Map({
alt: match[1], alt: match[1],
src: match[2], src: match[2],
title: match[3] title: match[3],
}).filter(Boolean); }).filter(Boolean);
return { return {
data: imgData data: imgData,
}; };
}); });
const mediaProxyMarkdownRule = mediaProxyRule.toText((state, token) => { const mediaProxyMarkdownRule = mediaProxyRule.toText((state, token) => {
var data = token.getData(); const data = token.getData();
var alt = data.get('alt', ''); const alt = data.get('alt', '');
var src = data.get('src', ''); const src = data.get('src', '');
var title = data.get('title', ''); const title = data.get('title', '');
if (title) { if (title) {
return '![' + alt + '](' + src + ' "' + title + '")'; return `![${ alt }](${ src } "${ title }")`;
} else { } else {
return '![' + alt + '](' + src + ')'; return `![${ alt }](${ src })`;
} }
}); });
const mediaProxyHTMLRule = mediaProxyRule.toText((state, token) => { const mediaProxyHTMLRule = mediaProxyRule.toText((state, token) => {
var data = token.getData(); const data = token.getData();
var alt = data.get('alt', ''); const alt = data.get('alt', '');
var src = data.get('src', ''); const src = data.get('src', '');
return `<img src=${getMedia(src)} alt=${alt} />`; return `<img src=${ getMedia(src) } alt=${ alt } />`;
}); });
nodes['mediaproxy'] = (props) => { nodes.mediaproxy = (props) => {
/* eslint react/prop-types: 0 */ /* eslint react/prop-types: 0 */
const { node, state } = props; const { node, state } = props;
const isFocused = state.selection.hasEdgeIn(node); const isFocused = state.selection.hasEdgeIn(node);
const className = isFocused ? 'active' : null; const className = isFocused ? 'active' : null;
const src = node.data.get('src'); const src = node.data.get('src');
return ( return (
<img {...props.attributes} src={getMedia(src)} className={className}/> <img {...props.attributes} src={getMedia(src)} className={className} />
); );
}; };
augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(mediaProxyMarkdownRule); augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(mediaProxyMarkdownRule);
@ -113,7 +113,7 @@ function getPlugins() {
return processedPlugins.map(plugin => ({ return processedPlugins.map(plugin => ({
id: plugin.id, id: plugin.id,
icon: plugin.icon, icon: plugin.icon,
fields: plugin.fields fields: plugin.fields,
})).toArray(); })).toArray();
} }

View File

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

View File

@ -1,30 +1,280 @@
@import '../components/UI/theme.css'; @import "material-icons.css";
.layout .navDrawer .drawerContent { html {
padding-top: 54px; box-sizing: border-box;
max-width: 240px; 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; *:before,
padding: 1rem; *: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; 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;
}

View File

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

View File

@ -1,10 +1,11 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux'; import { routerReducer } from 'react-router-redux';
import { reducer as notifReducer } from 'redux-notifications'; import { reducer as notifReducer } from 'redux-notifications';
import optimist from 'redux-optimist';
import reducers from '.'; import reducers from '.';
export default combineReducers({ export default optimist(combineReducers({
...reducers, ...reducers,
notifs: notifReducer, notifs: notifReducer,
routing: routerReducer, routing: routerReducer,
}); }));

View File

@ -5,8 +5,8 @@ import {
UNPUBLISHED_ENTRY_SUCCESS, UNPUBLISHED_ENTRY_SUCCESS,
UNPUBLISHED_ENTRIES_REQUEST, UNPUBLISHED_ENTRIES_REQUEST,
UNPUBLISHED_ENTRIES_SUCCESS, UNPUBLISHED_ENTRIES_SUCCESS,
UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS, UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST,
UNPUBLISHED_ENTRY_PUBLISH_SUCCESS UNPUBLISHED_ENTRY_PUBLISH_REQUEST,
} from '../actions/editorialWorkflow'; } from '../actions/editorialWorkflow';
import { CONFIG_SUCCESS } from '../actions/config'; import { CONFIG_SUCCESS } from '../actions/config';
@ -21,11 +21,11 @@ const unpublishedEntries = (state = null, action) => {
return state; return state;
} }
case UNPUBLISHED_ENTRY_REQUEST: 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: case UNPUBLISHED_ENTRY_SUCCESS:
return state.setIn( return state.setIn(
['entities', `${action.payload.status}.${action.payload.entry.slug}`], ['entities', `${ action.payload.status }.${ action.payload.entry.slug }`],
fromJS(action.payload.entry) fromJS(action.payload.entry)
); );
@ -36,30 +36,32 @@ const unpublishedEntries = (state = null, action) => {
case UNPUBLISHED_ENTRIES_SUCCESS: case UNPUBLISHED_ENTRIES_SUCCESS:
const { entries, pages } = action.payload; const { entries, pages } = action.payload;
return state.withMutations((map) => { return state.withMutations((map) => {
entries.forEach((entry) => ( entries.forEach(entry => (
map.setIn(['entities', `${entry.metaData.status}.${entry.slug}`], fromJS(entry).set('isFetching', false)) map.setIn(['entities', `${ entry.metaData.status }.${ entry.slug }`], fromJS(entry).set('isFetching', false))
)); ));
map.set('pages', Map({ map.set('pages', Map({
...pages, ...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) => { 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); entry = entry.setIn(['metaData', 'status'], action.payload.newStatus);
let entities = map.get('entities').filter((val, key) => ( 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); map.set('entities', entities);
}); });
case UNPUBLISHED_ENTRY_PUBLISH_SUCCESS: case UNPUBLISHED_ENTRY_PUBLISH_REQUEST:
return state.deleteIn(['entities', `${action.payload.status}.${action.payload.slug}`]); // Update Optimistically
return state.deleteIn(['entities', `${ action.payload.status }.${ action.payload.slug }`]);
default: default:
return state; return state;
@ -67,7 +69,7 @@ const unpublishedEntries = (state = null, action) => {
}; };
export const selectUnpublishedEntry = (state, status, slug) => { 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) => { export const selectUnpublishedEntries = (state, status) => {

View File

@ -17,7 +17,7 @@ const reducers = {
entries, entries,
editorialWorkflow, editorialWorkflow,
entryDraft, entryDraft,
medias medias,
}; };
export default reducers; export default reducers;
@ -31,7 +31,7 @@ export const selectEntry = (state, collection, slug) =>
export const selectEntries = (state, collection) => export const selectEntries = (state, collection) =>
fromEntries.selectEntries(state.entries, collection); fromEntries.selectEntries(state.entries, collection);
export const selectSearchedEntries = (state) => export const selectSearchedEntries = state =>
fromEntries.selectSearchedEntries(state.entries); fromEntries.selectSearchedEntries(state.entries);
export const selectUnpublishedEntry = (state, status, slug) => export const selectUnpublishedEntry = (state, status, slug) =>