diff --git a/.eslintrc b/.eslintrc index 6620eb90f2e54e6dd1eaf5bcc6f92396d0f5d31a..2a0497b593fb60dcd4dd91632ac94f98cb5fabce 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,22 @@ // Use this file as a starting point for your project's .eslintrc. // Copy this file, and add rule overrides as needed. { - "extends": ["standard", "standard-react"] + "extends": [ + "airbnb", + "standard", + "standard-react" + ], + "parser": "babel-eslint", + "react/sort-comp": [1, { + "order": [ + "constructor", + "lifecycle", + "everything-else", + "render" + ] + }], + "env": { + "es6": true, + "browser": true + } } diff --git a/.gitignore b/.gitignore index 10a8b909518f233620d978087555e52b48124e4b..0e92e2df7c1766a1fded8cd713bf147a35047171 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .env.* .DS_Store _build/* +__snapshots__/ api/db/* api/db/* coverage @@ -11,4 +12,3 @@ public/assets/* public/uploads/* pubsweet.log uploads/* -__snapshots__/ \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 1e8b314962144c26d5e0e50fd29d2ca327864913..fbf1779b2046961c349259c48897ce0c399d41cf 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -6 +7.9 diff --git a/README.md b/README.md index f550d9166d4020bb550ce43dde8761e5a4d18497..2a05ca07bfd73975678a0d6e17a9f50c87e5bded 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ -## Overview +## Overview Editoria is a book production platform, built with [Pubsweet](https://gitlab.coko.foundation/pubsweet/) and [Substance](http://substance.io/). -This application is being developed by the [Coko Foundation](https://coko.foundation/), for the [University of California Press](http://www.ucpress.edu/). +This application is being developed by the [Coko Foundation](https://coko.foundation/), for the [University of California Press](http://www.ucpress.edu/). <br> -## Installation +## Requirements + +Node >= 7.6 +Npm >= 3 +<br> + +Pubsweet >= 0.3.2 + +## Installation First you need to clone this repository on your machine. ```git clone git@gitlab.coko.foundation:yannisbarlas/editoria.git``` @@ -16,7 +24,7 @@ Once you have, navigate to the project's root directory. ```cd editoria``` <br> -This application is being developed with node 6 in mind. +This application requires node 7.6 or greater. If you use nvm for managing different node versions, the project includes an ```.nvmrc``` file that you can take advantage of. <br> @@ -34,7 +42,7 @@ Create a database. If you want to create a database for a development environment, simply pass the ```--dev``` option to the above command. ```pubsweet setupdb ./ --dev``` -Follow the instructions, create the administrator user and name your book. +Follow the instructions, create the administrator user and name your first book. <br> Once that is done, you can run the app like so: @@ -43,16 +51,13 @@ Or if you want the development environment: ```pubsweet run --dev``` <br> -## Set up +## Set up Log in as an administrator, and click on the "Teams" link in the navigation bar. <br> -Create 3 teams: -- Name the first "Production Editor", give it a type of "Production Editor all" and choose your book from the Collection dropdown. -- Name the second "Copy Editors", give it a type of "Copy Editor update" and choose your book from the Collection dropdown. -- Name the third "Authors", give it a type of "Author update" and choose your book from the Collection dropdown. -<br> - You can now assign different users to different roles. If a user is a production editor, the user can then also edit user roles for all other users. +<br> + +You're good to go! diff --git a/app/.eslintrc b/app/.eslintrc deleted file mode 100644 index bb57155c6b64077529267cf5e0430a45dfae3e8b..0000000000000000000000000000000000000000 --- a/app/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -// Use this file as a starting point for your project's .eslintrc. -// Copy this file, and add rule overrides as needed. - -{ - "extends": ["standard", "standard-react"] -} diff --git a/app/app.js b/app/app.js index 310ce71ce1e2e32f1f3bedbbc7b0fdab85630db4..dc28d425fe02bdcb81eafbfaa16540dc12be9589 100644 --- a/app/app.js +++ b/app/app.js @@ -1,8 +1,8 @@ import React from 'react' import ReactDOM from 'react-dom' -import configureStore from 'pubsweet-frontend/src/store/configureStore' -import Root from 'pubsweet-frontend/src/components/Root' +import configureStore from 'pubsweet-client/src/store/configureStore' +import Root from 'pubsweet-client/src/components/Root' import { AppContainer } from 'react-hot-loader' import { browserHistory } from 'react-router' @@ -21,8 +21,8 @@ ReactDOM.render( ) if (module.hot) { - module.hot.accept('pubsweet-frontend/src/components/Root', () => { - const NextRoot = require('pubsweet-frontend/src/components/Root').default + module.hot.accept('pubsweet-client/src/components/Root', () => { + const NextRoot = require('pubsweet-client/src/components/Root').default ReactDOM.render( <AppContainer> diff --git a/app/components/BookBuilder/BookBuilder.jsx b/app/components/BookBuilder/BookBuilder.jsx index f4e9fad8c14da13100d4e4612282551a68f42056..b994575fac4b6ebaa718ccb3b968033c014f89bf 100644 --- a/app/components/BookBuilder/BookBuilder.jsx +++ b/app/components/BookBuilder/BookBuilder.jsx @@ -3,46 +3,49 @@ import React from 'react' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' -import * as Actions from 'pubsweet-frontend/src/actions' +// TODO -- clean up this import +import Actions from 'pubsweet-client/src/actions' import Division from './Division' -import Modal from '../utils/Modal' +import TeamManagerModal from './TeamManager/TeamManagerModal' import styles from './styles/bookBuilder.local.scss' -// import { fragmentsOfCollection } from 'pubsweet-core/app/helpers/Utils' - export class BookBuilder extends React.Component { constructor (props) { super(props) this._toggleTeamManager = this._toggleTeamManager.bind(this) - // this.enableActions = false this._getRoles = this._getRoles.bind(this) this._isProductionEditor = this._isProductionEditor.bind(this) - this._setProductionEditor = this._setProductionEditor.bind(this) + this.setProductionEditor = this.setProductionEditor.bind(this) this.state = { outerContainer: {}, - productionEditor: null, showTeamManager: false } } componentWillMount () { - const { getUsers, getTeams, getCollections, getFragments } = this.props.actions - - // console.log(this.props.actions) + const { + getCollections, + getFragments, + getTeams, + getUsers + } = this.props.actions getUsers().then( () => getTeams() ).then( () => { - this._setProductionEditor() return getCollections() } ).then( - // TODO: This will have to work for multiple collections - (result) => getFragments(result.collections[0]) + () => { + const { book } = this.props + + this.setProductionEditor() + getFragments(book) + } ) } @@ -53,19 +56,45 @@ export class BookBuilder extends React.Component { this.setState({ outerContainer: this.refs.outerContainer }) } - // temporary HACK to add production editor to collection - _setProductionEditor () { - const { book, teams, users } = this.props + setProductionEditor () { + const { actions, book, teams, users } = this.props + const { updateCollection } = actions const productionEditorsTeam = _.find(teams, function (t) { return t.teamType.name === 'Production Editor' && t.object.id === book.id }) + if (!productionEditorsTeam) return + const productionEditors = _.filter(users, function (u) { return _.includes(productionEditorsTeam.members, u.id) }) - this.setState({ productionEditor: productionEditors[0].username }) + let patch + + if (_.isEmpty(productionEditors)) { + // production editor is already set to null + if (book.productionEditor === null) return + + patch = { + id: book.id, + productionEditor: null + } + + return updateCollection(patch) + } + + const currentEditor = book.productionEditor + const foundEditor = productionEditors[0] || null + + if (currentEditor === foundEditor) return + + patch = { + id: book.id, + productionEditor: _.pick(foundEditor, ['id', 'username']) + } + + updateCollection(patch) } _toggleTeamManager () { @@ -110,9 +139,32 @@ export class BookBuilder extends React.Component { return pass } + renderTeamManagerModal () { + if (!this._isProductionEditor()) return null + + const { outerContainer, showTeamManager } = this.state + + if (!showTeamManager) return null + if (_.isEmpty(outerContainer)) return null + + const { actions, teams, users } = this.props + const { updateTeam } = actions + + return ( + <TeamManagerModal + container={outerContainer} + show={showTeamManager} + teams={teams} + toggle={this._toggleTeamManager} + updateTeam={updateTeam} + users={users} + /> + ) + } + render () { - const { book, chapters, teams, users } = this.props - const { createFragment, deleteFragment, ink, updateFragment, updateTeam } = this.props.actions + const { book, chapters } = this.props + const { createFragment, deleteFragment, ink, updateFragment } = this.props.actions const { outerContainer } = this.state const roles = this._getRoles() @@ -145,7 +197,8 @@ export class BookBuilder extends React.Component { ) } - const productionEditor = this.state.productionEditor || 'unassigned' + const productionEditor = _.get(book, 'productionEditor.username') || 'unassigned' + const teamManagerModal = this.renderTeamManagerModal() return ( <div className='bootstrap modal pubsweet-component pubsweet-component-scroll'> @@ -157,7 +210,7 @@ export class BookBuilder extends React.Component { <h1>{this.props.book.title}</h1> <div className={styles.productionEditorContainer}> - <span>Production Editor: {productionEditor} </span> + <span>Production Editor: { productionEditor } </span> {teamManagerButton} <div className={styles.separator} /> </div> @@ -208,32 +261,21 @@ export class BookBuilder extends React.Component { </div> </div> - <Modal - title='Editoria Team Manager' - action='EditoriaTeamManager' - show={this.state.showTeamManager} - toggle={this._toggleTeamManager} - container={outerContainer} - size='large' - teams={teams} - users={users} - updateTeam={updateTeam} - /> - + { teamManagerModal } </div> ) } } BookBuilder.propTypes = { + actions: React.PropTypes.object.isRequired, book: React.PropTypes.object.isRequired, chapters: React.PropTypes.array.isRequired, - actions: React.PropTypes.object.isRequired, - error: React.PropTypes.string, - // userRoles: React.PropTypes.array, + // error: React.PropTypes.string, teams: React.PropTypes.array, users: React.PropTypes.array, user: React.PropTypes.object + // userRoles: React.PropTypes.array, } function mapStateToProps (state, ownProps) { diff --git a/app/components/BookBuilder/BookList.jsx b/app/components/BookBuilder/BookList.jsx deleted file mode 100644 index 8e35d61961447603c26b0376a1fed2ab89971167..0000000000000000000000000000000000000000 --- a/app/components/BookBuilder/BookList.jsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react' -import { bindActionCreators } from 'redux' -import { connect } from 'react-redux' -import { Modal } from 'react-bootstrap' -import { LinkContainer } from 'react-router-bootstrap' -import * as Actions from 'pubsweet-frontend/src/actions' -import styles from './styles/bookList.local.scss' - -export class BookList extends React.Component { - constructor (props) { - super(props) - - this._toggleModal = this._toggleModal.bind(this) - - this.state = { - showModal: false - } - } - - _toggleModal () { - this.setState({ - showModal: !this.state.showModal - }) - } - - render () { - const { book } = this.props - const { showModal } = this.state - let bookTitle = book ? book.title : 'Fetching...' - let bookId = book ? book.id : '' - - return ( - <div className={styles.bookList + ' bootstrap'}> - <div className='container col-lg-offset-2 col-lg-8'> - - <div className='col-lg-12'> - <h1 className={styles.bookTitle}> - Books - <div className={styles.addBookBtn} onClick={this._toggleModal}> - <a>add book</a> - </div> - </h1> - </div> - - <div className='col-lg-12'> - <div className={styles.bookContainer}> - <h2>{bookTitle}</h2> - <LinkContainer to={`/books/${bookId}/book-builder`}> - <a href='#' - className={styles.editBook} - onClick={this._toggleModal} > - Edit - </a> - </LinkContainer> - </div> - - </div> - </div> - - <Modal - className='modal' - show={showModal} - onHide={this._toggleModal} - container={this} - > - - <Modal.Header> - <Modal.Title> - Create a new book - </Modal.Title> - </Modal.Header> - - <Modal.Body> - This feature is currently disabled. - </Modal.Body> - - <Modal.Footer> - <a className='modal-button modal-discard' - onClick={this._toggleModal}> - Close - </a> - </Modal.Footer> - - </Modal> - - </div> - ) - } -} - -BookList.propTypes = { - book: React.PropTypes.object -} - -function mapStateToProps (state) { - return { - book: state.collections[0] - } -} - -function mapDispatchToProps (dispatch) { - return { - actions: bindActionCreators(Actions, dispatch) - } -} - -export default connect( - mapStateToProps, - mapDispatchToProps -)(BookList) diff --git a/app/components/BookBuilder/Chapter.jsx b/app/components/BookBuilder/Chapter.jsx index 91d47a39496c65a98b2b9be818f6d930c9a5ca7d..982dc297a1130fdf1978461e1f821f2ae8c59c79 100644 --- a/app/components/BookBuilder/Chapter.jsx +++ b/app/components/BookBuilder/Chapter.jsx @@ -19,9 +19,9 @@ export class Chapter extends React.Component { } } - update (changedChapter) { + update (patch) { const { book, update } = this.props - update(book, changedChapter) + update(book, patch) } toggleUpload () { diff --git a/app/components/BookBuilder/Chapter/AlignmentBox.jsx b/app/components/BookBuilder/Chapter/AlignmentBox.jsx index 19d98f0a2eb75991d0425ab169bb0fcf56b98861..f67dc8575013c0b18ca38a286833947bd09e52bc 100644 --- a/app/components/BookBuilder/Chapter/AlignmentBox.jsx +++ b/app/components/BookBuilder/Chapter/AlignmentBox.jsx @@ -14,8 +14,14 @@ class AlignmentBox extends React.Component { if (!includes(['left', 'right'], position)) return - chapter.alignment[position] = !chapter.alignment[position] - update(chapter) + const patch = { + alignment: chapter.alignment, + id: chapter.id + } + + patch.alignment[position] = !chapter.alignment[position] + + update(patch) } render () { diff --git a/app/components/BookBuilder/Chapter/ChapterButtons.jsx b/app/components/BookBuilder/Chapter/ChapterButtons.jsx index 1baf83b451be9ffb94969c1c3bc4f7319eb1904f..efa0f27790c6dc5ac394e42afd019351d7673aeb 100644 --- a/app/components/BookBuilder/Chapter/ChapterButtons.jsx +++ b/app/components/BookBuilder/Chapter/ChapterButtons.jsx @@ -114,15 +114,19 @@ class ChapterButtons extends React.Component { const { showDeleteModal } = this.state const toggle = this.toggleDeleteModal - const deleteModal = ( - <DeleteModal - chapter={chapter} - container={modalContainer} - remove={remove} - show={showDeleteModal} - toggle={toggle} - /> - ) + let deleteModal = null + + if (showDeleteModal) { + deleteModal = ( + <DeleteModal + chapter={chapter} + container={modalContainer} + remove={remove} + show={showDeleteModal} + toggle={toggle} + /> + ) + } return ( <a id='bb-delete' onClick={toggle} > diff --git a/app/components/BookBuilder/Chapter/FirstRow.jsx b/app/components/BookBuilder/Chapter/FirstRow.jsx index cd65fb71fadcbbd19629f2c05bea24531719a1e0..10263639b390ed05a1e7c065ab5305f941d8c8c4 100644 --- a/app/components/BookBuilder/Chapter/FirstRow.jsx +++ b/app/components/BookBuilder/Chapter/FirstRow.jsx @@ -25,13 +25,22 @@ class ChapterFirstRow extends React.Component { onSaveRename (title) { const { chapter, update } = this.props - title = title.trim() - if (title.length === 0) return this.setState({ isRenameEmpty: true }) + + if (title.length === 0) { + return this.setState({ + isRenameEmpty: true + }) + } + this.setState({ isRenameEmpty: false }) - chapter.title = title - update(chapter) + const patch = { + id: chapter.id, + title: title + } + update(patch) + this.setState({ isRenamingTitle: false }) } diff --git a/app/components/BookBuilder/Chapter/ProgressItem.jsx b/app/components/BookBuilder/Chapter/ProgressItem.jsx index 6ec269ae7a405b60e51b4ecdc6335e194316935d..58de092a972090881fc53f694239bd9cded72212 100644 --- a/app/components/BookBuilder/Chapter/ProgressItem.jsx +++ b/app/components/BookBuilder/Chapter/ProgressItem.jsx @@ -48,8 +48,13 @@ export class ProgressItem extends React.Component { position += 1 // move up a level if (position >= len) position = 0 // or cycle back to the beginning - chapter.progress[type] = position - update(chapter) + const patch = { + id: chapter.id, + progress: chapter.progress + } + + patch.progress[type] = position + update(patch) this.setState({ showModal: false }) } @@ -57,7 +62,6 @@ export class ProgressItem extends React.Component { canChange () { const { type, roles, chapter } = this.props - console.log('can change', includes(roles, 'admin')) if (includes(roles, 'admin') || includes(roles, 'production-editor')) return true const isActive = (chapter.progress[type] === 1) diff --git a/app/components/BookBuilder/Chapter/SecondRow.jsx b/app/components/BookBuilder/Chapter/SecondRow.jsx index f07d399c5a19f8db4c34c71c235eb126c20c1bd2..5d479711ff9a96fa9939b3ace81c9b34107a6d46 100644 --- a/app/components/BookBuilder/Chapter/SecondRow.jsx +++ b/app/components/BookBuilder/Chapter/SecondRow.jsx @@ -20,6 +20,7 @@ class ChapterSecondRow extends React.Component { accept='.docx' chapter={chapter} convertFile={convertFile} + modalContainer={outerContainer} title=' ' toggleUpload={toggleUpload} type='file' diff --git a/app/components/BookBuilder/Chapter/UploadButton.jsx b/app/components/BookBuilder/Chapter/UploadButton.jsx index 3b233bee1fd04fd125b0a9c8b21e7ed7d72a2fe9..cdb280d4baaafb536e54f060a0ad2db309f849ee 100644 --- a/app/components/BookBuilder/Chapter/UploadButton.jsx +++ b/app/components/BookBuilder/Chapter/UploadButton.jsx @@ -1,28 +1,36 @@ import React from 'react' + +import UploadWarningModal from './UploadWarningModal' import styles from '../styles/bookBuilder.local.scss' export class UploadButton extends React.Component { constructor (props) { super(props) + this.handleFileUpload = this.handleFileUpload.bind(this) + this.onClick = this.onClick.bind(this) + this.toggleModal = this.toggleModal.bind(this) + + this.state = { + showModal: false + } } handleFileUpload (event) { event.preventDefault() const file = event.target.files[0] - const { - chapter, - convertFile, - toggleUpload, - update - } = this.props + const { chapter, convertFile, toggleUpload, update } = this.props toggleUpload() convertFile(file).then(response => { - chapter.source = response.converted - update(chapter) + const patch = { + id: chapter.id, + source: response.converted + } + + update(patch) toggleUpload() }).catch((error) => { console.error('INK error', error) @@ -30,10 +38,30 @@ export class UploadButton extends React.Component { }) } - render () { + toggleModal () { + this.setState({ + showModal: !this.state.showModal + }) + } + + onClick () { + if (!this.isLocked()) return + this.toggleModal() + } + + isLocked () { + const { chapter } = this.props + + if (chapter.lock === null) return false + return true + } + + renderInput () { + if (this.isLocked()) return null + const { accept, title, type } = this.props - const input = ( + return ( <input accept={accept} onChange={this.handleFileUpload} @@ -41,14 +69,47 @@ export class UploadButton extends React.Component { type={type} /> ) + } + + renderModal () { + if (!this.isLocked()) return null + + const { showModal } = this.state + const { chapter, modalContainer } = this.props + const type = chapter.subCategory + + return ( + <UploadWarningModal + container={modalContainer} + show={showModal} + toggle={this.toggleModal} + type={type} + /> + ) + } + + render () { + const input = this.renderInput() + const modal = this.renderModal() + + // TODO -- refactor with chapter buttons lock + let buttonStyle = {} + if (this.isLocked()) { + buttonStyle = { + 'opacity': '0.3' + } + } return ( <div className={styles.btnFile} id='bb-upload' + onClick={this.onClick} + style={buttonStyle} > Upload Word { input } + { modal } </div> ) } @@ -58,6 +119,7 @@ UploadButton.propTypes = { accept: React.PropTypes.string.isRequired, chapter: React.PropTypes.object.isRequired, convertFile: React.PropTypes.func.isRequired, + modalContainer: React.PropTypes.object.isRequired, title: React.PropTypes.string.isRequired, toggleUpload: React.PropTypes.func.isRequired, type: React.PropTypes.string.isRequired, diff --git a/app/components/BookBuilder/Chapter/UploadWarningModal.jsx b/app/components/BookBuilder/Chapter/UploadWarningModal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a8fe28fc90ec122a0049bd5601bf358598b2b487 --- /dev/null +++ b/app/components/BookBuilder/Chapter/UploadWarningModal.jsx @@ -0,0 +1,40 @@ +import React from 'react' + +import AbstractModal from '../../common/AbstractModal' + +class UploadWarningModal extends React.Component { + renderBody () { + const { type } = this.props + + return ( + <div> + You are not allowed to import contents while a { type } is being edited. + </div> + ) + } + + render () { + const { container, show, toggle } = this.props + const body = this.renderBody() + const title = 'Import not allowed' + + return ( + <AbstractModal + body={body} + container={container} + show={show} + title={title} + toggle={toggle} + /> + ) + } +} + +UploadWarningModal.propTypes = { + container: React.PropTypes.object.isRequired, + show: React.PropTypes.bool.isRequired, + toggle: React.PropTypes.func.isRequired, + type: React.PropTypes.string.isRequired +} + +export default UploadWarningModal diff --git a/app/components/BookBuilder/Division.jsx b/app/components/BookBuilder/Division.jsx index 91073891b185e6a8d1855596f681a7b46e6ed800..325c86b88e58a3adb46e464eeab30c6d766f9d65 100644 --- a/app/components/BookBuilder/Division.jsx +++ b/app/components/BookBuilder/Division.jsx @@ -61,13 +61,18 @@ export class Division extends React.Component { }) _.forEach(chaptersToModify, function (c) { - c.index -= 1 - update(book, c) + const patch = { + id: c.id, + index: (c.index - 1) + } + + update(book, patch) }) } // Reorder chapters _onMove (dragIndex, hoverIndex) { + // hovering over current position if (dragIndex === hoverIndex) { return } const { book, chapters, update } = this.props @@ -75,32 +80,52 @@ export class Division extends React.Component { let toUpdate = [] + // dragging upwards if (dragIndex > hoverIndex) { - const toModify = _.filter(chapters, function (c) { + // find the chapters that changed place + const toModify = _.filter(chapters, c => { return c.index >= hoverIndex && c.index < dragIndex }) - _.forEach(toModify, function (c) { - c.index += 1 - // update(book, c) + + // build the patches for the chapters' updates + const patches = _.map(toModify, chapter => { + return { + id: chapter.id, + index: (chapter.index + 1) + } }) - toUpdate = _.union(toUpdate, toModify) - } else if (dragIndex < hoverIndex) { + + toUpdate = _.union(toUpdate, patches) + } + + // dragging downwards + if (dragIndex < hoverIndex) { + // TODO -- refactor? + // do the same as above const toModify = _.filter(chapters, function (c) { return c.index <= hoverIndex && c.index > dragIndex }) - _.forEach(toModify, function (c) { - c.index -= 1 - // update(book, c) + + const patches = _.map(toModify, chapter => { + return { + id: chapter.id, + index: (chapter.index - 1) + } }) - toUpdate = _.union(toUpdate, toModify) + + toUpdate = _.union(toUpdate, patches) } - dragChapter.index = hoverIndex - // update(book, dragChapter) - toUpdate.push(dragChapter) + // add the dragged chapter to the list of patches that are needed + const draggedPatch = { + id: dragChapter.id, + index: hoverIndex + } + toUpdate.push(draggedPatch) - _.forEach(toUpdate, function (chapter) { - update(book, chapter) + // perform all the updates + _.forEach(toUpdate, patch => { + update(book, patch) }) } @@ -117,7 +142,7 @@ export class Division extends React.Component { chapter={c} id={c.id} ink={ink} - key={c.index} + key={c.id} move={_onMove} no={i} outerContainer={outerContainer} diff --git a/app/components/BookBuilder/TeamManager/AddMember.jsx b/app/components/BookBuilder/TeamManager/AddMember.jsx index e32a6cd0c0322fdf6aab9c2b60a6d799ac153063..8973a64e4893f4f21e491b8029c535abf1fbe8bc 100644 --- a/app/components/BookBuilder/TeamManager/AddMember.jsx +++ b/app/components/BookBuilder/TeamManager/AddMember.jsx @@ -1,5 +1,5 @@ -import React from 'react' import { find, union } from 'lodash' +import React from 'react' import TextInput from '../../utils/TextInput' diff --git a/app/components/BookBuilder/TeamManager/Group.jsx b/app/components/BookBuilder/TeamManager/Group.jsx index 4e427bef68f6b6c9be47a7cd26657bd2b657d064..157bc21953df37c509c9cba292e1a92d47c192d5 100644 --- a/app/components/BookBuilder/TeamManager/Group.jsx +++ b/app/components/BookBuilder/TeamManager/Group.jsx @@ -1,5 +1,5 @@ -import React from 'react' import _ from 'lodash' +import React from 'react' import GroupHeader from './GroupHeader' import AddMember from './AddMember' diff --git a/app/components/BookBuilder/TeamManager/Member.jsx b/app/components/BookBuilder/TeamManager/Member.jsx index 8111d402b77d5e836607ac7dff83272fc30de51a..2ca97d2a5888861a68e80bebee0ac9121dc45bab 100644 --- a/app/components/BookBuilder/TeamManager/Member.jsx +++ b/app/components/BookBuilder/TeamManager/Member.jsx @@ -1,5 +1,5 @@ -import React from 'react' import { without } from 'lodash' +import React from 'react' import styles from '../styles/teamManager.local.scss' diff --git a/app/components/BookBuilder/TeamManager/MemberList.jsx b/app/components/BookBuilder/TeamManager/MemberList.jsx index 32580d3022676c046a162faf56fcc9e95371d4ea..13a1ea2feccfdcdddf1fbacd591da2714e0f2fe4 100644 --- a/app/components/BookBuilder/TeamManager/MemberList.jsx +++ b/app/components/BookBuilder/TeamManager/MemberList.jsx @@ -1,4 +1,5 @@ import React from 'react' + import Member from './Member' import styles from '../styles/teamManager.local.scss' diff --git a/app/components/BookBuilder/TeamManager/TeamManagerModal.jsx b/app/components/BookBuilder/TeamManager/TeamManagerModal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6d4d529e0a54a662b89b0e75d27b77b8c577d684 --- /dev/null +++ b/app/components/BookBuilder/TeamManager/TeamManagerModal.jsx @@ -0,0 +1,46 @@ +import React from 'react' + +import AbstractModal from '../../common/AbstractModal' +import TeamManager from './TeamManager' + +class TeamManagerModal extends React.Component { + renderBody () { + const { teams, users, updateTeam } = this.props + + return ( + <TeamManager + teams={teams} + users={users} + updateTeam={updateTeam} + /> + ) + } + + render () { + const { container, show, toggle } = this.props + const body = this.renderBody() + + return ( + <AbstractModal + body={body} + cancelText='Close' + container={container} + show={show} + size='large' + title='Editoria Team Manager' + toggle={toggle} + /> + ) + } +} + +TeamManagerModal.propTypes = { + container: React.PropTypes.object.isRequired, + show: React.PropTypes.bool.isRequired, + teams: React.PropTypes.array.isRequired, + toggle: React.PropTypes.func.isRequired, + users: React.PropTypes.array, + updateTeam: React.PropTypes.func.isRequired +} + +export default TeamManagerModal diff --git a/app/components/BookBuilder/styles/bookBuilder.local.scss b/app/components/BookBuilder/styles/bookBuilder.local.scss index 0db957f9eda107fb2d028eaba16d2a3c3a735d5a..465dbb4a81f729fce58dc20550dba31dc2cb7ae5 100644 --- a/app/components/BookBuilder/styles/bookBuilder.local.scss +++ b/app/components/BookBuilder/styles/bookBuilder.local.scss @@ -338,39 +338,42 @@ $white: #fff; border-bottom:2px solid; border-color:$light-grey;; } + .secondLineContainer { padding-top:1%; + .btnFile { - position: relative; - top:3px; + background-color: $light-grey; + color: #fff; + cursor: pointer; overflow: hidden; - background-color:$light-grey; - cursor:pointer; + position: relative; text-align: center; - color: #fff; + top: 3px; width: 75%; - // padding-top: 4px; - // padding-bottom: 4px; } + .btnFile input[type=file] { - position: absolute; - top: 0; - right: 0; - left: 0; - min-width: 100%; - min-height: 100%; + border: none !important; + color: #fff; cursor: pointer; + display: block; filter: alpha(opacity=0); + left: 0; + min-height: 100%; + min-width: 100%; opacity: 0; outline: none; - color:#fff; padding: 10px; - border:none!important; - display: block; + position: absolute; + right: 0; + top: 0; } + .btnFile:hover input[type=file] { font-size: 260px; } + .secondActions { float:left; padding-left: 2%; diff --git a/app/components/Dashboard/AddBookModal.jsx b/app/components/Dashboard/AddBookModal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a89e55b2114243f51ab92a749bbafa096344def9 --- /dev/null +++ b/app/components/Dashboard/AddBookModal.jsx @@ -0,0 +1,113 @@ +import React from 'react' + +import AbstractModal from '../common/AbstractModal' +import styles from './dashboard.local.scss' + +class AddBookModal extends React.Component { + constructor (props) { + super(props) + + this.handleKeyOnInput = this.handleKeyOnInput.bind(this) + this.onInputChange = this.onInputChange.bind(this) + this.onCreate = this.onCreate.bind(this) + + this.state = { error: false } + } + + componentDidUpdate () { + const { show } = this.props + if (show) this.inputRef.focus() + } + + handleKeyOnInput (event) { + if (event.charCode !== 13) return + this.onCreate() + } + + onCreate () { + const { create, toggle } = this.props + + const input = this.inputRef + const newTitle = input.value.trim() + + if (newTitle.length === 0) { + return this.setState({ + error: true + }) + } + + create(newTitle) + toggle() + } + + onInputChange () { + const { error } = this.state + if (!error) return + this.setState({ error: false }) + } + + renderBody () { + const error = this.renderError() + const message = ( + <div style={{'paddingBottom': 4}} > + Enter the title of the new book <br /> + </div> + ) + + return ( + <div> + { message } + { error } + + <input + className={styles['add-book-input']} + name='title' + onChange={this.onInputChange} + onKeyPress={this.handleKeyOnInput} + placeholder='eg. My new title' + ref={(item) => { this.inputRef = item }} + type='text' + /> + </div> + ) + } + + renderError () { + const { error } = this.state + + const el = ( + <div className='error' > + New book title cannot be empty + </div> + ) + + const res = error ? el : null + return res + } + + render () { + const { container, show, toggle } = this.props + const body = this.renderBody() + + return ( + <AbstractModal + body={body} + container={container} + show={show} + successAction={this.onCreate} + successText='Add' + title='Add a new book' + toggle={toggle} + /> + ) + } +} + +AddBookModal.propTypes = { + create: React.PropTypes.func.isRequired, + container: React.PropTypes.object.isRequired, + show: React.PropTypes.bool.isRequired, + toggle: React.PropTypes.func.isRequired +} + +export default AddBookModal diff --git a/app/components/Dashboard/Book.jsx b/app/components/Dashboard/Book.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5ee0de25b1ef2b24b7cf65d594c67382cd9024fa --- /dev/null +++ b/app/components/Dashboard/Book.jsx @@ -0,0 +1,198 @@ +import React from 'react' +import { Link } from 'react-router' + +import RemoveBookModal from './RemoveBookModal' +import styles from './dashboard.local.scss' + +// TODO -- Book and Chapter should both extend a common component +class Book extends React.Component { + constructor (props) { + super(props) + + this.handleKeyOnInput = this.handleKeyOnInput.bind(this) + this.onClickRename = this.onClickRename.bind(this) + this.onClickSave = this.onClickSave.bind(this) + this.removeBook = this.removeBook.bind(this) + this.renameBook = this.renameBook.bind(this) + this.toggleModal = this.toggleModal.bind(this) + + this.state = { + isRenaming: false, + showModal: false + } + } + + componentDidUpdate () { + const { isRenaming } = this.state + if (isRenaming) this.renameTitle.focus() + } + + toggleModal () { + this.setState({ + showModal: !this.state.showModal + }) + } + + handleKeyOnInput (event) { + if (event.charCode !== 13) return + this.renameBook() + } + + onClickSave () { + this.renameBook() + } + + renameBook () { + const { book, edit } = this.props + + const patch = { + id: book.id, + title: this.renameTitle.value + } + + edit(patch) + this.setState({ + isRenaming: false + }) + } + + onClickRename () { + this.setState({ + isRenaming: true + }) + } + + removeBook () { + const { book, remove } = this.props + remove(book) + } + + renderTitle () { + const { book } = this.props + const { isRenaming } = this.state + + if (isRenaming) { + return ( + <input + defaultValue={book.title} + name='renameTitle' + onKeyPress={this.handleKeyOnInput} + ref={(el) => { this.renameTitle = el }} + /> + ) + } + + return ( + <h2 onDoubleClick={this.onClickRename} > + { book.title } + </h2> + ) + } + + // TODO -- edit, rename and remove should be reusable components + renderEdit () { + const { book } = this.props + + return ( + <Link + className={styles.editBook} + to={`/books/${book.id}/book-builder`} + > + Edit + </Link> + ) + } + + renderRename () { + const { isRenaming } = this.state + + if (isRenaming) { + return ( + <a + className={styles.editBook} + href='#' + onClick={this.onClickSave} + > + Save + </a> + ) + } + + return ( + <a + className={styles.editBook} + href='#' + onClick={this.onClickRename} + > + Rename + </a> + ) + } + + renderRemove () { + return ( + <a + className={styles.editBook} + href='#' + onClick={this.toggleModal} + > + Remove + </a> + ) + } + + renderButtons () { + const rename = this.renderRename() + const edit = this.renderEdit() + const remove = this.renderRemove() + + return ( + <div className={styles.bookActions}> + { rename } + { edit } + { remove } + </div> + ) + } + + renderRemoveModal () { + const { book, container } = this.props + const { showModal } = this.state + if (!showModal) return null + + return ( + <RemoveBookModal + book={book} + container={container} + remove={this.removeBook} + show={showModal} + toggle={this.toggleModal} + /> + ) + } + + render () { + const { book } = this.props + + const title = this.renderTitle(book) + const buttons = this.renderButtons(book) + const removeModal = this.renderRemoveModal() + + return ( + <div className={styles.bookContainer}> + { title } + { buttons } + { removeModal } + </div> + ) + } +} + +Book.propTypes = { + book: React.PropTypes.object.isRequired, + container: React.PropTypes.object.isRequired, + edit: React.PropTypes.func.isRequired, + remove: React.PropTypes.func.isRequired +} + +export default Book diff --git a/app/components/Dashboard/BookList.jsx b/app/components/Dashboard/BookList.jsx new file mode 100644 index 0000000000000000000000000000000000000000..84230dd235a544ab8d06064a398cad4b1becd769 --- /dev/null +++ b/app/components/Dashboard/BookList.jsx @@ -0,0 +1,55 @@ +import { isEmpty, map, reverse, sortBy } from 'lodash' +import React from 'react' + +import Book from './Book' +import styles from './dashboard.local.scss' + +class BookList extends React.Component { + renderBookList () { + const { books, container, edit, remove } = this.props + if (!books) return 'Fetching...' + + if (isEmpty(books)) { + return ( + <div className={styles['booklist-empty']}> + There are no books to display. + </div> + ) + } + + const items = reverse(sortBy(books, 'created')) + + const bookComponents = map(items, book => { + return ( + <Book + book={book} + container={container} + edit={edit} + key={book.id} + remove={remove} + /> + ) + }) + + return bookComponents + } + + render () { + const bookList = this.renderBookList() + + return ( + <div className='col-lg-12'> + { bookList } + </div> + ) + } +} + +BookList.propTypes = { + books: React.PropTypes.array.isRequired, + container: React.PropTypes.object.isRequired, + edit: React.PropTypes.func.isRequired, + remove: React.PropTypes.func.isRequired +} + +export default BookList diff --git a/app/components/Dashboard/Dashboard.jsx b/app/components/Dashboard/Dashboard.jsx new file mode 100644 index 0000000000000000000000000000000000000000..563df6ab8adadc212b303424604473d56e324bed --- /dev/null +++ b/app/components/Dashboard/Dashboard.jsx @@ -0,0 +1,213 @@ +import { each, filter, isEmpty } from 'lodash' +// TODO -- clean up this import +import Actions from 'pubsweet-client/src/actions' +import React from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import AddBookModal from './AddBookModal' +import BookList from './BookList' +import DashboardHeader from './DashboardHeader' +import { teamTypes } from '../utils/config' +import styles from './dashboard.local.scss' + +export class Dashboard extends React.Component { + constructor (props) { + super(props) + + this.createBook = this.createBook.bind(this) + this.createTeamsForBook = this.createTeamsForBook.bind(this) + this.editBook = this.editBook.bind(this) + this.findBooksWithNoTeams = this.findBooksWithNoTeams.bind(this) + this.removeBook = this.removeBook.bind(this) + this.removeTeamsForBook = this.removeTeamsForBook.bind(this) + this.toggleModal = this.toggleModal.bind(this) + + this.state = { + showModal: false + } + } + + /* + Get books and teams. + Make sure all books have teams associated with them. + */ + componentWillMount () { + const { actions } = this.props + const { getCollections, getTeams } = actions + + getCollections().then( + () => getTeams() + ).then( + () => this.findBooksWithNoTeams() + ) + } + + /* + Toggle showing 'add book' modal + */ + toggleModal () { + this.setState({ + showModal: !this.state.showModal + }) + } + + /* + Create a new book with the given title. + Once you have the new book's db id, make the teams for it as well. + */ + createBook (newTitle) { + const { createCollection } = this.props.actions + + const book = { + title: newTitle || 'Untitled' + } + + createCollection(book).then(res => { + const createdBook = res.collection + this.createTeamsForBook(createdBook) + }) + } + + /* + Edit a book's properties. + */ + editBook (patch) { + const { updateCollection } = this.props.actions + updateCollection(patch) + } + + /* + Remove the given book. + Also remove all teams associated with it. + */ + removeBook (book) { + const { deleteCollection } = this.props.actions + + deleteCollection(book).then(res => { + this.removeTeamsForBook(book) + }) + } + + /* + Find all books that have no teams associated with them. + If one is found, make the teams for it. + This will most likely only happen once: + # The first time the app is run, on the collection created by + # the command 'pubsweet setupdb'. + */ + // TODO -- refactor so that less operations run most of the time + findBooksWithNoTeams () { + const { books, teams } = this.props + + each(books, book => { + const teamsForBook = filter(teams, t => { + return t.object.id === book.id + }) + + if (isEmpty(teamsForBook)) { + this.createTeamsForBook(book) + } + }) + } + + /* + Create the teams found in the config file for the given book. + This should run either when a new book is created, + or when a book with no teams associated with it is found. + */ + createTeamsForBook (book) { + const { createTeam } = this.props.actions + + each(teamTypes, teamType => { + // TODO -- Review the idea that the name needs to be plural for some teams + const name = (teamType.name === 'Production Editor') + ? teamType.name + : teamType.name + 's' + + const newTeam = { + members: [], + name: name, + object: { + id: book.id, + type: 'collection' + }, + teamType: teamType + } + + createTeam(newTeam) + }) + } + + /* + Delete all teams associated with the given book. + This should only run after a book is deleted. + */ + removeTeamsForBook (book) { + const { actions, teams } = this.props + const { deleteTeam } = actions + + const teamsToDelete = filter(teams, team => { + return team.object.id === book.id + }) + + each(teamsToDelete, team => { + deleteTeam(team) + }) + } + + render () { + const { books } = this.props + const { showModal } = this.state + + const className = styles.bookList + + ' bootstrap pubsweet-component pubsweet-component-scroll' + + return ( + <div className={className}> + <div className='container col-lg-offset-2 col-lg-8'> + + <DashboardHeader toggle={this.toggleModal} /> + + <BookList + books={books} + container={this} + edit={this.editBook} + remove={this.removeBook} + /> + </div> + + <AddBookModal + container={this} + create={this.createBook} + show={showModal} + toggle={this.toggleModal} + /> + </div> + ) + } +} + +Dashboard.propTypes = { + actions: React.PropTypes.object.isRequired, + books: React.PropTypes.arrayOf(React.PropTypes.object), // TODO -- ?? + teams: React.PropTypes.arrayOf(React.PropTypes.object) +} + +function mapStateToProps (state, { params }) { + return { + books: state.collections, + teams: state.teams + } +} + +function mapDispatchToProps (dispatch) { + return { + actions: bindActionCreators(Actions, dispatch) + } +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Dashboard) diff --git a/app/components/Dashboard/DashboardHeader.jsx b/app/components/Dashboard/DashboardHeader.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0ae63e3fed9906e546a5c847fe8ecbc3fbe1041e --- /dev/null +++ b/app/components/Dashboard/DashboardHeader.jsx @@ -0,0 +1,31 @@ +import React from 'react' + +import styles from './dashboard.local.scss' + +class DashboardHeader extends React.Component { + render () { + const { toggle } = this.props + + return ( + <div className='col-lg-12'> + <h1 className={styles.bookTitle}> + Books + + <div + className={styles.addBookBtn} + onClick={toggle} + > + <a>add book</a> + </div> + + </h1> + </div> + ) + } +} + +DashboardHeader.propTypes = { + toggle: React.PropTypes.func.isRequired +} + +export default DashboardHeader diff --git a/app/components/Dashboard/RemoveBookModal.jsx b/app/components/Dashboard/RemoveBookModal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a1f1e1e6c739870d88ca80f19248fab821361e7a --- /dev/null +++ b/app/components/Dashboard/RemoveBookModal.jsx @@ -0,0 +1,46 @@ +import React from 'react' + +import AbstractModal from '../common/AbstractModal' + +class RemoveBookModal extends React.Component { + renderBody () { + const { book } = this.props + + return ( + <span> + Are you sure you want to permanently delete { book.title }? + </span> + ) + } + + render () { + const { container, remove, show, toggle } = this.props + + const title = 'Delete Book' + const successText = 'Delete' + + const body = this.renderBody() + + return ( + <AbstractModal + body={body} + container={container} + show={show} + successAction={remove} + successText={successText} + title={title} + toggle={toggle} + /> + ) + } +} + +RemoveBookModal.propTypes = { + book: React.PropTypes.object.isRequired, + container: React.PropTypes.object.isRequired, + remove: React.PropTypes.func.isRequired, + show: React.PropTypes.bool.isRequired, + toggle: React.PropTypes.func.isRequired +} + +export default RemoveBookModal diff --git a/app/components/BookBuilder/styles/bookList.local.scss b/app/components/Dashboard/dashboard.local.scss similarity index 57% rename from app/components/BookBuilder/styles/bookList.local.scss rename to app/components/Dashboard/dashboard.local.scss index 656213a3aa4427f13e44a7acfc626e110cb4e75e..3a7a399945a8923f526b6be23f197ea3b587f79a 100644 --- a/app/components/BookBuilder/styles/bookList.local.scss +++ b/app/components/Dashboard/dashboard.local.scss @@ -1,20 +1,28 @@ $primary-color: #515253; +$black: #000; +$dark-grey: #fff; +$grey: #808080; +$maroon: #502424; +$medium-grey: #838587; + .bookList { .bookTitle { border-bottom: .1em solid; position: relative; } - h1, .h1 { - font-weight: 500; - font-style: italic; + h1, + .h1 { font-size: 48px; + font-style: italic; + font-weight: 500; line-height: 56px; margin: 0; } - h2, .h2 { + h2, + .h2 { color: $primary-color; font-size: 32px; font-style: italic; @@ -24,9 +32,9 @@ $primary-color: #515253; } .addBookBtn { - background-color: grey; - border: 1px solid grey; - color: #fff; + background-color: $grey; + border: 1px solid $grey; + color: $dark-grey; cursor: pointer; float: right; font-size: 16px; @@ -36,40 +44,65 @@ $primary-color: #515253; text-align: center; &:hover { - border: 1px solid #000; + border: 1px solid $black; } + a { - color: #fff; - text-decoration: none; - position: relative; bottom: 14px; + color: $dark-grey; + position: relative; + text-decoration: none; + &:hover { + cursor: pointer; text-decoration: none; - cursor:pointer; } } } .bookContainer { - border-bottom: 1px solid #838587; + border-bottom: 1px solid $medium-grey; margin-top: 3em; padding-bottom: 0; - padding-left: 2em; + padding-left: 1em; padding-top: 0; + position: relative; + } + + .bookActions { + bottom: 0; + position: absolute; + right: 13px; } .editBook { - color: #838587; - text-decoration: none; + color: $medium-grey; + display: inline-block; font-style: italic; font-weight: 500; - right: 13px; - display: block; - bottom: 0; - position: absolute; + margin-left: 13px; + text-decoration: none; + &:hover { - color:#502424; + color: $maroon; + text-decoration: none; + } + + &:link { + color: $medium-grey; text-decoration: none; } } + + .booklist-empty { + padding-top: 15px; + } + + .add-book-input { + width: 400px; + + &:placeholder-shown { + font-size: 13px; + } + } } diff --git a/app/components/Navigation/Navigation.jsx b/app/components/Navigation/Navigation.jsx index 673ecb095546fcd633d8ddf4174572276ddb8a0e..013e3aad55b467238e8e965e874fc5e6da0fab70 100644 --- a/app/components/Navigation/Navigation.jsx +++ b/app/components/Navigation/Navigation.jsx @@ -3,7 +3,7 @@ import { browserHistory } from 'react-router' import { LinkContainer } from 'react-router-bootstrap' import { Navbar, Nav, NavItem, NavbarBrand } from 'react-bootstrap' -import Authorize from 'pubsweet-frontend/src/helpers/Authorize' +import Authorize from 'pubsweet-client/src/helpers/Authorize' import NavbarUser from 'pubsweet-component-navigation/NavbarUser' export default class Navigation extends React.Component { diff --git a/app/components/SimpleEditor/ContainerEditor.js b/app/components/SimpleEditor/ContainerEditor.js index e124656032bf8d768371d3a6be4e4443bcfe84d5..ad3241bda9012258f818ad6c8c47554a0fb0bcd6 100644 --- a/app/components/SimpleEditor/ContainerEditor.js +++ b/app/components/SimpleEditor/ContainerEditor.js @@ -45,6 +45,7 @@ class ContainerEditor extends SubstanceContainerEditor { this.addTargetToLinks() } + // TODO -- this.props.history is deprecated and gives a warning if (this.props.history) { this.props.history.listenBefore((location, callback) => { const commandStates = this.getCommandStates() diff --git a/app/components/SimpleEditor/SimpleEditor.jsx b/app/components/SimpleEditor/SimpleEditor.jsx index dd008f79880634143663094f6a6fb5c041258aa8..3492ac3b9d8db4b71a19b5b44aef8b5918663489 100644 --- a/app/components/SimpleEditor/SimpleEditor.jsx +++ b/app/components/SimpleEditor/SimpleEditor.jsx @@ -62,8 +62,13 @@ export default class SimpleEditor extends React.Component { updateTrackChangesStatus () { const { fragment, update } = this.props - fragment.trackChanges = !fragment.trackChanges - update(fragment) + + const patch = { + id: fragment.id, + trackChanges: !fragment.trackChanges + } + + update(patch) } save (source, changes, callback) { @@ -212,8 +217,8 @@ export default class SimpleEditor extends React.Component { } SimpleEditor.propTypes = { - book: React.PropTypes.object.isRequired, - canEdit: React.PropTypes.bool, // needed? + book: React.PropTypes.object, + // canEdit: React.PropTypes.bool, // needed? fragment: React.PropTypes.object, history: React.PropTypes.object.isRequired, onSave: React.PropTypes.func.isRequired, diff --git a/app/components/SimpleEditor/SimpleEditorWrapper.jsx b/app/components/SimpleEditor/SimpleEditorWrapper.jsx index cba0a6169c6b7c29dc6c00639491e344e3d3e12d..180008ea2620a14443f48c2ba418cd4bec363881 100644 --- a/app/components/SimpleEditor/SimpleEditorWrapper.jsx +++ b/app/components/SimpleEditor/SimpleEditorWrapper.jsx @@ -3,7 +3,7 @@ import React from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' -import * as Actions from 'pubsweet-frontend/src/actions' +import Actions from 'pubsweet-client/src/actions' import SimpleEditor from './SimpleEditor' @@ -19,8 +19,10 @@ export class SimpleEditorWrapper extends React.Component { componentWillMount () { const { getCollections, getFragments } = this.props.actions - getCollections().then(result => { - getFragments(result.collections[0]) + // TODO: might not need to fetch all the collections? + getCollections().then(() => { + const { book } = this.props + getFragments(book) }) const { user } = this.props @@ -71,26 +73,31 @@ export class SimpleEditorWrapper extends React.Component { save (source, callback) { const { fragment } = this.props - fragment.source = source - return this.update(fragment) + const patch = { + id: fragment.id, + source: source + } + + return this.update(patch) } - update (newChapter) { + update (patch) { const { book } = this.props const { updateFragment } = this.props.actions - return updateFragment(book, newChapter) + + return updateFragment(book, patch) } } // TODO -- review required props SimpleEditorWrapper.propTypes = { actions: React.PropTypes.object.isRequired, - book: React.PropTypes.object.isRequired, + book: React.PropTypes.object, fragment: React.PropTypes.object, history: React.PropTypes.object.isRequired, - user: React.PropTypes.object.isRequired, - update: React.PropTypes.func + user: React.PropTypes.object.isRequired + // update: React.PropTypes.func } const mapStateToProps = (state, ownProps) => { diff --git a/app/components/SimpleEditor/elements/track_change/TrackChangeComponent.js b/app/components/SimpleEditor/elements/track_change/TrackChangeComponent.js index 6a0d7224e23824273116e564d6faf57ef0ca0709..2abef42b437266a6e8acc4b781ebfc5e259b2bb4 100644 --- a/app/components/SimpleEditor/elements/track_change/TrackChangeComponent.js +++ b/app/components/SimpleEditor/elements/track_change/TrackChangeComponent.js @@ -3,7 +3,8 @@ import { AnnotationComponent } from 'substance' class TrackChangeComponent extends AnnotationComponent { didMount () { - this.context.editorSession.onUpdate('document', this.onTrackChangesUpdated, this) + const { editorSession } = this.context + editorSession.onUpdate('document', this.onTrackChangesUpdated, this) } render ($$) { diff --git a/app/components/common/AbstractModal.jsx b/app/components/common/AbstractModal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0efa66c387f537c33bbe78b256f7df024161cd7e --- /dev/null +++ b/app/components/common/AbstractModal.jsx @@ -0,0 +1,104 @@ +import React from 'react' +import { Modal } from 'react-bootstrap' + +export class AbstractModal extends React.Component { + constructor (props) { + super(props) + this.performAction = this.performAction.bind(this) + } + + performAction () { + const { successAction } = this.props + successAction() + } + + renderHeader () { + const { title } = this.props + + return ( + <Modal.Header> + <Modal.Title> + { title } + </Modal.Title> + </Modal.Header> + ) + } + + renderBody () { + const { body } = this.props + + return ( + <Modal.Body> + { body } + </Modal.Body> + ) + } + + renderFooter () { + const { cancelText, successAction, successText, toggle } = this.props + + const successButton = ( + <a + className='modal-button bb-modal-act' + onClick={this.performAction} + > + { successText } + </a> + ) + + const success = successAction ? successButton : null + + return ( + <Modal.Footer> + <div className='modal-buttons-container'> + + <a className='modal-button modal-discard bb-modal-cancel' + onClick={toggle}> + { cancelText || 'Cancel' } + </a> + + { success } + + </div> + </Modal.Footer> + ) + } + + render () { + const { container, size, show, toggle } = this.props + + const header = this.renderHeader() + const body = this.renderBody() + const footer = this.renderFooter() + + return ( + <Modal + bsSize={size || null} + className='modal' + container={container} + onHide={toggle} + show={show} + > + + { header } + { body } + { footer } + + </Modal> + ) + } +} + +AbstractModal.propTypes = { + body: React.PropTypes.object, + cancelText: React.PropTypes.string, + container: React.PropTypes.object.isRequired, + show: React.PropTypes.bool.isRequired, + size: React.PropTypes.string, + successAction: React.PropTypes.func, + successText: React.PropTypes.string, + title: React.PropTypes.string.isRequired, + toggle: React.PropTypes.func.isRequired +} + +export default AbstractModal diff --git a/app/components/utils/Modal.jsx b/app/components/utils/Modal.jsx index 82162fe3c2bb771fded56df3b5731eca2586db68..e69251f194d78c254b3865176b7296c72c5d32d9 100644 --- a/app/components/utils/Modal.jsx +++ b/app/components/utils/Modal.jsx @@ -1,3 +1,5 @@ +// TODO -- deprecate this in favor of abstract modal + import React from 'react' import { Modal } from 'react-bootstrap' import TeamManager from '../BookBuilder/TeamManager/TeamManager' diff --git a/app/components/utils/config.js b/app/components/utils/config.js index 59ce4b32dfc1034030075c5896532d7a79926924..5da4c9929dd36d3aae31275b44aad93e8e96eeac 100644 --- a/app/components/utils/config.js +++ b/app/components/utils/config.js @@ -1,3 +1,6 @@ +/* global CONFIG */ +const teamTypes = CONFIG.authsome.teams + const chapter = { dropdownValues: { front: [ @@ -23,4 +26,4 @@ const chapter = { } } -export { chapter } +export { chapter, teamTypes } diff --git a/app/routes.jsx b/app/routes.jsx index 2f2b463f197b0b904b5d3c582f4e8e1fe28e06f1..48cecfd80c93567b11b2b93908affeadfb7ffe1e 100644 --- a/app/routes.jsx +++ b/app/routes.jsx @@ -1,7 +1,7 @@ import React from 'react' import { Redirect, Route } from 'react-router' -import { requireAuthentication } from 'pubsweet-frontend/src/components/AuthenticatedComponent' +import { requireAuthentication } from 'pubsweet-client/src/components/AuthenticatedComponent' // Manage import Manage from 'pubsweet-component-manage/Manage' @@ -11,13 +11,15 @@ import Blog from 'pubsweet-component-blog/Blog' // Editoria import BookBuilder from './components/BookBuilder/BookBuilder' -import BookList from './components/BookBuilder/BookList' +import Dashboard from './components/Dashboard/Dashboard' import SimpleEditorWrapper from './components/SimpleEditor/SimpleEditorWrapper' // Authentication import Login from 'pubsweet-component-login/Login' import Signup from 'pubsweet-component-signup/Signup' +// FIXME: this shouldn't be using the collection as the object + const AuthenticatedManage = requireAuthentication( Manage, 'create', (state) => state.collections[0] ) @@ -36,7 +38,7 @@ export default ( <Redirect from='/manage/posts' to='books' /> <Route path='/' component={AuthenticatedManage}> - <Route path='books' component={BookList} /> + <Route path='books' component={Dashboard} /> <Route path='blog' component={Blog} /> <Route path='books/:id/book-builder' component={BookBuilder} /> <Route path='books/:bookId/fragments/:fragmentId' component={SimpleEditorWrapper} /> diff --git a/config/dev.js b/config/dev.js index 09589eb13e5d7184649f912af5b74b7ae69d0508..1f19acc0205273a13a1bbf3a0f4340b6cea9b10e 100644 --- a/config/dev.js +++ b/config/dev.js @@ -10,15 +10,16 @@ module.exports = { pubsweet: { components: universal.components }, - 'pubsweet-backend': { + 'pubsweet-client': { + theme: universal.theme, + routes: 'app/routes.jsx', + navigation: 'app/components/Navigation/Navigation.jsx' + }, + 'pubsweet-component-ink-backend': universal.inkBackend, + 'pubsweet-server': { dbPath: path.join(__dirname, '..', 'api', 'db'), secret: process.env.PUBSWEET_SECRET, API_ENDPOINT: '/api' }, - 'pubsweet-component-ink-backend': universal.inkBackend, - 'pubsweet-frontend': { - theme: universal.theme, - routes: 'app/routes.jsx', - navigation: 'app/components/Navigation/Navigation.jsx' - } + 'validations': universal.validations } diff --git a/config/production.js b/config/production.js index 0d1a71d53bb45803fe8492eadef21fa5698a2883..b70b54bd9794f7a3390402d557ae638d59bac5de 100644 --- a/config/production.js +++ b/config/production.js @@ -10,15 +10,16 @@ module.exports = { pubsweet: { components: universal.components }, - 'pubsweet-backend': { + 'pubsweet-client': { + theme: universal.theme, + routes: 'app/routes.jsx', + navigation: 'app/components/Navigation/Navigation.jsx' + }, + 'pubsweet-component-ink-backend': universal.inkBackend, + 'pubsweet-server': { dbPath: path.join(__dirname, '..', 'api', 'db'), secret: '71dcce42-2245-4944-925b-0a62b83425ce', API_ENDPOINT: '/api' }, - 'pubsweet-component-ink-backend': universal.inkBackend, - 'pubsweet-frontend': { - theme: universal.theme, - routes: 'app/routes.jsx', - navigation: 'app/components/Navigation/Navigation.jsx' - } + 'validations': universal.validations } diff --git a/config/universal.js b/config/universal.js index c714b5492835ba0f60b9c2f714c7063efeead8b9..ab3a727918e586a7125c21236f7cdfb351fb3d1b 100644 --- a/config/universal.js +++ b/config/universal.js @@ -1,3 +1,4 @@ +const Joi = require('joi') const editoriaMode = require('../app/authsome_editoria') const inkUsername = process.env.INK_USERNAME @@ -30,5 +31,26 @@ module.exports = { permissions: 'update' } }, - theme: 'ThemeEditoria' + theme: 'ThemeEditoria', + validations: { + collection: { + productionEditor: Joi.object().allow(null) + }, + fragment: { + alignment: Joi.object(), + author: Joi.string().allow(''), + book: Joi.string().guid().required(), + comments: Joi.object(), + division: Joi.string(), + index: Joi.number(), + kind: Joi.string(), + lock: Joi.object().allow(null), + progress: Joi.object(), + source: Joi.string().allow(''), + status: Joi.string(), + subCategory: Joi.string(), + title: Joi.string(), + trackChanges: Joi.boolean() + } + } } diff --git a/package.json b/package.json index 8dabd90b41bb78ed4f39f3e3372f5d7d3e78416f..58d46d362b7ec2b6dc9b268595516b35c7fa8c21 100644 --- a/package.json +++ b/package.json @@ -2,62 +2,67 @@ "name": "editoria", "description": "A new pubsweet app", "dependencies": { - "authsome": "^0.0.4", - "autobind-decorator": "^1.3.4", - "babel-core": "^6.14.0", - "babel-loader": "^6.2.5", - "babel-plugin-transform-decorators-legacy": "^1.3.4", - "babel-preset-es2015": "^6.14.0", - "babel-preset-es2015-native-modules": "^6.9.4", - "babel-preset-react": "^6.11.1", - "babel-preset-stage-2": "^6.13.0", - "bootstrap-sass": "^3.3.7", - "copy-webpack-plugin": "^4.0.1", - "css-loader": "^0.25.0", + "autobind-decorator": "1.4.0", + "babel-plugin-transform-decorators-legacy": "1.3.4", + "babel-preset-es2015-native-modules": "6.9.4", + "babel-preset-stage-2": "6.24.1", + "copy-webpack-plugin": "4.0.1", + "css-loader": "0.25.0", + "extract-text-webpack-plugin": "2.1.0", + "file-loader": "0.9.0", + "font-awesome": "4.7.0", + "html-webpack-plugin": "2.28.0", + "json-loader": "0.5.4", + "lodash": "4.17.4", + "pubsweet-client": "0.9.1", + "pubsweet-component-blog": "0.1.5", + "pubsweet-component-ink-backend": "0.0.6", + "pubsweet-component-ink-frontend": "0.0.3", + "pubsweet-component-login": "0.2.3", + "pubsweet-component-manage": "0.1.5", + "pubsweet-component-navigation": "0.2.2", + "pubsweet-component-signup": "0.1.3", + "pubsweet-component-teams-manager": "0.1.3", + "pubsweet-component-theme-editoria": "git+https://gitlab.coko.foundation/yannisbarlas/pubsweet-component-theme-editoria.git", + "pubsweet-component-users-manager": "0.1.3", + "pubsweet-server": "0.8.1", + "pubsweet-theme-plugin": "0.0.1", + "react": "15.5.4", + "react-bootstrap": "0.30.10", + "react-dnd": "2.3.0", + "react-dnd-html5-backend": "2.3.0", + "react-dom": "15.5.4", + "react-redux": "5.0.4", + "react-router": "2.8.1", + "react-router-bootstrap": "0.23.2", + "redux": "3.6.0", + "sass-loader": "4.1.1", + "script-loader": "0.7.0", + "string-replace-loader": "1.2.0", + "style-loader": "0.13.2", + "substance": "1.0.0-beta.6.5", + "url-loader": "0.5.8", + "webpack": "2.4.1", + "webpack-dev-middleware": "1.10.2", + "webpack-hot-middleware": "2.18.0" + }, + "devDependencies": { + "babel-eslint": "^7.2.1", + "enzyme": "^2.7.1", + "enzyme-to-json": "^1.4.5", "eslint": "^3.6.0", + "eslint-config-airbnb": "^14.1.0", "eslint-config-standard": "^6.2.0", "eslint-config-standard-react": "^4.2.0", "eslint-loader": "^1.6.0", - "eslint-plugin-promise": "^2.0.1", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jsx-a11y": "3.0.2", + "eslint-plugin-promise": "^3.3.0", "eslint-plugin-react": "^6.4.1", "eslint-plugin-standard": "^2.0.0", - "extract-text-webpack-plugin": "^2.0.0-beta.4", - "file-loader": "^0.9.0", - "font-awesome": "^4.7.0", - "html-webpack-plugin": "^2.24.0", - "json-loader": "^0.5.4", - "pubsweet-backend": "0.6.0", - "pubsweet-component-blog": "0.1.0", - "pubsweet-component-ink-backend": "0.0.4-alpha.3", - "pubsweet-component-ink-frontend": "0.0.1", - "pubsweet-component-login": "0.2.1", - "pubsweet-component-manage": "0.1.0", - "pubsweet-component-navigation": "0.1.0", - "pubsweet-component-signup": "0.1.0", - "pubsweet-component-teams-manager": "0.1.1", - "pubsweet-component-theme-editoria": "git+https://gitlab.coko.foundation/yannisbarlas/pubsweet-component-theme-editoria.git", - "pubsweet-component-users-manager": "0.1.0", - "pubsweet-frontend": "0.7.0", - "pubsweet-theme-plugin": "^0.0.1", - "react-dnd": "^2.1.4", - "react-dnd-html5-backend": "^2.1.2", - "react-hot-loader": "^3.0.0-beta.5", - "sass-loader": "^4.0.2", - "script-loader": "^0.7.0", - "string-replace-loader": "^1.0.5", - "style-loader": "^0.13.1", - "substance": "^1.0.0-beta.5.7", - "url-loader": "^0.5.7", - "webpack": "^2.1.0-beta.25", - "webpack-dev-middleware": "^1.8.4", - "webpack-hot-middleware": "^2.13.0" - }, - "devDependencies": { - "enzyme": "^2.7.1", - "enzyme-to-json": "^1.4.5", - "react-addons-test-utils": "^15.4.2", "identity-obj-proxy": "^3.0.0", "jest": "^18.1.0", + "react-addons-test-utils": "^15.4.2", "react-test-renderer": "^15.4.2", "sinon": "^1.17.7", "sinon-as-promised": "^4.0.2" diff --git a/test/jest.config.js b/test/jest.config.js index fde58284fe6a2bd43cd38c5741f9b4b17711145d..7c2bd1c3df6d833a2c2839af6b9f41b18d9c0ec0 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -1,4 +1,4 @@ -global.CONFIG = { 'pubsweet-backend': '' } +global.CONFIG = { 'pubsweet-server': '' } global.PUBSWEET_COMPONENTS = [] global.mock = { diff --git a/webpack/babel-includes.js b/webpack/babel-includes.js index c459384215f365e6891ae6b51c3ef630ac12cedc..0cf6d13a37715dccfa7c42491932ba879c41f768 100644 --- a/webpack/babel-includes.js +++ b/webpack/babel-includes.js @@ -1,7 +1,7 @@ const path = require('path') var babelIncludes = [ - new RegExp(path.join(__dirname, '../node_modules/pubsweet-frontend/src')), + new RegExp(path.join(__dirname, '../node_modules/pubsweet-client/src')), new RegExp(path.join(__dirname, '../app')), new RegExp(path.join(__dirname, '../node_modules/pubsweet-.*')) ] diff --git a/webpack/webpack.dev.config.js b/webpack/webpack.dev.config.js index f187ce07c7e727c0d0a995ba79d7894d00f91d43..6074123277175931f232f8fd7af5206d315fbc43 100644 --- a/webpack/webpack.dev.config.js +++ b/webpack/webpack.dev.config.js @@ -18,11 +18,11 @@ module.exports = [ ] }, output: { - path: path.join(__dirname, '..', 'public', 'assets'), + path: path.join(__dirname, '..', '_build', 'assets'), filename: '[name].js', publicPath: '/assets/' }, - devtool: 'inline-source-map', + devtool: 'cheap-module-source-map', module: { rules: require('./common-rules') }, @@ -33,7 +33,7 @@ module.exports = [ path.resolve(__dirname, '..', 'node_modules'), 'node_modules' ], - plugins: [new ThemePlugin(config['pubsweet-frontend'].theme)], + plugins: [new ThemePlugin(config['pubsweet-client'].theme)], extensions: ['.js', '.jsx', '.json', '.scss'], enforceExtension: false }, @@ -55,6 +55,8 @@ module.exports = [ ], node: { fs: 'empty', + net: 'empty', + dns: 'empty', __dirname: true } } diff --git a/webpack/webpack.production.config.js b/webpack/webpack.production.config.js index 9699d4df2be5f77763d7f2edbad28deb1d2deaa3..9b5b3b3dfbfee7757f9f5afab1024e13afe62d1f 100644 --- a/webpack/webpack.production.config.js +++ b/webpack/webpack.production.config.js @@ -17,7 +17,7 @@ module.exports = [ ] }, output: { - path: path.join(__dirname, '..', 'public', 'assets'), + path: path.join(__dirname, '..', '_build', 'assets'), filename: '[name]-[hash].js', publicPath: '/assets/' }, @@ -32,7 +32,7 @@ module.exports = [ 'node_modules' ], extensions: ['.js', '.jsx', '.json', '.scss'], - plugins: [new ThemePlugin(config['pubsweet-frontend'].theme)] + plugins: [new ThemePlugin(config['pubsweet-client'].theme)] }, plugins: [ new HtmlWebpackPlugin({