Skip to content

Commit 76d879f

Browse files
committed
feat(notes): add optional admin-only visibility for notes
re #702
1 parent 1f91096 commit 76d879f

File tree

5 files changed

+140
-71
lines changed

5 files changed

+140
-71
lines changed

i18n/english.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,13 +487,16 @@ components:
487487
info: "Tip: when changing travel times, consider
488488
using the 'Normalize stop times' button above to automatically update
489489
all stop times to the updated travel time."
490+
NoteForm:
491+
adminOnly: 'Admin only?'
492+
new: Post
493+
postComment: Post a New Comment
490494
NotesViewer:
495+
adminOnly: 'This message is visible to admins only.'
491496
all: All Comments
492497
feedSource: Feed Source
493498
feedVersion: Version
494-
new: Post
495499
none: No comments.
496-
postComment: Post a New Comment
497500
refresh: Refresh
498501
title: Comments
499502
OrganizationList:

lib/manager/components/NoteForm.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// @flow
2+
3+
import Icon from '@conveyal/woonerf/components/icon'
4+
import React, {Component} from 'react'
5+
import { Button, Checkbox, Col, FormControl, Media, Panel } from 'react-bootstrap'
6+
7+
import {getComponentMessages} from '../../common/util/config'
8+
import { getProfileLink } from '../../common/util/util'
9+
import type {Feed} from '../../types'
10+
import type {ManagerUserState} from '../../types/reducers'
11+
12+
type Props = {
13+
feedSource: Feed,
14+
newNotePosted: ({body: string}) => void,
15+
stacked?: boolean,
16+
user: ManagerUserState
17+
}
18+
19+
type State = {
20+
adminOnly: boolean,
21+
body: string
22+
}
23+
24+
const DEFAULT_STATE = {
25+
adminOnly: false,
26+
body: ''
27+
}
28+
29+
export default class NoteForm extends Component<Props, State> {
30+
messages = getComponentMessages('NoteForm')
31+
state = DEFAULT_STATE
32+
33+
_onChange = ({target}: {target: HTMLInputElement}) =>
34+
this.setState({[target.name]: target.type === 'checkbox' ? target.checked : target.value})
35+
36+
_onClickPublish = () => {
37+
this.props.newNotePosted(this.state)
38+
this.setState(DEFAULT_STATE)
39+
}
40+
41+
render () {
42+
const {
43+
feedSource,
44+
stacked,
45+
user
46+
} = this.props
47+
const {Body, Heading, Left} = Media
48+
const {adminOnly, body} = this.state
49+
const {profile} = user
50+
if (!profile) {
51+
console.warn('Could not locate user profile', user)
52+
return null
53+
}
54+
const isProjectAdmin = user.permissions && user.permissions.isProjectAdmin(
55+
feedSource.projectId, feedSource.organizationId
56+
)
57+
const userLink = user
58+
? <a href={getProfileLink(profile.email)}>{profile.email}</a>
59+
: 'Unknown user'
60+
return (
61+
<Col xs={12} sm={stacked ? 12 : 4} md={stacked ? 12 : 6}>
62+
<h3>{this.messages('postComment')}</h3>
63+
<Media>
64+
<Left>
65+
<img
66+
alt={profile.email}
67+
width={64}
68+
height={64}
69+
src={user ? profile.picture : ''} />
70+
</Left>
71+
<Body>
72+
<Panel
73+
className='comment-panel'
74+
header={<Heading>{userLink}</Heading>}>
75+
<FormControl
76+
ref='newNoteBody'
77+
componentClass='textarea'
78+
name='body'
79+
value={body}
80+
onChange={this._onChange} />
81+
<Button
82+
className='pull-right'
83+
style={{marginTop: '10px'}}
84+
disabled={body === ''}
85+
onClick={this._onClickPublish}>
86+
{this.messages('new')}
87+
</Button>
88+
{isProjectAdmin &&
89+
<Checkbox
90+
name='adminOnly'
91+
onChange={this._onChange}
92+
value={adminOnly}
93+
>
94+
<Icon type='lock' /> {this.messages('adminOnly')}
95+
</Checkbox>
96+
}
97+
</Panel>
98+
</Body>
99+
</Media>
100+
</Col>
101+
)
102+
}
103+
}

lib/manager/components/NotesViewer.js

Lines changed: 24 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,46 @@
11
// @flow
22

3+
import Icon from '@conveyal/woonerf/components/icon'
34
import React, {Component} from 'react'
45
import moment from 'moment'
56
import gravatar from 'gravatar'
6-
import { Panel, Row, Col, Glyphicon, FormControl, Button, ButtonToolbar, Media } from 'react-bootstrap'
7+
import { Panel, Row, Col, Glyphicon, Button, ButtonToolbar, Media } from 'react-bootstrap'
78

89
import {getComponentMessages} from '../../common/util/config'
910
import { getProfileLink } from '../../common/util/util'
10-
11-
import type {Feed, Note, FeedVersion} from '../../types'
11+
import type {Feed, Note} from '../../types'
1212
import type {ManagerUserState} from '../../types/reducers'
1313

14+
import NoteForm from './NoteForm'
15+
1416
type Props = {
15-
feedSource?: Feed,
17+
feedSource: Feed,
1618
newNotePosted: ({body: string}) => void,
1719
notes: ?Array<Note>,
1820
notesRequested: () => void,
1921
stacked?: boolean,
20-
type: string,
21-
user: ManagerUserState,
22-
version?: FeedVersion
22+
user: ManagerUserState
2323
}
2424

25-
type State = {
26-
value: string
27-
}
28-
29-
export default class NotesViewer extends Component<Props, State> {
25+
export default class NotesViewer extends Component<Props> {
3026
messages = getComponentMessages('NotesViewer')
31-
state = {
32-
value: ''
33-
}
3427

3528
componentWillMount () {
3629
this.props.notesRequested()
3730
}
3831

39-
_onChangeBody = (evt: SyntheticInputEvent<HTMLInputElement>) =>
40-
this.setState({value: evt.target.value})
41-
42-
_onClickPublish = () => {
43-
this.props.newNotePosted({body: this.state.value})
44-
this.setState({value: ''})
45-
}
46-
4732
_getUserUrl = (email: string): string =>
4833
gravatar.url(email, {protocol: 'https', s: '100'})
4934

5035
render () {
5136
const {
37+
feedSource,
38+
newNotePosted,
5239
notes,
5340
notesRequested,
5441
stacked,
5542
user
5643
} = this.props
57-
const {profile} = user
58-
if (!profile) {
59-
console.warn('Could not locate user profile', user)
60-
return null
61-
}
62-
const userLink = user
63-
? <a href={getProfileLink(profile.email)}>{profile.email}</a>
64-
: 'Unknown user'
6544
const {Body, Heading, Left} = Media
6645
return (
6746
<Row>
@@ -84,7 +63,7 @@ export default class NotesViewer extends Component<Props, State> {
8463
</h3>
8564
{/* List of notes */}
8665
{notes && notes.length > 0
87-
? notes.map(note => {
66+
? notes.sort((a, b) => b.dateCreated - a.dateCreated).map(note => {
8867
const noteDate = moment(note.date)
8968
return (
9069
<Media key={note.id}>
@@ -107,6 +86,13 @@ export default class NotesViewer extends Component<Props, State> {
10786
<small title={noteDate.format('h:MMa, MMM. DD YYYY')}>
10887
commented {noteDate.fromNow()}
10988
</small>
89+
{note.adminOnly &&
90+
<span
91+
title={this.messages('adminOnly')}
92+
className='pull-right'>
93+
<Icon type='lock' />
94+
</span>
95+
}
11096
</Heading>
11197
}>
11298
<p>{note.body || '(no content)'}</p>
@@ -118,37 +104,12 @@ export default class NotesViewer extends Component<Props, State> {
118104
: <p><i>{this.messages('none')}</i></p>
119105
}
120106
</Col>
121-
{/* Post note form */}
122-
<Col xs={12} sm={stacked ? 12 : 4} md={stacked ? 12 : 6}>
123-
<h3>{this.messages('postComment')}</h3>
124-
<Media>
125-
<Left>
126-
<img
127-
alt={profile.email}
128-
width={64}
129-
height={64}
130-
src={user ? profile.picture : ''} />
131-
</Left>
132-
<Body>
133-
<Panel
134-
className='comment-panel'
135-
header={<Heading>{userLink}</Heading>}>
136-
<FormControl
137-
ref='newNoteBody'
138-
componentClass='textarea'
139-
value={this.state.value}
140-
onChange={this._onChangeBody} />
141-
<Button
142-
className='pull-right'
143-
style={{marginTop: '10px'}}
144-
disabled={this.state.value === ''}
145-
onClick={this._onClickPublish}>
146-
{this.messages('new')}
147-
</Button>
148-
</Panel>
149-
</Body>
150-
</Media>
151-
</Col>
107+
<NoteForm
108+
feedSource={feedSource}
109+
newNotePosted={newNotePosted}
110+
stacked={stacked}
111+
user={user}
112+
/>
152113
</Row>
153114
)
154115
}

lib/manager/components/version/FeedVersionViewer.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,20 @@ import { LinkContainer } from 'react-router-bootstrap'
1818
import * as versionsActions from '../../actions/versions'
1919
import {getComponentMessages, isModuleEnabled} from '../../../common/util/config'
2020
import Loading from '../../../common/components/Loading'
21-
import DeltaStat from './DeltaStat'
2221
import * as snapshotActions from '../../../editor/actions/snapshots'
23-
import FeedVersionReport from './FeedVersionReport'
2422
import * as plusActions from '../../../gtfsplus/actions/gtfsplus'
2523
import ActiveGtfsPlusVersionSummary from '../../../gtfsplus/containers/ActiveGtfsPlusVersionSummary'
26-
import VersionDateLabel from './VersionDateLabel'
2724
import NotesViewer from '../NotesViewer'
2825
import {errorPriorityLevels, getTableFatalExceptions} from '../../util/version'
2926
import GtfsValidationViewer from '../validation/GtfsValidationViewer'
30-
import VersionButtonToolbar from './VersionButtonToolbar'
31-
3227
import type {Feed, FeedVersion, GtfsPlusValidation, Note, Project} from '../../../types'
3328
import type {GtfsState, ManagerUserState} from '../../../types/reducers'
3429

30+
import VersionButtonToolbar from './VersionButtonToolbar'
31+
import VersionDateLabel from './VersionDateLabel'
32+
import FeedVersionReport from './FeedVersionReport'
33+
import DeltaStat from './DeltaStat'
34+
3535
export type Props = {
3636
comparedVersion?: FeedVersion,
3737
deleteDisabled: ?boolean,
@@ -125,6 +125,7 @@ export default class FeedVersionViewer extends Component<Props> {
125125
version={version} />
126126
: versionSection === 'comments'
127127
? <NotesViewer
128+
feedSource={feedSource}
128129
type='feed-version'
129130
stacked
130131
user={user}

lib/types/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
FETCH_FREQUENCIES,
88
RETRIEVAL_METHODS
99
} from '../common/constants'
10-
1110
import type UserPermissions from '../common/user/UserPermissions'
1211

1312
type InputType =
@@ -154,8 +153,10 @@ type DatatoolsSettings = {
154153
}
155154

156155
export type Note = {
156+
adminOnly: boolean,
157157
body: string,
158158
date: number,
159+
dateCreated: number,
159160
id: string,
160161
type: string,
161162
userEmail: string

0 commit comments

Comments
 (0)