Skip to content

Commit e7f12c1

Browse files
feat(Labels): add label editor
1 parent 6379c57 commit e7f12c1

File tree

9 files changed

+264
-54
lines changed

9 files changed

+264
-54
lines changed

lib/common/components/FeedLabel.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import tinycolor from 'tinycolor2'
33
import Icon from '@conveyal/woonerf/components/icon'
44

55
import ConfirmModal from '../../common/components/ConfirmModal'
6-
import { LabelEditorModal } from '../../manager/components/LabelEditor'
6+
import LabelEditorModal from '../../manager/components/LabelEditorModal'
77

88
const getComplementaryColor = (cssHex, strength) => {
99
const color = tinycolor(cssHex)
@@ -28,31 +28,31 @@ export default class FeedLabel extends React.Component {
2828
}
2929

3030
render () {
31-
const { name, color, small, adminOnly } = this.props
31+
const {small, label} = this.props
3232

3333
return (
3434
<div
3535
className={`feedLabel ${small ? 'small' : ''}`}
3636
style={{
37-
backgroundColor: color,
38-
color: getComplementaryColor(color, 40),
39-
borderColor: getComplementaryColor(color, 10)
37+
backgroundColor: label.color,
38+
color: getComplementaryColor(label.color, 40),
39+
borderColor: getComplementaryColor(label.color, 10)
4040
}}
4141
>
4242

4343
<div className='labelName'>
44-
{adminOnly ? <Icon type='lock' /> : ''}
45-
<span>{name}</span>
44+
{label.adminOnly ? <Icon type='lock' /> : ''}
45+
<span>{label.name}</span>
4646
</div>
4747
{ small ? ''
4848
: <div className='actionButtons'>
4949
<ConfirmModal
5050
ref='deleteModal'
5151
title='Delete Label?'
52-
body={`Are you sure you want to delete the label ${name}?`}
52+
body={`Are you sure you want to delete the label ${label.name}?`}
5353
onConfirm={this._onConfirmDelete}
5454
/>
55-
<LabelEditorModal ref='editModal' label={{ ...this.props }} />
55+
<LabelEditorModal ref='editModal' label={this.props.label} />
5656

5757
<button onClick={() => this._onClickEdit()}><Icon type='pencil' /></button>
5858
<button onClick={() => this._onClickDelete()}><Icon type='trash' /></button>

lib/common/containers/FeedLabel.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import FeedLabel from '../components/FeedLabel'
77

88
const mapStateToProps = (state, ownProps) => {
99
return {
10-
label: state.label
1110
}
1211
}
1312

lib/manager/actions/labels.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ const LABEL_URL = '/api/manager/secure/label'
1313
/**
1414
* Create new label from provided properties.
1515
*/
16-
export function createLabel (newLabel: Label) {
16+
export function createLabel (label: Label) {
1717
return function (dispatch: dispatchFn, getState: getStateFn) {
18-
return dispatch(secureFetch(LABEL_URL, 'post', newLabel))
18+
return dispatch(secureFetch(LABEL_URL, 'post', label))
1919
.then((res) => res.json())
2020
.then((createdLabel) => {
21-
// TODO
21+
dispatch(fetchProject(createdLabel.projectId))
22+
dispatch(fetchProjectFeeds(createdLabel.projectId))
2223
})
2324
}
2425
}
@@ -27,9 +28,16 @@ export function createLabel (newLabel: Label) {
2728
* Update existing label with provided properties.
2829
*/
2930
export function updateLabel (label: Label, properties: {[string]: any}) {
31+
// remove keys which the server doesn't like
32+
// TODO: is there a cleaner/more dynamic way to do this? Properties can't be guaranteed to
33+
// include all the keys we need, so can't use that
34+
delete label.organizationId
35+
delete label.user
36+
3037
return function (dispatch: dispatchFn, getState: getStateFn) {
31-
return dispatch(secureFetch(`${LABEL_URL}/${label.id}`, 'put', {...label, ...properties}))
38+
return dispatch(secureFetch(`${LABEL_URL}/${label.id}`, 'put', {...label}))
3239
.then((res) => dispatch(fetchProject(label.projectId)))
40+
.then(() => dispatch(fetchProjectFeeds(label.projectId)))
3341
}
3442
}
3543

lib/manager/components/FeedSourceTableRow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ class FeedInfo extends PureComponent<{ feedSource: Feed }> {
271271
<div className='feedSourceLabelRow'>
272272
<Icon type={'tag'} />
273273
<div className='feedLabelContainer'>
274-
{feedSource.labels.map(label => <FeedLabel small key={label.id} {...label} />)}
274+
{feedSource.labels.map(label => <FeedLabel small key={label.id} label={label} />)}
275275
</div>
276276
</div>
277277
</Col>}
Lines changed: 164 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,180 @@
11
// @flow
22

33
import React from 'react'
4-
import { Modal } from 'react-bootstrap'
4+
import {
5+
Button,
6+
Col,
7+
ControlLabel,
8+
FormControl,
9+
FormGroup,
10+
HelpBlock,
11+
ListGroup,
12+
ListGroupItem,
13+
Panel,
14+
Row
15+
} from 'react-bootstrap'
16+
import update from 'react-addons-update'
517

6-
type LabelEditorProps = {
7-
label?: Label,
8-
}
18+
import {validationState} from '../util'
19+
import type {ManagerUserState} from '../../types/reducers'
920

10-
type LabelEditorState = {
11-
showModal: boolean,
12-
}
21+
// TODO: props
22+
export default class LabelEditor extends React.Component {
23+
state = {
24+
validation: {
25+
name: true
26+
},
27+
newLabel: {...this.props.label}
28+
}
1329

14-
export function LabelEditor ({label}) {
15-
return <div>{label.name}</div>
16-
}
17-
export class LabelEditorModal extends React.Component<LabelEditorProps, LabelEditorState> {
18-
state = {
19-
showModal: false
20-
}
30+
componentDidMount = () => {
31+
// If we didn't get a label passed in, declare the label to be new
32+
if (!this.props.label) {
33+
this.setState({
34+
labelIsNew: true
35+
})
36+
}
37+
}
2138

22-
close = () => {
23-
this.setState({
24-
showModal: false
25-
})
26-
}
39+
userIsAdmin = (user: ManagerUserState) => {
40+
const {permissions} = user
41+
if (!permissions) return false
42+
else return permissions.isApplicationAdmin() || permissions.canAdministerAnOrganization()
43+
}
44+
45+
_onFormChange = ({target}: {target: HTMLInputElement}) => {
46+
const {name, value, type, checked} = target
47+
const universalValue = type === 'checkbox' ? checked : value
48+
const valid = type === 'checkbox' ? true : universalValue.length > 0
49+
50+
this.setState(
51+
update(this.state, {
52+
newLabel: { $merge: {[name]: universalValue} },
53+
validation: { [name]: { $set: universalValue !== undefined && valid } }
54+
})
55+
)
56+
}
2757

28-
open = () => {
29-
this.setState({
30-
showModal: true
58+
_getChanges = () => {
59+
const newLabel = this.state.newLabel
60+
const oldLabel = this.props.label || {}
61+
const changes: any = {}
62+
Object.keys(oldLabel).map(k => {
63+
if (oldLabel[k] !== newLabel[k]) {
64+
changes[k] = oldLabel[k]
65+
}
3166
})
67+
return changes
3268
}
69+
_settingsAreUnedited = () => Object.keys(this._getChanges()).length === 0
3370

34-
ok = () => {
35-
this.close()
71+
_formIsValid = () => {
72+
const {validation} = this.state
73+
return Object.keys(validation).every(k => validation[k])
74+
}
75+
76+
_onSaveSettings = () => {
77+
const {labelIsNew, newLabel: label} = this.state
78+
const {createLabel, updateLabel} = this.props
79+
80+
if (labelIsNew) {
81+
createLabel(label)
82+
} else {
83+
updateLabel(label)
84+
}
85+
86+
// This passed method will close the editor
87+
this.props.onDone()
3688
}
3789

3890
render () {
39-
const {Body, Header, Title} = Modal
40-
const {label} = this.props
41-
return (
42-
<Modal show={this.state.showModal} onHide={this.close}>
43-
<Header>
44-
<Title>Editing {label.name ? label.name : 'New Label'}</Title>
45-
</Header>
46-
47-
<Body>
48-
<LabelEditor label={label} />
49-
</Body>
50-
</Modal>
51-
)
91+
const {newLabel, validation} = this.state
92+
const {user} = this.props
93+
94+
return <div>
95+
<Panel>
96+
97+
<ListGroup fill>
98+
<ListGroupItem>
99+
<FormGroup
100+
data-test-id='label-name'
101+
validationState={validationState(validation.name)}
102+
>
103+
104+
<ControlLabel>Name</ControlLabel>
105+
<FormControl
106+
value={newLabel.name}
107+
name={'name'}
108+
onChange={this._onFormChange}
109+
/>
110+
<FormControl.Feedback />
111+
<HelpBlock>Required.</HelpBlock>
112+
</FormGroup>
113+
</ListGroupItem>
114+
<ListGroupItem>
115+
<Row>
116+
<Col xs={3}>
117+
<FormGroup
118+
data-test-id='label-color'
119+
>
120+
<ControlLabel>Color</ControlLabel>
121+
<FormControl
122+
value={newLabel.color}
123+
name={'color'}
124+
type='color'
125+
onChange={this._onFormChange}
126+
/>
127+
<FormControl.Feedback />
128+
</FormGroup>
129+
</Col>
130+
<Col xs={9}>
131+
{this.userIsAdmin(user)
132+
? <FormGroup
133+
data-test-id='label-adminOnly'
134+
>
135+
<ControlLabel>Visible to admins only?</ControlLabel>
136+
<Row>
137+
<Col xs={3}>
138+
<FormControl
139+
checked={newLabel.adminOnly}
140+
name={'adminOnly'}
141+
type='checkbox'
142+
onChange={this._onFormChange}
143+
/>
144+
</Col>
145+
<Col xs={9}>
146+
<div>If checked, this label will only be visible to project and application admins. Users with feed-specific priveleges will not see this label applied to any feed sources.</div>
147+
</Col>
148+
</Row>
149+
</FormGroup>
150+
: ''}
151+
</Col>
152+
</Row>
153+
</ListGroupItem>
154+
</ListGroup>
155+
</Panel>
156+
<Row>
157+
<Col xs={12}>
158+
{/* Cancel button */}
159+
<Button
160+
onClick={this.props.onDone}
161+
style={{marginRight: 10}}
162+
>
163+
Cancel
164+
</Button>
165+
{/* Save button */}
166+
<Button
167+
bsStyle='primary'
168+
data-test-id='label-form-save-button'
169+
disabled={
170+
this._settingsAreUnedited() ||
171+
!this._formIsValid()
172+
}
173+
onClick={this._onSaveSettings}>
174+
Save
175+
</Button>
176+
</Col>
177+
</Row>
178+
</div>
52179
}
53180
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react'
2+
import {Modal} from 'react-bootstrap'
3+
4+
// @flow
5+
6+
import LabelEditor from '../containers/LabelEditor'
7+
import type {Label} from '../../types'
8+
9+
type LabelEditorModalProps = {
10+
label?: Label,
11+
}
12+
13+
type LabelEditorModalState = {
14+
showModal: boolean,
15+
}
16+
17+
export default class LabelEditorModal extends React.Component<LabelEditorModalProps, LabelEditorModalState> {
18+
state = {
19+
showModal: false
20+
}
21+
22+
close = () => {
23+
this.setState({
24+
showModal: false
25+
})
26+
}
27+
28+
open = () => {
29+
this.setState({
30+
showModal: true
31+
})
32+
}
33+
34+
ok = () => {
35+
this.close()
36+
}
37+
38+
render () {
39+
const {Body, Header, Title} = Modal
40+
const {label} = this.props
41+
return (
42+
<Modal show={this.state.showModal} onHide={this.close}>
43+
<Header>
44+
<Title>Editing {label.name ? 'Label' : 'New Label'}</Title>
45+
</Header>
46+
47+
<Body>
48+
<LabelEditor label={label} onDone={this.close} />
49+
</Body>
50+
</Modal>
51+
)
52+
}
53+
}

lib/manager/components/ProjectViewer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ const LabelPanel = ({ project }) => {
314314
let labelBody = 'There are no labels in this project.'
315315
if (labels.length > 0) {
316316
labelBody = labels.map((label) => (
317-
<FeedLabel key={label.id} {...label} />
317+
<FeedLabel key={label.id} label={label} />
318318
))
319319
}
320320

0 commit comments

Comments
 (0)