Skip to content

Commit

Permalink
Merge pull request #703 from ibi-group/admin-notes
Browse files Browse the repository at this point in the history
Add optional admin-only visibility for notes
  • Loading branch information
landonreed committed Aug 5, 2021
2 parents 0538280 + fe059a9 commit c6bffda
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 73 deletions.
7 changes: 5 additions & 2 deletions i18n/english.yml
Original file line number Diff line number Diff line change
Expand Up @@ -487,13 +487,16 @@ components:
info: "Tip: when changing travel times, consider
using the 'Normalize stop times' button above to automatically update
all stop times to the updated travel time."
NoteForm:
adminOnly: 'Admin only?'
new: Post
postComment: Post a New Comment
NotesViewer:
adminOnly: 'This message is visible to admins only.'
all: All Comments
feedSource: Feed Source
feedVersion: Version
new: Post
none: No comments.
postComment: Post a New Comment
refresh: Refresh
title: Comments
OrganizationList:
Expand Down
117 changes: 117 additions & 0 deletions lib/manager/components/NoteForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// @flow

import Icon from '@conveyal/woonerf/components/icon'
import React, {Component} from 'react'
import { Button, Checkbox, Col, FormControl, Media, Panel } from 'react-bootstrap'

import {getComponentMessages} from '../../common/util/config'
import { getProfileLink } from '../../common/util/util'
import type {Feed} from '../../types'
import type {ManagerUserState} from '../../types/reducers'

type Props = {
feedSource: Feed,
newNotePosted: ({body: string}) => void,
stacked?: boolean,
user: ManagerUserState
}

type State = {
adminOnly: boolean,
body: string
}

const DEFAULT_STATE = {
adminOnly: false,
body: ''
}

/**
* This component shows a form that allows a user to submit a note for either a
* feed source or feed version.
*/
export default class NoteForm extends Component<Props, State> {
messages = getComponentMessages('NoteForm')
state = DEFAULT_STATE

_onChange = ({target}: {target: HTMLInputElement}) => {
const value = target.type === 'checkbox' ? target.checked : target.value
this.setState({[target.name]: value})
}

_onClickPublish = () => {
this.props.newNotePosted(this.state)
this.setState(DEFAULT_STATE)
}

render () {
console.log(this.state)
const {
feedSource,
stacked,
user
} = this.props
const {Body, Heading, Left} = Media
const {adminOnly, body} = this.state
const {profile} = user
if (!profile) {
console.warn('Could not locate user profile', user)
return null
}
const isProjectAdmin = user.permissions && user.permissions.isProjectAdmin(
feedSource.projectId, feedSource.organizationId
)
return (
<form>
<Col xs={12} sm={stacked ? 12 : 4} md={stacked ? 12 : 6}>
<h3>{this.messages('postComment')}</h3>
<Media>
<Left>
<img
alt={profile.email}
height={64}
width={64}
src={profile.picture} />
</Left>
<Body>
<Panel
className='comment-panel'
header={
<Heading>
<a href={getProfileLink(profile.email)}>{profile.email}</a>
</Heading>
}
>
<FormControl
componentClass='textarea'
name='body'
style={{resize: 'none'}}
onChange={this._onChange}
value={body} />
<Button
className='pull-right'
disabled={body === ''}
onClick={this._onClickPublish}
// Ensure button clicking is not obscured by checkbox.
style={{marginTop: '10px', position: 'relative', zIndex: 2}}
type='submit'
>
{this.messages('new')}
</Button>
{isProjectAdmin &&
<Checkbox
checked={adminOnly}
name='adminOnly'
onChange={this._onChange}
>
<Icon type='lock' /> {this.messages('adminOnly')}
</Checkbox>
}
</Panel>
</Body>
</Media>
</Col>
</form>
)
}
}
86 changes: 23 additions & 63 deletions lib/manager/components/NotesViewer.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,46 @@
// @flow

import Icon from '@conveyal/woonerf/components/icon'
import React, {Component} from 'react'
import moment from 'moment'
import gravatar from 'gravatar'
import { Panel, Row, Col, Glyphicon, FormControl, Button, ButtonToolbar, Media } from 'react-bootstrap'
import { Panel, Row, Col, Glyphicon, Button, ButtonToolbar, Media } from 'react-bootstrap'

import {getComponentMessages} from '../../common/util/config'
import { getProfileLink } from '../../common/util/util'

import type {Feed, Note, FeedVersion} from '../../types'
import type {Feed, Note} from '../../types'
import type {ManagerUserState} from '../../types/reducers'

import NoteForm from './NoteForm'

type Props = {
feedSource?: Feed,
feedSource: Feed,
newNotePosted: ({body: string}) => void,
notes: ?Array<Note>,
notesRequested: () => void,
stacked?: boolean,
type: string,
user: ManagerUserState,
version?: FeedVersion
user: ManagerUserState
}

type State = {
value: string
}

export default class NotesViewer extends Component<Props, State> {
export default class NotesViewer extends Component<Props> {
messages = getComponentMessages('NotesViewer')
state = {
value: ''
}

componentWillMount () {
this.props.notesRequested()
}

_onChangeBody = (evt: SyntheticInputEvent<HTMLInputElement>) =>
this.setState({value: evt.target.value})

_onClickPublish = () => {
this.props.newNotePosted({body: this.state.value})
this.setState({value: ''})
}

_getUserUrl = (email: string): string =>
gravatar.url(email, {protocol: 'https', s: '100'})

render () {
const {
feedSource,
newNotePosted,
notes,
notesRequested,
stacked,
user
} = this.props
const {profile} = user
if (!profile) {
console.warn('Could not locate user profile', user)
return null
}
const userLink = user
? <a href={getProfileLink(profile.email)}>{profile.email}</a>
: 'Unknown user'
const {Body, Heading, Left} = Media
return (
<Row>
Expand All @@ -84,7 +63,7 @@ export default class NotesViewer extends Component<Props, State> {
</h3>
{/* List of notes */}
{notes && notes.length > 0
? notes.map(note => {
? notes.sort((a, b) => b.dateCreated - a.dateCreated).map(note => {
const noteDate = moment(note.date)
return (
<Media key={note.id}>
Expand All @@ -100,6 +79,12 @@ export default class NotesViewer extends Component<Props, State> {
className='comment-panel'
header={
<Heading>
{note.adminOnly &&
<Icon
className='pull-right'
title={this.messages('adminOnly')}
type='lock' />
}
<a href={getProfileLink(note.userEmail)}>
{note.userEmail}
</a>
Expand All @@ -118,37 +103,12 @@ export default class NotesViewer extends Component<Props, State> {
: <p><i>{this.messages('none')}</i></p>
}
</Col>
{/* Post note form */}
<Col xs={12} sm={stacked ? 12 : 4} md={stacked ? 12 : 6}>
<h3>{this.messages('postComment')}</h3>
<Media>
<Left>
<img
alt={profile.email}
width={64}
height={64}
src={user ? profile.picture : ''} />
</Left>
<Body>
<Panel
className='comment-panel'
header={<Heading>{userLink}</Heading>}>
<FormControl
ref='newNoteBody'
componentClass='textarea'
value={this.state.value}
onChange={this._onChangeBody} />
<Button
className='pull-right'
style={{marginTop: '10px'}}
disabled={this.state.value === ''}
onClick={this._onClickPublish}>
{this.messages('new')}
</Button>
</Panel>
</Body>
</Media>
</Col>
<NoteForm
feedSource={feedSource}
newNotePosted={newNotePosted}
stacked={stacked}
user={user}
/>
</Row>
)
}
Expand Down
14 changes: 6 additions & 8 deletions lib/manager/components/version/FeedVersionViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ import { LinkContainer } from 'react-router-bootstrap'
import * as versionsActions from '../../actions/versions'
import {getComponentMessages, isModuleEnabled} from '../../../common/util/config'
import Loading from '../../../common/components/Loading'
import DeltaStat from './DeltaStat'
import * as snapshotActions from '../../../editor/actions/snapshots'
import FeedVersionReport from './FeedVersionReport'
import * as plusActions from '../../../gtfsplus/actions/gtfsplus'
import ActiveGtfsPlusVersionSummary from '../../../gtfsplus/containers/ActiveGtfsPlusVersionSummary'
import VersionDateLabel from './VersionDateLabel'
import NotesViewer from '../NotesViewer'
import {errorPriorityLevels, getTableFatalExceptions} from '../../util/version'
import GtfsValidationViewer from '../validation/GtfsValidationViewer'
import VersionButtonToolbar from './VersionButtonToolbar'

import type {Feed, FeedVersion, GtfsPlusValidation, Note, Project} from '../../../types'
import type {GtfsState, ManagerUserState} from '../../../types/reducers'

import VersionButtonToolbar from './VersionButtonToolbar'
import VersionDateLabel from './VersionDateLabel'
import FeedVersionReport from './FeedVersionReport'
import DeltaStat from './DeltaStat'

export type Props = {
comparedVersion?: FeedVersion,
deleteDisabled: ?boolean,
Expand Down Expand Up @@ -125,12 +125,10 @@ export default class FeedVersionViewer extends Component<Props> {
version={version} />
: versionSection === 'comments'
? <NotesViewer
type='feed-version'
feedSource={feedSource}
stacked
user={user}
version={version}
notes={version.notes}
noteCount={version.noteCount}
notesRequested={notesRequested}
newNotePosted={newNotePosted} />
: null
Expand Down
2 changes: 2 additions & 0 deletions lib/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,10 @@ type DatatoolsSettings = {
}

export type Note = {
adminOnly: boolean,
body: string,
date: number,
dateCreated: number,
id: string,
type: string,
userEmail: string
Expand Down

0 comments on commit c6bffda

Please sign in to comment.