From 5ead2a52c0caa78a4ce16b314837ba92019acaf5 Mon Sep 17 00:00:00 2001 From: john <johnbarlas39@gmail.com> Date: Tue, 13 Dec 2016 02:31:42 +0200 Subject: [PATCH] chapter refactor - part one --- app/components/BookBuilder/Chapter.jsx | 575 ++---------------- .../BookBuilder/Chapter/ChapterButtons.jsx | 12 + .../BookBuilder/Chapter/ChapterTitle.jsx | 73 +++ .../BookBuilder/Chapter/DropdownTitle.jsx | 200 ++++++ .../BookBuilder/Chapter/FirstRow.jsx | 308 ++++++++++ .../{ => Chapter}/PagePositionAlignment.jsx | 2 +- .../{ => Chapter}/ProgressIndicator.jsx | 4 +- .../BookBuilder/Chapter/RenameEmptyError.jsx | 25 + .../BookBuilder/Chapter/SecondRow.jsx | 99 +++ app/components/BookBuilder/Chapter/Title.jsx | 50 ++ .../{ => Chapter}/UploadWordBtn.jsx | 2 +- app/components/utils/DnD.js | 69 +++ app/components/utils/config.js | 26 + package.json | 2 +- webpack/common-rules.js | 1 - 15 files changed, 915 insertions(+), 533 deletions(-) create mode 100644 app/components/BookBuilder/Chapter/ChapterButtons.jsx create mode 100644 app/components/BookBuilder/Chapter/ChapterTitle.jsx create mode 100644 app/components/BookBuilder/Chapter/DropdownTitle.jsx create mode 100644 app/components/BookBuilder/Chapter/FirstRow.jsx rename app/components/BookBuilder/{ => Chapter}/PagePositionAlignment.jsx (96%) rename app/components/BookBuilder/{ => Chapter}/ProgressIndicator.jsx (97%) create mode 100644 app/components/BookBuilder/Chapter/RenameEmptyError.jsx create mode 100644 app/components/BookBuilder/Chapter/SecondRow.jsx create mode 100644 app/components/BookBuilder/Chapter/Title.jsx rename app/components/BookBuilder/{ => Chapter}/UploadWordBtn.jsx (94%) create mode 100644 app/components/utils/DnD.js create mode 100644 app/components/utils/config.js diff --git a/app/components/BookBuilder/Chapter.jsx b/app/components/BookBuilder/Chapter.jsx index ead3156..3cece69 100644 --- a/app/components/BookBuilder/Chapter.jsx +++ b/app/components/BookBuilder/Chapter.jsx @@ -1,265 +1,38 @@ import React from 'react' -import { DropdownButton, MenuItem } from 'react-bootstrap' -import { LinkContainer } from 'react-router-bootstrap' import { DragSource, DropTarget } from 'react-dnd' -import { findDOMNode } from 'react-dom' -import { includes, get, map, flow, slice } from 'lodash' +import { flow } from 'lodash' -import BookBuilderModal from './BookBuilderModal' -import PagePositionAlignment from './PagePositionAlignment' -import ProgressIndicator from './ProgressIndicator' -import TextInput from '../utils/TextInput' -import UploadWordButton from './UploadWordBtn' +import FirstRow from './Chapter/FirstRow' +// import SecondRow from './Chapter/SecondRow' -import styles from './styles/bookBuilder.local.scss' - -const itemTypes = { - CHAPTER: 'chapter' -} - -const chapterSource = { - beginDrag (props) { - return { - id: props.id, - no: props.no, - division: props.chapter.division - } - }, - - isDragging (props, monitor) { - return props.id === monitor.getItem().id - } -} - -const chapterTarget = { - // for an explanation of how this works go to - // https://github.com/gaearon/react-dnd/blob/master/examples/04%20Sortable/Simple/Card.js - - hover (props, monitor, component) { - // can only reorder within the same division - const dragDivision = monitor.getItem().division - const hoverDivision = props.chapter.division - - if (dragDivision !== hoverDivision) { return } - - const dragIndex = monitor.getItem().no - const hoverIndex = props.no - - if (dragIndex === hoverIndex) { return } - - const hoverBoundingRect = findDOMNode(component).getBoundingClientRect() - const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 - const clientOffset = monitor.getClientOffset() - const hoverClientY = clientOffset.y - hoverBoundingRect.top - - if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { return } - if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { return } - - props.move(dragIndex, hoverIndex) - monitor.getItem().no = hoverIndex - } -} - -const collectDrag = (connect, monitor) => { - return { - connectDragSource: connect.dragSource(), - isDragging: monitor.isDragging() - } -} +import { chapterSource, chapterTarget, collectDrag, collectDrop, itemTypes } from '../utils/DnD' -const collectDrop = (connect, monitor) => { - return { - connectDropTarget: connect.dropTarget() - } -} +import styles from './styles/bookBuilder.local.scss' export class Chapter extends React.Component { constructor (props) { super(props) - this._onClickRename = this._onClickRename.bind(this) - this._onSaveRename = this._onSaveRename.bind(this) - - this._onClickDelete = this._onClickDelete.bind(this) - this._onClickUnlock = this._onClickUnlock.bind(this) - this._toggleDelete = this._toggleDelete.bind(this) - this._toggleUnlock = this._toggleUnlock.bind(this) - - this._isAdmin = this._isAdmin.bind(this) - this._formatDate = this._formatDate.bind(this) - - this._onClickTitleDropdown = this._onClickTitleDropdown.bind(this) - this._onClickCustomTitle = this._onClickCustomTitle.bind(this) - this._toggleList = this._toggleList.bind(this) - this._myHandler = this._myHandler.bind(this) - this._viewOrEdit = this._viewOrEdit.bind(this) - this.update = this.update.bind(this) this.state = { - isRenamingTitle: false, - isRenameEmpty: false, - showDeleteModal: false, - showUnlockModal: false, - open: false, // control if the dropdwon list is open or not - canEdit: false - } - } - - _onClickRename () { - this.setState({ - isRenamingTitle: true - }) - } - - _onSaveRename (title) { - // save button has been clicked from outside the component - if (typeof title !== 'string') { - // console.log(this.refs) - title = this.refs.chapterInput.state.value - } - - if (title.length === 0) { - this.setState({ - isRenameEmpty: true - }) - return - } - - this.setState({ - isRenameEmpty: false - }) - - const { book, chapter, update } = this.props - chapter.title = title - - update(book, chapter) - - this.setState({ - isRenamingTitle: false - }) - } - - _onClickDelete () { - const { chapter, remove } = this.props - remove(chapter) - this._toggleDelete() - } - - _isAdmin () { - const { roles } = this.props - return includes(roles, 'admin') - } - - _formatDate (timestamp) { - const date = new Date(timestamp) - - const day = date.getDate() - const month = date.getMonth() + 1 - const year = date.getFullYear() - - let hours = date.getHours().toString() - if (hours.length === 1) { - hours = '0' + hours - } - - let minutes = date.getMinutes().toString() - if (minutes.length === 1) { - minutes = '0' + minutes - } - - const theDate = month + '/' + day + '/' + year - const theTime = hours + ':' + minutes - const formatted = theDate + ' ' + theTime - return formatted - } - - _toggleDelete () { - this.setState({ showDeleteModal: !this.state.showDeleteModal }) - } - - _toggleUnlock () { - if (!this._isAdmin()) { return } - this.setState({ showUnlockModal: !this.state.showUnlockModal }) - } - - _onClickUnlock () { - const { book, chapter, update } = this.props - const isAdmin = this._isAdmin() - - if (!isAdmin) { return } - - chapter.lock = null - update(book, chapter) - this._toggleUnlock() - } - - _viewOrEdit () { - const { roles, chapter } = this.props - - if (includes(roles, 'production-editor')) return this.setState({ canEdit: true }) - - if (chapter.progress['review'] === 1 && includes(roles, 'author') || - chapter.progress['edit'] === 1 && includes(roles, 'copy-editor')) { - this.setState({ canEdit: true }) - } else { - this.setState({ canEdit: false }) - } - } - - componentDidMount () { - window.addEventListener('click', this._myHandler) - this._viewOrEdit() - } - - componentWillUnmount () { - window.removeEventListener('click', this._myHandler) - } - - _myHandler (evt) { - if (evt.target.id === 'dropbutton' || - evt.target.parentElement.id === 'dropbutton' || - evt.target.classList.contains('caret') || - evt.target.classList.contains('drop-input')) { - const input = findDOMNode(this.refs.dropDownInput) - if (input) input.focus() - return + isUploadInProgress: false } - this.setState({ - open: false - }) - } - - _onClickTitleDropdown (title) { - const { book, chapter, update } = this.props - const self = this - - if (title === '') { - return - } - - function clickTitleDropdown () { - chapter.title = title - update(book, chapter) - setTimeout(() => { - self.setState({open: false}) - }, 10) - } - - return clickTitleDropdown - } - - _onClickCustomTitle () { - let customTitle = get(this.refs, 'dropDownInput.state.value', null) - this._onClickTitleDropdown(customTitle)() } - _toggleList () { - this.setState({ - open: !this.state.open - }) - } + // _viewOrEdit () { + // const { roles, chapter } = this.props + // + // if (includes(roles, 'production-editor')) return this.setState({ canEdit: true }) + // + // if (chapter.progress['review'] === 1 && includes(roles, 'author') || + // chapter.progress['edit'] === 1 && includes(roles, 'copy-editor')) { + // this.setState({ canEdit: true }) + // } else { + // this.setState({ canEdit: false }) + // } + // } update (changedChapter) { const { book, update } = this.props @@ -267,201 +40,26 @@ export class Chapter extends React.Component { } render () { - const { book, chapter, connectDragSource, connectDropTarget, ink, isDragging, outerContainer, roles, title, type } = this.props - const { isRenamingTitle, isRenameEmpty } = this.state - // const { _onSaveRename } = this - const opacity = isDragging ? 0 : 1 - - let titleArea = null - let renameButton = null - - let renameEmptyError = isRenameEmpty - ? ( - <span className={styles.emptyTitle}> - New title cannot be empty - </span> - ) - : null - - if (type === 'chapter' || type === 'part') { - // if type is chapter, make the title editable text - let renameButtonText, renameButtonFunction - - const input = ( - <TextInput - className='edit' - ref='chapterInput' - onSave={this._onSaveRename} - value={title} - /> - ) - - if (isRenamingTitle) { - titleArea = input - renameButtonText = 'Save' - renameButtonFunction = this._onSaveRename - } else { - titleArea = (<h3 onDoubleClick={this._onClickRename}> { title } </h3>) - renameButtonText = 'Rename' - renameButtonFunction = this._onClickRename - } - - // add id so that it can be selected for testing - // could do with refs, but that would mean mounting instead of - // shallow rendering to access enzyme's refs() api method - renameButton = ( - <a id='bb-rename' - onClick={renameButtonFunction}> - { renameButtonText } - </a> - ) - } else if (type === 'component') { - // if type is component, make title a dropdown choice - - let dropdownOptions - if (chapter.division === 'front') { - dropdownOptions = [ - 'Table of Contents', - 'Introduction', - 'Preface', - 'Preface 1', - 'Preface 2', - 'Preface 3', - 'Preface 4', - 'Preface 5', - 'Preface 6', - 'Preface 7', - 'Preface 8', - 'Preface 9', - 'Preface 10' - ] - } else if (chapter.division === 'back') { - dropdownOptions = [ - 'Appendix A', - 'Appendix B', - 'Appendix C' - ] - } - - const onClickTitleDropdown = this._onClickTitleDropdown - - let width = 180 - let TotalColumns = 1 - if (dropdownOptions.length > 9) { - TotalColumns = Math.ceil(dropdownOptions.length / 5) - } - - const menuItems = map(dropdownOptions, function (item, i) { - const onClickItem = onClickTitleDropdown(item) - - return ( - <MenuItem - className={styles.menuItem} - onClick={onClickItem} - key={i}> - { item } - </MenuItem> - ) - }) - - let columns = menuItems - - if (TotalColumns > 1) { - columns = [] - let loopIt = 1 + const { + book, + chapter, + connectDragSource, + connectDropTarget, + // ink, + isDragging, + outerContainer, + // roles, + title, + type + } = this.props - while (loopIt <= width) { - let start = (loopIt - 1) * 5 - let end = start + 5 - columns.push(slice(menuItems, start, end)) - loopIt += 1 - } - columns = map(columns, function (column, i) { - return ( - <div className={styles.menuItemContainer} key={i}> - { column } - </div> - ) - }) - } - - width = (width * TotalColumns) - - titleArea = ( - <DropdownButton - title={title} - id='dropbutton' - className={styles.dropDown} - open={this.state.open} - onClick={this._toggleList} - > - <div style={{ width: width }}> - <div className={styles.dropDownInputContairer}> - <TextInput - ref='dropDownInput' - className={'drop-input ' + styles.dropDownInput} - onSave={this._onClickCustomTitle} - placeholder='Type a custom title' - /> - </div> - - { columns } - </div> - - </DropdownButton> - ) - } - - const editOrView = this.state.canEdit ? 'Edit' : 'View' - - const buttons = ( - <div> - { renameButton } - <LinkContainer - to={`/books/${book.id}/fragments/${chapter.id}`} - id='bb-edit' - > - <a>{ editOrView } </a> - </LinkContainer> - - <a id='bb-delete' - onClick={this._toggleDelete}> - Delete - </a> - </div> - ) - - let editorArea - if (get(chapter, 'lock.editor.username')) { - let message = ' is editing' - if (chapter.lock.timestamp && this._isAdmin()) { - message = ' has been editing since ' + this._formatDate(chapter.lock.timestamp) - } - - editorArea = ( - <a id='bb-unlock' - className={styles.lEditing} - onClick={this._toggleUnlock}> - - <i - className={styles.lockIcon + ' fa fa-lock'} - aria-hidden='true' - alt='unlock' - /> - <span className={styles.lockMessage}> - { chapter.lock.editor.username + message} - </span> - - </a> - ) - } - - const rightArea = chapter.lock ? editorArea : buttons + const opacity = isDragging ? 0 : 1 return connectDragSource(connectDropTarget( <li className={styles.chapterContainer + ' col-lg-12 bb-chapter ' + (chapter.subCategory === 'chapter' ? styles.isChapter : styles.isPart)} style={{ opacity: opacity }}> + <div className={styles.grabIcon + ' ' + (chapter.division === 'body' ? styles.grabIconBody : '')}> <i className='fa fa-circle' /> <div className={styles.tooltip}> @@ -470,104 +68,27 @@ export class Chapter extends React.Component { </div> <div className={styles.chapterMainContent}> - <div className={styles.chapterTitle}> - { titleArea } - { renameEmptyError } - <div className={styles.separator} /> - </div> - - <div className={styles.chapterActions + ' pull-right'}> - {rightArea} - </div> + <FirstRow + book={book} + chapter={chapter} + outerContainer={outerContainer} + title={title} + type={type} + update={this.update} + /> <div className={styles.chapterBottomLine} /> - <div className={styles.secondLineContainer}> - <div className={styles.noPadding + ' col-lg-2 col-md-12 col-sm-12 col-xs-12'}> - <UploadWordButton - accept='.docx' - ink={ink} - title=' ' - type='file' - /> - </div> - - <ul className={styles.secondActions + ' col-lg-7 col-md-12 col-sm-12 col-xs-12'}> - <ProgressIndicator - type='style' - chapter={chapter} - update={this.update} - roles={roles} - outerContainer={outerContainer} - hasIcon - viewOrEdit={this._viewOrEdit} - /> - - <ProgressIndicator - type='edit' - chapter={chapter} - update={this.update} - roles={roles} - outerContainer={outerContainer} - hasIcon - viewOrEdit={this._viewOrEdit} - /> - - <ProgressIndicator - type='review' - chapter={chapter} - update={this.update} - roles={roles} - outerContainer={outerContainer} - hasIcon - viewOrEdit={this._viewOrEdit} - /> - - <ProgressIndicator - type='clean' - chapter={chapter} - roles={roles} - outerContainer={outerContainer} - update={this.update} - viewOrEdit={this._viewOrEdit} - /> - </ul> - - <div className={styles.noPadding + ' col-lg-3 col-md-12 col-sm-12 col-xs-12'}> - <PagePositionAlignment - chapter={chapter} - update={this.update} - /> - </div> - - <div className={styles.separator} /> - </div> + {/* <SecondRow + chapter={chapter} + ink={ink} + outerContainer={outerContainer} + roles={roles} + update={this.update} + viewOrEdit={this._viewOrEdit} + /> */} </div> - <BookBuilderModal - title={'Delete ' + type} - chapter={chapter} - action='delete' - successText='Delete' - type={type} - successAction={this._onClickDelete} - show={this.state.showDeleteModal} - toggle={this._toggleDelete} - container={outerContainer} - /> - - <BookBuilderModal - title={'Unlock ' + type} - chapter={chapter} - action='unlock' - successText='Unlock' - type={type} - successAction={this._onClickUnlock} - show={this.state.showUnlockModal} - toggle={this._toggleUnlock} - container={outerContainer} - /> - <div className={chapter.division === 'body' ? styles.leftBorderBody : styles.leftBorderComponent} /> </li> )) diff --git a/app/components/BookBuilder/Chapter/ChapterButtons.jsx b/app/components/BookBuilder/Chapter/ChapterButtons.jsx new file mode 100644 index 0000000..d10878d --- /dev/null +++ b/app/components/BookBuilder/Chapter/ChapterButtons.jsx @@ -0,0 +1,12 @@ +// import React from 'react' +// import { LinkContainer } from 'react-router-bootstrap' +// +// class ChapterButtons extends React.Component { +// // constructor (props) { +// // super(props) +// // } +// +// render () { +// +// } +// } diff --git a/app/components/BookBuilder/Chapter/ChapterTitle.jsx b/app/components/BookBuilder/Chapter/ChapterTitle.jsx new file mode 100644 index 0000000..8334f89 --- /dev/null +++ b/app/components/BookBuilder/Chapter/ChapterTitle.jsx @@ -0,0 +1,73 @@ +import React from 'react' + +import DropdownTitle from './DropdownTitle' +import RenameEmptyError from './RenameEmptyError' +import Title from './Title' + +import styles from '../styles/bookBuilder.local.scss' + +class ChapterTitle extends React.Component { + constructor (props) { + super(props) + + this.state = { + isRenameEmpty: false, + isRenamingTitle: false + } + } + + render () { + const { + chapter, + division, + onClickRename, + onSaveRename, + title, + type, + update + } = this.props + const { isRenameEmpty, isRenaming } = this.state + + let titleArea + + if (type === 'chapter' || type === 'part') { + titleArea = ( + <Title + isRenaming={isRenaming} + onClickRename={onClickRename} + onSaveRename={onSaveRename} + title={title} + /> + ) + } else if (type === 'component') { + titleArea = ( + <DropdownTitle + chapter={chapter} + division={division} + title={title} + update={update} + /> + ) + } + + return ( + <div className={styles.chapterTitle}> + { titleArea } + <RenameEmptyError isRenameEmpty={isRenameEmpty} /> + <div className={styles.separator} /> + </div> + ) + } +} + +ChapterTitle.propTypes = { + chapter: React.PropTypes.object.isRequired, + division: React.PropTypes.string.isRequired, + onClickRename: React.PropTypes.func.isRequired, + onSaveRename: React.PropTypes.func.isRequired, + title: React.PropTypes.string.isRequired, + type: React.PropTypes.string.isRequired, + update: React.PropTypes.func.isRequired +} + +export default ChapterTitle diff --git a/app/components/BookBuilder/Chapter/DropdownTitle.jsx b/app/components/BookBuilder/Chapter/DropdownTitle.jsx new file mode 100644 index 0000000..0015521 --- /dev/null +++ b/app/components/BookBuilder/Chapter/DropdownTitle.jsx @@ -0,0 +1,200 @@ +import { + get, + map, + slice +} from 'lodash' + +import React from 'react' +import { DropdownButton, MenuItem } from 'react-bootstrap' +import { findDOMNode } from 'react-dom' + +import TextInput from '../../utils/TextInput' +import { chapter as config } from '../../utils/config' + +import styles from '../styles/bookBuilder.local.scss' + +class DropdownTitle extends React.Component { + constructor (props) { + super(props) + + this.breakIntoColumns = this.breakIntoColumns.bind(this) + this.close = this.close.bind(this) + this.getColumnCount = this.getColumnCount.bind(this) + this.getDropdownOptions = this.getDropdownOptions.bind(this) + this.getMenuItems = this.getMenuItems.bind(this) + this.handleClickOutside = this.handleClickOutside.bind(this) + this.onClickOption = this.onClickOption.bind(this) + this.setCustomTitle = this.setCustomTitle.bind(this) + this.toggle = this.toggle.bind(this) + this.update = this.update.bind(this) + + this.state = { + open: false + } + + this.maxItemsInColumn = 5 + this.width = 180 + } + + breakIntoColumns (items) { + const max = this.maxItemsInColumn + const width = this.width + + const columns = [] + let loopIt = 1 + + // TODO -- width is 180, why am I looping that?! + while (loopIt <= width) { + let start = (loopIt - 1) * max + let end = start + max + + columns.push(slice(items, start, end)) + loopIt += 1 + } + + return map(columns, function (column, i) { + return ( + <div + className={styles.menuItemContainer} + key={i} + > + { column } + </div> + ) + }) + } + + getColumnCount () { + const dropdownOptions = this.getDropdownOptions() + const len = dropdownOptions.length + + if (len > 9) return Math.ceil(len / 5) + return 1 + } + + getDropdownOptions () { + const { division } = this.props + return config.dropdownValues[division] + } + + getMenuItems () { + const dropdownOptions = this.getDropdownOptions() + const onClickOption = this.onClickOption + + const menuItems = map(dropdownOptions, function (item, i) { + return ( + <MenuItem + className={styles.menuItem} + onClick={onClickOption} + key={i} + > + { item } + </MenuItem> + ) + }) + + return menuItems + } + + onClickOption (event) { + const value = event.target.innerHTML.trim() + this.update(value) + this.close() + } + + setCustomTitle (e) { + let value = get(this.refs, 'dropDownInput.state.value', null) + this.update(value) + // TODO -- why the timeout here? + setTimeout(() => this.close(), 10) + } + + toggle () { + this.setState({ open: !this.state.open }) + } + + close () { + this.setState({ open: false }) + } + + update (title) { + const { chapter, update } = this.props + + chapter.title = title + update(chapter) + } + + componentDidMount () { + window.addEventListener('click', this.handleClickOutside) + } + + componentWillUnmount () { + window.removeEventListener('click', this.handleClickOutside) + } + + handleClickOutside (event) { + var domNode = findDOMNode(this) + + if (domNode.classList.contains('open')) { + if (!domNode.contains(event.target)) { + this.close() + } + } + } + + renderInput () { + return ( + <div className={styles.dropDownInputContairer}> + <TextInput + ref='dropDownInput' + className={'drop-input ' + styles.dropDownInput} + onSave={this.setCustomTitle} + placeholder='Type a custom title' + /> + </div> + ) + } + + render () { + const { title } = this.props + + const columnCount = this.getColumnCount() + const menuItems = this.getMenuItems() + const input = this.renderInput() + const width = this.width + + let columns = menuItems + if (columnCount > 1) columns = this.breakIntoColumns(menuItems) + + const dropdownStyle = { + width: width * columnCount + } + + return ( + <DropdownButton + className={styles.dropDown} + id={'dropdown-title-menu'} + open={this.state.open} + onClick={this.toggle} + title={title} + ref={'dropdown-title'} + > + + <div style={dropdownStyle}> + { input } + { columns } + </div> + + </DropdownButton> + ) + } +} + +DropdownTitle.propTypes = { + chapter: React.PropTypes.object.isRequired, + division: React.PropTypes.string.isRequired, + title: React.PropTypes.string.isRequired, + update: React.PropTypes.func.isRequired +} + +export default DropdownTitle diff --git a/app/components/BookBuilder/Chapter/FirstRow.jsx b/app/components/BookBuilder/Chapter/FirstRow.jsx new file mode 100644 index 0000000..17344be --- /dev/null +++ b/app/components/BookBuilder/Chapter/FirstRow.jsx @@ -0,0 +1,308 @@ +import { get, includes, map, slice } from 'lodash' +import React from 'react' +import { DropdownButton, MenuItem } from 'react-bootstrap' +import { LinkContainer } from 'react-router-bootstrap' + +import { findDOMNode } from 'react-dom' + +import BookBuilderModal from '../BookBuilderModal' +import TextInput from '../../utils/TextInput' + +import ChapterTitle from './ChapterTitle' + +class ChapterFirstRow extends React.Component { + constructor (props) { + super(props) + + this._viewOrEdit = this._viewOrEdit.bind(this) + + this._onClickRename = this._onClickRename.bind(this) + this._onSaveRename = this._onSaveRename.bind(this) + + this._onClickDelete = this._onClickDelete.bind(this) + this._onClickUnlock = this._onClickUnlock.bind(this) + this._toggleDelete = this._toggleDelete.bind(this) + this._toggleUnlock = this._toggleUnlock.bind(this) + + this._isAdmin = this._isAdmin.bind(this) + this._formatDate = this._formatDate.bind(this) + + this._viewOrEdit = this._viewOrEdit.bind(this) + + // this._onClickTitleDropdown = this._onClickTitleDropdown.bind(this) + // this._onClickCustomTitle = this._onClickCustomTitle.bind(this) + + this.state = { + canEdit: true, + isRenameEmpty: false, + isRenamingTitle: false, + showDeleteModal: false, + showUnlockModal: false + } + } + + _toggleDelete () { + this.setState({ showDeleteModal: !this.state.showDeleteModal }) + } + + _toggleUnlock () { + if (!this._isAdmin()) { return } + this.setState({ showUnlockModal: !this.state.showUnlockModal }) + } + + _isAdmin () { + const { roles } = this.props + return includes(roles, 'admin') + } + + _onClickUnlock () { + const { book, chapter, update } = this.props + const isAdmin = this._isAdmin() + + if (!isAdmin) { return } + + chapter.lock = null + update(book, chapter) + this._toggleUnlock() + } + + _onClickRename () { + this.setState({ + isRenamingTitle: true + }) + } + + _onSaveRename (title) { + // save button has been clicked from outside the component + if (typeof title !== 'string') { + // console.log(this.refs) + title = this.refs.chapterInput.state.value + } + + if (title.length === 0) { + this.setState({ + isRenameEmpty: true + }) + return + } + + this.setState({ + isRenameEmpty: false + }) + + const { book, chapter, update } = this.props + chapter.title = title + + update(book, chapter) + + this.setState({ + isRenamingTitle: false + }) + } + + _onClickDelete () { + const { chapter, remove } = this.props + remove(chapter) + this._toggleDelete() + } + + _viewOrEdit () { + const { roles, chapter } = this.props + + if (includes(roles, 'production-editor')) return this.setState({ canEdit: true }) + + if (chapter.progress['review'] === 1 && includes(roles, 'author') || + chapter.progress['edit'] === 1 && includes(roles, 'copy-editor')) { + this.setState({ canEdit: true }) + } else { + this.setState({ canEdit: false }) + } + } + + _formatDate (timestamp) { + const date = new Date(timestamp) + + const day = date.getDate() + const month = date.getMonth() + 1 + const year = date.getFullYear() + + let hours = date.getHours().toString() + if (hours.length === 1) { + hours = '0' + hours + } + + let minutes = date.getMinutes().toString() + if (minutes.length === 1) { + minutes = '0' + minutes + } + + const theDate = month + '/' + day + '/' + year + const theTime = hours + ':' + minutes + const formatted = theDate + ' ' + theTime + return formatted + } + + render () { + const { + book, + chapter, + outerContainer, + title, + type, + update + } = this.props + + const { isRenameEmpty, isRenamingTitle } = this.state + + // let titleArea = null + // let renameButton = null + + // let renameEmptyError = isRenameEmpty + // ? ( + // <span className={styles.emptyTitle}> + // New title cannot be empty + // </span> + // ) + // : null + + // if (type === 'chapter' || type === 'part') { + // // if type is chapter, make the title editable text + // let renameButtonText, renameButtonFunction + // + // const input = ( + // <TextInput + // className='edit' + // ref='chapterInput' + // onSave={this._onSaveRename} + // value={title} + // /> + // ) + // + // if (isRenamingTitle) { + // titleArea = input + // renameButtonText = 'Save' + // renameButtonFunction = this._onSaveRename + // } else { + // titleArea = (<h3 onDoubleClick={this._onClickRename}> { title } </h3>) + // renameButtonText = 'Rename' + // renameButtonFunction = this._onClickRename + // } + // + // // add id so that it can be selected for testing + // // could do with refs, but that would mean mounting instead of + // // shallow rendering to access enzyme's refs() api method + // renameButton = ( + // <a id='bb-rename' + // onClick={renameButtonFunction}> + // { renameButtonText } + // </a> + // ) + // } + + // const editOrView = this.state.canEdit ? 'Edit' : 'View' + + // const buttons = ( + // <div> + // { renameButton } + // <LinkContainer + // to={`/books/${book.id}/fragments/${chapter.id}`} + // id='bb-edit' + // > + // <a>{ editOrView } </a> + // </LinkContainer> + // + // <a id='bb-delete' + // onClick={this._toggleDelete}> + // Delete + // </a> + // </div> + // ) + + // let editorArea + // if (get(chapter, 'lock.editor.username')) { + // let message = ' is editing' + // if (chapter.lock.timestamp && this._isAdmin()) { + // message = ' has been editing since ' + this._formatDate(chapter.lock.timestamp) + // } + // + // editorArea = ( + // <a id='bb-unlock' + // className={styles.lEditing} + // onClick={this._toggleUnlock}> + // + // <i + // className={styles.lockIcon + ' fa fa-lock'} + // aria-hidden='true' + // alt='unlock' + // /> + // <span className={styles.lockMessage}> + // { chapter.lock.editor.username + message} + // </span> + // + // </a> + // ) + // } + + // const rightArea = chapter.lock ? editorArea : buttons + + const division = chapter.division + + return ( + <span> + <ChapterTitle + chapter={chapter} + division={division} + onClickRename={this._onClickRename} + onSaveRename={this._onSaveRename} + title={title} + type={type} + update={update} + /> + + {/* <ChapterButtons /> */} + + {/* <div className={styles.chapterActions + ' pull-right'}> + { rightArea } + </div> */} + + <BookBuilderModal + title={'Delete ' + type} + chapter={chapter} + action='delete' + successText='Delete' + type={type} + successAction={this._onClickDelete} + show={this.state.showDeleteModal} + toggle={this._toggleDelete} + container={outerContainer} + /> + + <BookBuilderModal + title={'Unlock ' + type} + chapter={chapter} + action='unlock' + successText='Unlock' + type={type} + successAction={this._onClickUnlock} + show={this.state.showUnlockModal} + toggle={this._toggleUnlock} + container={outerContainer} + /> + </span> + ) + } +} + +ChapterFirstRow.propTypes = { + book: React.PropTypes.object.isRequired, + chapter: React.PropTypes.object.isRequired, + // ink: React.PropTypes.func.isRequired, + outerContainer: React.PropTypes.object.isRequired, + remove: React.PropTypes.func.isRequired, + roles: React.PropTypes.array, + title: React.PropTypes.string.isRequired, + type: React.PropTypes.string.isRequired, + update: React.PropTypes.func.isRequired +} + +export default ChapterFirstRow diff --git a/app/components/BookBuilder/PagePositionAlignment.jsx b/app/components/BookBuilder/Chapter/PagePositionAlignment.jsx similarity index 96% rename from app/components/BookBuilder/PagePositionAlignment.jsx rename to app/components/BookBuilder/Chapter/PagePositionAlignment.jsx index e84f038..2c0f603 100644 --- a/app/components/BookBuilder/PagePositionAlignment.jsx +++ b/app/components/BookBuilder/Chapter/PagePositionAlignment.jsx @@ -1,5 +1,5 @@ import React from 'react' -import styles from './styles/bookBuilder.local.scss' +import styles from '../styles/bookBuilder.local.scss' import { includes } from 'lodash' export class PagePositionAlignment extends React.Component { diff --git a/app/components/BookBuilder/ProgressIndicator.jsx b/app/components/BookBuilder/Chapter/ProgressIndicator.jsx similarity index 97% rename from app/components/BookBuilder/ProgressIndicator.jsx rename to app/components/BookBuilder/Chapter/ProgressIndicator.jsx index 154088a..f5015cf 100644 --- a/app/components/BookBuilder/ProgressIndicator.jsx +++ b/app/components/BookBuilder/Chapter/ProgressIndicator.jsx @@ -1,8 +1,8 @@ import React from 'react' import { includes } from 'lodash' import { Alert } from 'react-bootstrap' -import BookBuilderModal from './BookBuilderModal' -import styles from './styles/bookBuilder.local.scss' +import BookBuilderModal from '../BookBuilderModal' +import styles from '../styles/bookBuilder.local.scss' export class ProgressIndicator extends React.Component { constructor (props) { diff --git a/app/components/BookBuilder/Chapter/RenameEmptyError.jsx b/app/components/BookBuilder/Chapter/RenameEmptyError.jsx new file mode 100644 index 0000000..822f264 --- /dev/null +++ b/app/components/BookBuilder/Chapter/RenameEmptyError.jsx @@ -0,0 +1,25 @@ +import React from 'react' + +import styles from '../styles/bookBuilder.local.scss' + +class RenameEmptyError extends React.Component { + render () { + const { isRenameEmpty } = this.props + + if (isRenameEmpty) { + return ( + <span className={styles.emptyTitle}> + New title cannot be empty + </span> + ) + } + + return null + } +} + +RenameEmptyError.propTypes = { + isRenameEmpty: React.PropTypes.bool.isRequired +} + +export default RenameEmptyError diff --git a/app/components/BookBuilder/Chapter/SecondRow.jsx b/app/components/BookBuilder/Chapter/SecondRow.jsx new file mode 100644 index 0000000..131623d --- /dev/null +++ b/app/components/BookBuilder/Chapter/SecondRow.jsx @@ -0,0 +1,99 @@ +import React from 'react' + +import PagePositionAlignment from './PagePositionAlignment' +import ProgressIndicator from './ProgressIndicator' +import UploadWordButton from './UploadWordBtn' + +import styles from '../styles/bookBuilder.local.scss' + +class ChapterSecondRow extends React.Component { + render () { + const { + chapter, + ink, + outerContainer, + roles + } = this.props + + return ( + <div className={styles.secondLineContainer}> + + <div className={styles.noPadding + ' col-lg-2 col-md-12 col-sm-12 col-xs-12'}> + <UploadWordButton + accept='.docx' + ink={ink} + title=' ' + type='file' + /> + </div> + + <ul className={styles.secondActions + ' col-lg-7 col-md-12 col-sm-12 col-xs-12'}> + <ProgressIndicator + type='style' + chapter={chapter} + update={this.update} + roles={roles} + outerContainer={outerContainer} + hasIcon + viewOrEdit={this._viewOrEdit} + /> + + <ProgressIndicator + type='edit' + chapter={chapter} + update={this.update} + roles={roles} + outerContainer={outerContainer} + hasIcon + viewOrEdit={this._viewOrEdit} + /> + + <ProgressIndicator + type='review' + chapter={chapter} + update={this.update} + roles={roles} + outerContainer={outerContainer} + hasIcon + viewOrEdit={this._viewOrEdit} + /> + + <ProgressIndicator + type='clean' + chapter={chapter} + roles={roles} + outerContainer={outerContainer} + update={this.update} + viewOrEdit={this._viewOrEdit} + /> + </ul> + + <div className={styles.noPadding + ' col-lg-3 col-md-12 col-sm-12 col-xs-12'}> + <PagePositionAlignment + chapter={chapter} + update={this.update} + /> + </div> + + <div className={styles.separator} /> + </div> + ) + } +} + +ChapterSecondRow.propTypes = { + book: React.PropTypes.object.isRequired, + chapter: React.PropTypes.object.isRequired, + connectDragSource: React.PropTypes.func.isRequired, + connectDropTarget: React.PropTypes.func.isRequired, + ink: React.PropTypes.func.isRequired, + isDragging: React.PropTypes.bool.isRequired, + outerContainer: React.PropTypes.object.isRequired, + remove: React.PropTypes.func.isRequired, + roles: React.PropTypes.array, + title: React.PropTypes.string.isRequired, + type: React.PropTypes.string.isRequired, + update: React.PropTypes.func.isRequired +} + +export default ChapterSecondRow diff --git a/app/components/BookBuilder/Chapter/Title.jsx b/app/components/BookBuilder/Chapter/Title.jsx new file mode 100644 index 0000000..dac49bc --- /dev/null +++ b/app/components/BookBuilder/Chapter/Title.jsx @@ -0,0 +1,50 @@ +import React from 'react' + +import TextInput from '../../utils/TextInput' + +class Title extends React.Component { + // constructor (props) { + // super(props) + // + // // this.state = { + // // isRenaming: false + // // } + // } + + // _onClickRename () { + // this.setState({ + // isRenaming: true + // }) + // } + + render () { + const { isRenaming, onClickRename, onSaveRename, title } = this.props + + const input = ( + <TextInput + className='edit' + ref='chapterInput' + onSave={onSaveRename} + value={title} + /> + ) + + const plainTitle = ( + <h3 onDoubleClick={onClickRename}> + { title } + </h3> + ) + + if (isRenaming) return input + return plainTitle + } +} + +Title.propTypes = { + isRenaming: React.PropTypes.bool.isRequired, + onClickRename: React.PropTypes.func.isRequired, + onSaveRename: React.PropTypes.func.isRequired, + title: React.PropTypes.string.isRequired +} + +export default Title diff --git a/app/components/BookBuilder/UploadWordBtn.jsx b/app/components/BookBuilder/Chapter/UploadWordBtn.jsx similarity index 94% rename from app/components/BookBuilder/UploadWordBtn.jsx rename to app/components/BookBuilder/Chapter/UploadWordBtn.jsx index 41e27a0..2e827d9 100644 --- a/app/components/BookBuilder/UploadWordBtn.jsx +++ b/app/components/BookBuilder/Chapter/UploadWordBtn.jsx @@ -1,5 +1,5 @@ import React from 'react' -import styles from './styles/bookBuilder.local.scss' +import styles from '../styles/bookBuilder.local.scss' export class UploadWordButton extends React.Component { constructor (props) { diff --git a/app/components/utils/DnD.js b/app/components/utils/DnD.js new file mode 100644 index 0000000..782f3c2 --- /dev/null +++ b/app/components/utils/DnD.js @@ -0,0 +1,69 @@ +import { findDOMNode } from 'react-dom' + +const itemTypes = { + CHAPTER: 'chapter' +} + +const chapterSource = { + beginDrag (props) { + return { + id: props.id, + no: props.no, + division: props.chapter.division + } + }, + + isDragging (props, monitor) { + return props.id === monitor.getItem().id + } +} + +const chapterTarget = { + // for an explanation of how this works go to + // https://github.com/gaearon/react-dnd/blob/master/examples/04%20Sortable/Simple/Card.js + + hover (props, monitor, component) { + // can only reorder within the same division + const dragDivision = monitor.getItem().division + const hoverDivision = props.chapter.division + + if (dragDivision !== hoverDivision) { return } + + const dragIndex = monitor.getItem().no + const hoverIndex = props.no + + if (dragIndex === hoverIndex) { return } + + const hoverBoundingRect = findDOMNode(component).getBoundingClientRect() + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 + const clientOffset = monitor.getClientOffset() + const hoverClientY = clientOffset.y - hoverBoundingRect.top + + if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { return } + if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { return } + + props.move(dragIndex, hoverIndex) + monitor.getItem().no = hoverIndex + } +} + +const collectDrag = (connect, monitor) => { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging() + } +} + +const collectDrop = (connect, monitor) => { + return { + connectDropTarget: connect.dropTarget() + } +} + +export { + chapterSource, + chapterTarget, + collectDrag, + collectDrop, + itemTypes +} diff --git a/app/components/utils/config.js b/app/components/utils/config.js new file mode 100644 index 0000000..59ce4b3 --- /dev/null +++ b/app/components/utils/config.js @@ -0,0 +1,26 @@ +const chapter = { + dropdownValues: { + front: [ + 'Table of Contents', + 'Introduction', + 'Preface', + 'Preface 1', + 'Preface 2', + 'Preface 3', + 'Preface 4', + 'Preface 5', + 'Preface 6', + 'Preface 7', + 'Preface 8', + 'Preface 9', + 'Preface 10' + ], + back: [ + 'Appendix A', + 'Appendix B', + 'Appendix C' + ] + } +} + +export { chapter } diff --git a/package.json b/package.json index 0728413..ab469f5 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,9 @@ "json-loader": "^0.5.4", "pubsweet-backend": "^0.5.0", "pubsweet-component-blog": "^0.1.0", - "pubsweet-component-login": "^0.1.0", "pubsweet-component-ink-backend": "^0.0.3", "pubsweet-component-ink-frontend": "^0.0.1", + "pubsweet-component-login": "^0.1.0", "pubsweet-component-manage": "^0.1.0", "pubsweet-component-navigation": "^0.1.0", "pubsweet-component-signup": "^0.1.0", diff --git a/webpack/common-rules.js b/webpack/common-rules.js index c68872f..60b4018 100644 --- a/webpack/common-rules.js +++ b/webpack/common-rules.js @@ -89,7 +89,6 @@ module.exports = [ loader: 'string-replace-loader', query: { search: 'PUBSWEET_COMPONENTS', - // replace: '[' + config.pubsweet.components.map(component => `require('${component}')`).join(', ') + ']' replace: '[' + frontendComponents.map(component => `require('${component}')`).join(', ') + ']' }, include: babelIncludes -- GitLab