Skip to content

Commit

Permalink
Add ability to collapse the queue message (#212)
Browse files Browse the repository at this point in the history
* Rewrite QueueMessage with hooks

* Extract parrot-enabled markdown to component

* Collapse staff message; refactor message editor

* Extract message viewer into component

* Improve styling of staff message viewer

* Improve styling of staff message editor

* Improve styling of message panel

* Persist message collapsed state in local storage

* Add changelog entry

* Move cursor to start of textarea on edit

* Fix errors in hook

* Fix errors in hook

* Improve variable names in local storage hook

* Add hooks linter; switch to published hook package
  • Loading branch information
nwalters512 authored Feb 16, 2019
1 parent bba9d34 commit 1504591
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 193 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = {
parser: 'babel-eslint',
extends: ['airbnb', 'prettier', 'prettier/react'],
plugins: ['react-hooks'],
rules: {
'no-unused-vars': [
'error',
Expand All @@ -20,5 +21,6 @@ module.exports = {
aspects: ['noHref', 'invalidHref', 'preferButton'],
},
],
'react-hooks/rules-of-hooks': 'error',
},
}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ with the current date and the next changes should go under a **[Next]** header.
* Update all dependencies. ([@nwalters512](https://github.com/nwalters512) in [#206](https://github.com/illinois/queue/pull/206))
* Implement landing url redirects. ([@james9909](https://github.com/james9909) in [#208](https://github.com/illinois/queue/pull/208))
* Fix page crash after deleting a queue. ([@james9909](https://github.com/james9909) in [#209](https://github.com/illinois/queue/pull/209))
* Add ability to collapse the queue message. ([@nwalters512](https://github.com/nwalters512) in [#212](https://github.com/illinois/queue/pull/212))

## 8 February 2019

Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@fortawesome/free-brands-svg-icons": "^5.7.1",
"@fortawesome/free-solid-svg-icons": "^5.7.1",
"@fortawesome/react-fontawesome": "^0.1.4",
"@illinois/react-use-local-storage": "^1.0.1",
"@zeit/next-css": "^1.0.1",
"axios": "^0.18.0",
"body-parser": "^1.18.3",
Expand Down Expand Up @@ -52,6 +53,7 @@
"react-dom": "^16.8.1",
"react-flip-move": "^3.0.3",
"react-fontawesome": "^1.6.1",
"react-hanger": "^1.1.2",
"react-markdown": "^4.0.6",
"react-moment": "^0.8.4",
"react-redux": "^6.0.0",
Expand Down Expand Up @@ -84,6 +86,7 @@
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-react": "^7.12.4",
"eslint-plugin-react-hooks": "^1.0.2",
"husky": "^1.3.1",
"jest": "^24.1.0",
"jest-enzyme": "^7.0.1",
Expand Down
2 changes: 1 addition & 1 deletion src/components/NewQuestion.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export default class NewQuestion extends React.Component {
className={headerClassNames}
onClick={() => this.onCardHeaderClick()}
>
<span>New question</span>
<strong>New question</strong>
{isUserCourseStaff && (
<FontAwesomeIcon
icon={faChevronDown}
Expand Down
15 changes: 15 additions & 0 deletions src/components/ParrotMarkdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react'
import ReactMarkdown from 'react-markdown'

import ParrotText from './ParrotText'

const ParrotMarkdown = props => {
const renderers = {
text: element => <ParrotText text={element.children} />,
}
return <ReactMarkdown {...props} renderers={renderers} />
}

ParrotMarkdown.propTypes = ReactMarkdown.propTypes

export default ParrotMarkdown
228 changes: 36 additions & 192 deletions src/components/QueueMessage.js
Original file line number Diff line number Diff line change
@@ -1,207 +1,51 @@
import React from 'react'
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'

import {
Alert,
Button,
Card,
CardHeader,
CardBody,
FormText,
Input,
Nav,
NavItem,
NavLink,
TabContent,
TabPane,
} from 'reactstrap'
import ReactMarkdown from 'react-markdown'
import ParrotText from './ParrotText'
import QueueMessageEditor from './QueueMessageEditor'
import QueueMessageViewer from './QueueMessageViewer'

class QueueMessage extends React.Component {
constructor(props) {
super(props)
const QueueMessage = props => {
const [editing, setEditing] = useState(false)
const { message, isUserCourseStaff } = props

this.state = {
editing: false,
editedMessage: '',
activeTab: '1',
}

this.onMessageChanged = this.onMessageChanged.bind(this)
this.onStartEdit = this.onStartEdit.bind(this)
this.onCancelEdit = this.onCancelEdit.bind(this)
this.onFinishEdit = this.onFinishEdit.bind(this)
this.onChangeTab = this.onChangeTab.bind(this)

this.inputRef = React.createRef()
}

componentDidUpdate(prevProps, prevState) {
// If we we just started editing or we switched to the editor tab from
// another tab, focus the editor input
const { editing, activeTab } = this.state
const startedEditing = !prevState.editing && editing
const switchedToEditor = prevState.activeTab !== '1' && activeTab === '1'
if ((startedEditing || switchedToEditor) && this.inputRef.current) {
this.inputRef.current.focus()
}
}

onMessageChanged(e) {
this.setState({
editedMessage: e.target.value,
})
}

onStartEdit() {
this.setState({
editing: true,
editedMessage: this.props.message || '',
})
}

onCancelEdit() {
this.setState({
editing: false,
})
}

onFinishEdit() {
const attributes = {
message: this.state.editedMessage,
}
this.props.updateQueue(this.props.queueId, attributes).then(() => {
this.setState({ editing: false })
})
}

onChangeTab(tab) {
if (this.state.activeTab !== tab) {
this.setState({ activeTab: tab })
}
// If the user is not on course staff and the message is null or empty,
// don't render the panel to them.
if (!isUserCourseStaff && !message) {
return null
}

render() {
const { message, isUserCourseStaff } = this.props
const { editing, editedMessage } = this.state

// If the user is not on course staff and the message is null or empty,
// don't render the panel to them.
if (!isUserCourseStaff && !message) {
return null
}

const handleTabClick = (e, tab) => {
e.stopPropagation()
e.preventDefault()
this.onChangeTab(tab)
}
let content

const handleTabKeyPress = (e, tab) => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation()
this.onChangeTab(tab)
if (editing) {
const handleSave = savedMessage => {
const attributes = {
message: savedMessage,
}
props.updateQueue(props.queueId, attributes).then(() => {
setEditing(false)
})
}

// We use a custom renderer for text so that we can support parrots!
const renderers = {
text: props => <ParrotText text={props.children} />,
}

let content
let button
if (editing) {
content = (
<Card>
<CardHeader>
<Nav card tabs>
<NavItem>
<NavLink
href="#"
className={classnames({
active: this.state.activeTab === '1',
})}
onClick={e => handleTabClick(e, '1')}
onKeyPress={e => handleTabKeyPress(e, '1')}
role="tab"
aria-selected={this.state.activeTab === '1'}
>
Edit
</NavLink>
</NavItem>
<NavItem>
<NavLink
href="#"
className={classnames({
active: this.state.activeTab === '2',
})}
onClick={e => handleTabClick(e, '2')}
onKeyPress={e => handleTabKeyPress(e, '2')}
role="tab"
aria-selected={this.state.activeTab === '2'}
>
Preview
</NavLink>
</NavItem>
</Nav>
</CardHeader>
<CardBody className="p-0">
<TabContent activeTab={this.state.activeTab}>
<TabPane className="m-2" tabId="1">
<Input
type="textarea"
name="text"
rows="6"
value={editedMessage}
onChange={this.onMessageChanged}
innerRef={this.inputRef}
/>
<FormText color="muted">
You can use Markdown to format this message.
</FormText>
</TabPane>
<TabPane className="m-3" tabId="2">
<ReactMarkdown
source={
editedMessage === '' ? 'Nothing to preview' : editedMessage
}
renderers={renderers}
/>
</TabPane>
</TabContent>
</CardBody>
</Card>
)
button = (
<>
<Button color="primary" onClick={this.onFinishEdit}>
Save
</Button>
<Button color="danger" className="ml-1" onClick={this.onCancelEdit}>
Cancel
</Button>
</>
)
} else {
content = <ReactMarkdown source={message} renderers={renderers} />
button = (
<Button color="primary" onClick={this.onStartEdit}>
Edit
</Button>
)
}

return (
<Alert color="primary" fade={false}>
<h6 className="alert-heading">A message from the queue staff</h6>
{content}
{isUserCourseStaff && <div className="mt-3">{button}</div>}
</Alert>
content = (
<QueueMessageEditor
message={message}
onSave={handleSave}
onCancel={() => setEditing(false)}
/>
)
} else {
content = (
<QueueMessageViewer
queueId={props.queueId}
message={message}
editable={isUserCourseStaff}
collapsible={isUserCourseStaff}
onEdit={() => setEditing(true)}
/>
)
}

return <div className="mb-3">{content}</div>
}

QueueMessage.defaultProps = {
Expand Down
Loading

0 comments on commit 1504591

Please sign in to comment.