Skip to content

Commit

Permalink
Add Post Options (Automattic#63)
Browse files Browse the repository at this point in the history
Add tags, categories and site refresh options to new expandable drawer under each site
  • Loading branch information
georgeh authored Apr 12, 2017
1 parent 8dfc327 commit 594933e
Show file tree
Hide file tree
Showing 16 changed files with 563 additions and 257 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules
gapps.config.json*
src/Code.js
src/javascript.html
.vscode
123 changes: 123 additions & 0 deletions client/app.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/* global React */
import { loadSites, getAuthUrl, deleteSite, refreshSite } from './services';
import Site from './site.jsx';
import ErrorMessage from './error-message.jsx'

export default class App extends React.Component {
constructor( props ) {
super( props )
this.state = {
sitesLoaded: false,
sites: [],
error: null,
authorizationUrl: null
};
this.updateAuthUrl = this.updateAuthUrl.bind( this )
this.updateSiteList = this.updateSiteList.bind( this )
this.errorHandler = this.errorHandler.bind( this )
this.clearError = this.clearError.bind( this )
}

componentDidMount() {
this.updateSiteList()
this.updateAuthUrl()
this.authTimer = setInterval( () => this.updateAuthUrl(), 1000 * 60 * 3 )
}

componentWillUnmount() {
clearInterval( this.authTimer )
}

updateSiteList() {
loadSites()
.then( ( sites ) => this.setState( { sites, sitesLoaded: true } ) )
.catch( ( e ) => this.setState( { error: e } ) )
}

updateAuthUrl() {
getAuthUrl()
.then( ( authorizationUrl ) => this.setState( { authorizationUrl } ) )
.catch( ( e ) => this.setState( { error: e } ) )
}

errorHandler( e ) {
this.setState( { error: e } )
}

clearError() {
this.setState( { error: null } )
}

removeSite( blog_id ) {
return deleteSite( blog_id )
.then( this.updateSiteList )
.catch( this.errorHandler )
}

setPost( blog_id, post ) {
const sites = this.state.sites.map( site => {
if ( site.blog_id !== blog_id ) {
return site
}

return Object.assign( {}, site, { post } )
} )
this.setState( { sites } )
}

/**
* @param {number} blog_id unique id for the site
* @returns {Promise} for new site information
*/
updateSite( blog_id ) {
return refreshSite( blog_id )
.then( updatedSite => {
const sites = this.state.sites.map( site => ( site.blog_id === updatedSite.blog_id ) ? updatedSite : site )
this.setState( { sites } )
return updatedSite
} )
.catch( this.errorHandler )
}

render() {
const hasSites = this.state.sitesLoaded && ( this.state.sites.length > 0 )
const headerCopy = hasSites
? 'Pick a site to copy this document to below. It will be saved on your site as a draft.'
: 'Welcome! To get started, add your first WordPress.com or Jetpack-connected site by clicking the button at the bottom.'
const loadingMessage = ( ! this.state.sitesLoaded ) ? <p className="loading">Loading…</p> : null

return <div className="container">
<div className="header">
<h1><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="0" fill="none" width="24" height="24"/><g><path d="M21 11v8c0 1.105-.895 2-2 2H5c-1.105 0-2-.895-2-2V5c0-1.105.895-2 2-2h8l-2 2H5v14h14v-6l2-2zM7 17h3l7.5-7.5-3-3L7 14v3zm9.94-12.94L15.5 5.5l3 3 1.44-1.44c.585-.585.585-1.535 0-2.12l-.88-.88c-.585-.585-1.535-.585-2.12 0z"/></g></svg> Draft to WordPress</h1>

<div className="header__help-text">
<p>{ headerCopy }</p>
</div>
</div>

<div className="sites-list" id="sites-list">
{ loadingMessage }
<ul>
{ this.state.sites.map( site =>
<Site
key={ site.blog_id }
site={ site }
errorHandler={ this.errorHandler }
setPost={ this.setPost.bind( this, site.blog_id ) }
removeSite={ this.removeSite.bind( this, site.blog_id ) }
refreshSite={ this.updateSite.bind( this, site.blog_id ) }
updateSiteList={ this.updateSiteList } /> ) }
<li className="sites-list__add-site"><a className="button button-secondary" href={ this.state.authorizationUrl } target="_blank">Add WordPress Site</a></li>
</ul>
</div>

<ErrorMessage msg={ this.state.error } clearError={ this.clearError } />

<div className="footer">
<div className="footer__help-link">
<a title="Help" href="https://apps.wordpress.com/google-docs/support/"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="0" fill="none" width="24" height="24"/><g><path d="M12 4c4.41 0 8 3.59 8 8s-3.59 8-8 8-8-3.59-8-8 3.59-8 8-8m0-2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm4 8c0-2.21-1.79-4-4-4s-4 1.79-4 4h2c0-1.103.897-2 2-2s2 .897 2 2-.897 2-2 2c-.552 0-1 .448-1 1v2h2v-1.14c1.722-.447 3-1.998 3-3.86zm-3 6h-2v2h2v-2z"/></g></svg></a>
</div>
</div>
</div>
}
}
13 changes: 13 additions & 0 deletions client/category-input.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default ( props ) => {
const { category } = props
const checked = ( -1 !== props.postCategories.indexOf( category.name ) )
const changeHandler = ( { target } ) => {
if ( target.checked ) {
props.addCategory( props.category.name )
} else {
props.removeCategory( props.category.name )
}
}

return <li><label><input type="checkbox" checked={ checked } onChange={ changeHandler } /> { category.name }</label></li>
}
30 changes: 30 additions & 0 deletions client/error-message.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export default ( props ) => {
if ( ! props.msg ) {
return null;
}

const errorMatch = props.msg.message.match( /"message":"(.*?)"}/ );
let errorBody = <div>
<p><code style={ { fontSize: '12px', wordWrap: 'break-word' } }>{ props.msg.message }</code></p>
<p><a href="https://support.wordpress.com" target="_blank">Please report this error</a></p>
</div>

// Bit of sniffing
if ( props.msg.message.indexOf( 'API calls to this blog have been disabled' ) !== -1 ) {
errorBody = <p>Jetpack JSON API has been disabled - please <a href="https://apps.wordpress.com/google-docs/support/#json-api" target="_blank">re-enable it</a> to continue posting.</p>
} else if ( props.msg.message.indexOf( 'cURL error 28:' ) !== -1 ) {
errorBody = <p>Connection to your site timed out. Try again and/or investigate server load.</p>
} else if ( props.msg.message.indexOf( '[-32700]' ) !== -1 ) {
errorBody = <p>Your site returned an unexpected response - are you <a href="https://apps.wordpress.com/google-docs/support/#other-plugins">running plugins</a> that may be causing a problem?</p>
} else if ( props.msg.message.indexOf( 'HTTP status code was not 200' ) !== -1 ) {
errorBody = <p>Your server returned an unexpected response. This could be a misconfiguration, a bad plugin, or something may be broken. <a href="https://apps.wordpress.com/google-docs/support/#unexpected-response">Don't panic yet though!</a></p>
} else if ( errorMatch && errorMatch.length > 0 ) {
errorBody = <p>{ errorMatch[ 1 ] }</p>
}

return <div className="error" style={ {margin: '0px 6px', position: 'relative' } }>
<p style={ { width: '100%' } } ><svg onClick={ props.clearError } style={ { cursor: 'pointer', position: 'absolute', right: '5px', top: '12px' } } width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="0" fill="none" width="24" height="24"/><g><path d="M17.705 7.705l-1.41-1.41L12 10.59 7.705 6.295l-1.41 1.41L10.59 12l-4.295 4.295 1.41 1.41L12 13.41l4.295 4.295 1.41-1.41L13.41 12l4.295-4.295z"/></g></svg>
<strong>Error { props.errorTitle }</strong></p>
{ errorBody }
</div>
}
182 changes: 1 addition & 181 deletions client/index.jsx
Original file line number Diff line number Diff line change
@@ -1,184 +1,4 @@
/* global document, React, ReactDOM */
import { loadSites, deleteSite, postToWordPress, getAuthUrl } from './services';

class PostButton extends React.Component {
constructor( props ) {
super( props );
this.state = {
disabled: false,
post: props.post
};
this.savePost = this.savePost.bind( this )
}

savePost() {
this.setState( { disabled: true } )
postToWordPress( this.props.site.blog_id )
.then( ( post ) => {
this.setState( { disabled: false } )
this.setState( { post} )
this.props.onPostSave( post )
} )
.catch( ( e ) => {
this.props.errorHandler( e )
this.setState( { disabled: false } )
} )
}

render() {
const buttonLabel = ( this.state.post ) ? 'Update' : 'Save';

return <button className="sites-list__save-draft" disabled={ this.state.disabled } onClick={ this.savePost }>{ buttonLabel }</button>
}
}

class Site extends React.Component {
constructor( props ) {
super( props )
this.state = {
post: props.site.post,
};
this.setPost = this.setPost.bind( this )
}

setPost( post ) {
this.setState( { post } )
}

render() {
const site = this.props.site
const blavatar = ( site.info.icon && site.info.icon.img ) ? site.info.icon.img : 'https://secure.gravatar.com/blavatar/e6392390e3bcfadff3671c5a5653d95b'
const previewLink = ( this.state.post ) ? <span className="sites-list__post-link"><a href={ this.state.post.URL }>Preview on { site.info.name }</a></span> : null;
const removeSite = () => deleteSite( site.blog_id ).then( this.props.updateSiteList ).catch( this.props.errorHandler )

return <li>
<div className="sites-list__basic">
<div className="sites-list__blavatar">
<img src={ blavatar } alt="" />
</div>
<div className="sites-list__sitename">
<a className="sites-list__title" href={ site.blog_url }>{ site.info.name }<br />
<em>{ site.blog_url }</em></a>
<a title="Remove site from this list" className="sites-list__delete-site" onClick={ removeSite }><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="0" fill="none" width="24" height="24"/><g><path d="M17.705 7.705l-1.41-1.41L12 10.59 7.705 6.295l-1.41 1.41L10.59 12l-4.295 4.295 1.41 1.41L12 13.41l4.295 4.295 1.41-1.41L13.41 12l4.295-4.295z"/></g></svg></a>
</div>
<PostButton site={ site } post={ this.state.post } onPostSave={ this.setPost } errorHandler={ this.props.errorHandler } />
</div>
<div className="sites-list__extended">
{ previewLink }
</div>
</li>
}
}

const ErrorMessage = ( props ) => {
if ( ! props.msg ) {
return null;
}

const errorMatch = props.msg.message.match( /"message":"(.*?)"}/ );
let errorBody = <div>
<p><code style="font-size: 12px; word-wrap: break-word">{ props.msg.message }</code></p>
<p><a href="https://support.wordpress.com" target="_blank">Please report this error</a></p>
</div>

// Bit of sniffing
if ( props.msg.message.indexOf( 'API calls to this blog have been disabled' ) !== -1 ) {
errorBody = <p>Jetpack JSON API has been disabled - please <a href="https://apps.wordpress.com/google-docs/support/#json-api" target="_blank">re-enable it</a> to continue posting.</p>
} else if ( props.msg.message.indexOf( 'cURL error 28:' ) !== -1 ) {
errorBody = <p>Connection to your site timed out. Try again and/or investigate server load.</p>
} else if ( props.msg.message.indexOf( '[-32700]' ) !== -1 ) {
errorBody = <p>Your site returned an unexpected response - are you <a href="https://apps.wordpress.com/google-docs/support/#other-plugins">running plugins</a> that may be causing a problem?</p>
} else if ( props.msg.message.indexOf( 'HTTP status code was not 200' ) !== -1 ) {
errorBody = <p>Your server returned an unexpected response. This could be a misconfiguration, a bad plugin, or something may be broken. <a href="https://apps.wordpress.com/google-docs/support/#unexpected-response">Don't panic yet though!</a></p>
} else if ( errorMatch && errorMatch.length > 0 ) {
errorBody = <p>{ errorMatch[ 1 ] }</p>
}

return <div className="error" style={ {margin: '0px 6px', position: 'relative' } }>
<p style={ { width: '100%' } } ><svg onClick={ props.clearError } style={ { cursor: 'pointer', position: 'absolute', right: '5px', top: '12px' } } width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="0" fill="none" width="24" height="24"/><g><path d="M17.705 7.705l-1.41-1.41L12 10.59 7.705 6.295l-1.41 1.41L10.59 12l-4.295 4.295 1.41 1.41L12 13.41l4.295 4.295 1.41-1.41L13.41 12l4.295-4.295z"/></g></svg>
<strong>Error { props.errorTitle }</strong></p>
{ errorBody }
</div>
}

class App extends React.Component {
constructor( props ) {
super( props )
this.state = {
sitesLoaded: false,
sites: [],
error: null,
authorizationUrl: null
};
this.updateAuthUrl = this.updateAuthUrl.bind( this )
this.updateSiteList = this.updateSiteList.bind( this )
this.errorHandler = this.errorHandler.bind( this )
this.clearError = this.clearError.bind( this )
}

componentDidMount() {
this.updateSiteList()
this.updateAuthUrl()
this.authTimer = setInterval( () => this.updateAuthUrl(), 1000 * 60 * 3 )
}

componentWillUnmount() {
clearInterval( this.authTimer )
}

updateSiteList() {
loadSites()
.then( ( sites ) => this.setState( { sites, sitesLoaded: true } ) )
.catch( ( e ) => this.setState( { error: e } ) )
}

updateAuthUrl() {
getAuthUrl()
.then( ( authorizationUrl ) => this.setState( { authorizationUrl } ) )
.catch( ( e ) => this.setState( { error: e } ) )
}

errorHandler( e ) {
this.setState( { error: e } )
}

clearError() {
this.setState( { error: null } )
}

render() {
const hasSites = this.state.sitesLoaded && ( this.state.sites.length > 0 )
const headerCopy = hasSites
? 'Pick a site to copy this document to below. It will be saved on your site as a draft.'
: 'Welcome! To get started, add your first WordPress.com or Jetpack-connected site by clicking the button at the bottom.'
const loadingMessage = ( ! this.state.sitesLoaded ) ? <p className="loading">Loading…</p> : null

return <div className="container">
<div className="header">
<h1><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="0" fill="none" width="24" height="24"/><g><path d="M21 11v8c0 1.105-.895 2-2 2H5c-1.105 0-2-.895-2-2V5c0-1.105.895-2 2-2h8l-2 2H5v14h14v-6l2-2zM7 17h3l7.5-7.5-3-3L7 14v3zm9.94-12.94L15.5 5.5l3 3 1.44-1.44c.585-.585.585-1.535 0-2.12l-.88-.88c-.585-.585-1.535-.585-2.12 0z"/></g></svg> Draft to WordPress</h1>

<div className="header__help-text">
<p>{ headerCopy }</p>
</div>
</div>

<div className="sites-list" id="sites-list">
{ loadingMessage }
<ul>
{ this.state.sites.map( site => <Site key={ site.blog_id } site={ site } errorHandler={ this.errorHandler } updateSiteList={ this.updateSiteList } /> ) }
<li><a className="button button-secondary" href={ this.state.authorizationUrl } target="_blank">Add WordPress Site</a></li>
</ul>
</div>

<ErrorMessage msg={ this.state.error } clearError={ this.clearError } />

<div className="footer">
<div className="footer__help-link">
<a title="Help" href="https://apps.wordpress.com/google-docs/support/"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect x="0" fill="none" width="24" height="24"/><g><path d="M12 4c4.41 0 8 3.59 8 8s-3.59 8-8 8-8-3.59-8-8 3.59-8 8-8m0-2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm4 8c0-2.21-1.79-4-4-4s-4 1.79-4 4h2c0-1.103.897-2 2-2s2 .897 2 2-.897 2-2 2c-.552 0-1 .448-1 1v2h2v-1.14c1.722-.447 3-1.998 3-3.86zm-3 6h-2v2h2v-2z"/></g></svg></a>
</div>
</div>
</div>
}
}
import App from './app.jsx';

ReactDOM.render( <App />, document.getElementById( 'container' ) );
32 changes: 32 additions & 0 deletions client/post-button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* global React */
import { postToWordPress } from './services';

export default class PostButton extends React.Component {
constructor( props ) {
super( props );
this.state = { disabled: false };
this.savePost = this.savePost.bind( this )
}

savePost() {
this.setState( { disabled: true } )
postToWordPress( this.props.site.blog_id, {
categories: this.props.postCategories,
tags: this.props.postTags
} )
.then( ( post ) => {
this.setState( { disabled: false } )
this.props.onPostSave( post )
} )
.catch( ( e ) => {
this.props.errorHandler( e )
this.setState( { disabled: false } )
} )
}

render() {
const buttonLabel = ( this.props.site.post ) ? 'Update' : 'Save';

return <button className="sites-list__save-draft" disabled={ this.state.disabled } onClick={ this.savePost }>{ buttonLabel }</button>
}
}
Loading

0 comments on commit 594933e

Please sign in to comment.