Skip to content

Commit

Permalink
feat(protocol-designer): implement liquids page interactivity
Browse files Browse the repository at this point in the history
* liquids form & sidebar interactivity

Closes #2427
  • Loading branch information
IanLondon committed Oct 12, 2018
1 parent 6a0ad8a commit f831b26
Show file tree
Hide file tree
Showing 14 changed files with 236 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
lost-column: 4/16;
}

.individualize {
.serialize {
lost-column: 6/16;
}

Expand Down
14 changes: 7 additions & 7 deletions protocol-designer/src/components/IngredientPropertiesForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class IngredientPropertiesForm extends React.Component<Props, State> {
name: null,
volume: null,
description: null,
individualize: false,
serialize: false,
},
commonIngredGroupId: null,
}
Expand All @@ -127,7 +127,7 @@ class IngredientPropertiesForm extends React.Component<Props, State> {
: this.state.commonIngredGroupId,
})
},
getSubstate: (inputKey) => this.state.input[inputKey],
getSubstate: (inputKey) => this.state.input[inputKey] || null,
})
}

Expand All @@ -143,15 +143,15 @@ class IngredientPropertiesForm extends React.Component<Props, State> {
const allIngredientGroupFields = (nextIngredGroupFields || this.props.allIngredientGroupFields || {})

if (ingredGroupId && ingredGroupId in allIngredientGroupFields) {
const {name, volume, description, individualize} = this.state.input
const {name, volume, description, serialize} = this.state.input
const newIngredFields = allIngredientGroupFields[ingredGroupId]
this.setState({
...this.state,
input: {
name: newIngredFields.name || name,
volume: newIngredFields.volume || volume,
description: newIngredFields.description || description,
individualize: newIngredFields.individualize || individualize,
serialize: newIngredFields.serialize || serialize,
},
}, cb)
} else {
Expand All @@ -166,7 +166,7 @@ class IngredientPropertiesForm extends React.Component<Props, State> {
name: null,
volume: null,
description: null,
individualize: false,
serialize: false,
},
}, cb)
}
Expand Down Expand Up @@ -290,11 +290,11 @@ class IngredientPropertiesForm extends React.Component<Props, State> {

<FormGroup
label={'\u00A0'} // non-breaking space
className={styles.individualize}
className={styles.serialize}
>
<Field
label='Serialize'
accessor='individualize'
accessor='serialize'
type='checkbox'
/>
</FormGroup>
Expand Down
4 changes: 2 additions & 2 deletions protocol-designer/src/components/IngredientsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class IngredGroupCard extends React.Component<CardProps, CardState> {
groupId,
labwareWellContents,
} = this.props
const {individualize, description, name} = ingredGroup
const {serialize, description, name} = ingredGroup
const {isExpanded} = this.state

const wellsWithIngred = Object.keys(labwareWellContents)
Expand Down Expand Up @@ -91,7 +91,7 @@ class IngredGroupCard extends React.Component<CardProps, CardState> {
}

return <IngredIndividual key={well}
name={individualize
name={serialize
? `${ingredGroup.name || ''} ${i + 1}`
: ''
}
Expand Down
102 changes: 93 additions & 9 deletions protocol-designer/src/components/LiquidsPage/LiquidEditForm.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// @flow
import * as React from 'react'
import {connect} from 'react-redux'
import assert from 'assert'

import * as labwareIngredActions from '../../labware-ingred/actions'
import {selectors as labwareIngredSelectors} from '../../labware-ingred/reducers'
import type {IngredInputs} from '../../labware-ingred/types'
import type {BaseState, ThunkDispatch} from '../../types'

import {
Card,
Expand All @@ -14,40 +20,118 @@ import styles from './LiquidEditForm.css'
import formStyles from '../forms.css'

type Props = {
// TODO
...IngredInputs,
deleteLiquidGroup: () => mixed,
cancelForm: () => mixed,
saveForm: (IngredInputs) => mixed,
}
type State = IngredInputs

type WrapperProps = {showForm: boolean, formKey: string, formProps: Props}

type SP = {
...IngredInputs,
_liquidGroupId: ?string,
showForm: boolean,
}

// TODO IMMEDIATELY: internationalization of copy
class LiquidEditForm extends React.Component<Props> {
class LiquidEditForm extends React.Component<Props, State> {
constructor (props: Props) {
super(props)
this.state = {
name: props.name,
description: props.description,
serialize: props.serialize || false,
}
}

updateForm = (fieldName: $Keys<IngredInputs>) => (e: SyntheticInputEvent<*>) => {
// TODO how to handle checkbox cleanly???
if (fieldName === 'serialize') {
this.setState({[fieldName]: !this.state[fieldName]})
} else {
this.setState({[fieldName]: e.currentTarget.value})
}
}

handleSaveForm = (e: SyntheticMouseEvent<*>) => {
this.props.saveForm(this.state)
}

render () {
const {deleteLiquidGroup, cancelForm} = this.props
const {name, description, serialize} = this.state
return (
<Card className={styles.form_card}>
<section className={styles.section}>
<div className={formStyles.header}>Details</div>
<div className={formStyles.row_wrapper}>
<FormGroup label='Liquid name:' className={formStyles.column_1_2}>
<InputField />
<InputField value={name} onChange={this.updateForm('name')} />
</FormGroup>
<FormGroup label='Description:' className={formStyles.column_1_2}>
<InputField />
<InputField value={description} onChange={this.updateForm('description')} />
</FormGroup>
</div>
</section>

<section className={styles.section}>
<div className={formStyles.header}>Serialization</div>
<p className={styles.info_text}>{'Each placement of the liquid will get its own number. ("Sample 1", "Sample 2", "Sample 3")'}</p>
<CheckboxField label='Serialize' onChange={() => console.log('TODO IMMEDIATELY')} />
<CheckboxField label='Serialize' value={serialize} onChange={this.updateForm('serialize')} />
</section>

<div className={styles.button_row}>
<OutlineButton>DELETE</OutlineButton>
<PrimaryButton>CANCEL</PrimaryButton>
<PrimaryButton>SAVE</PrimaryButton>
<OutlineButton onClick={deleteLiquidGroup}>DELETE</OutlineButton>
<PrimaryButton onClick={cancelForm}>CANCEL</PrimaryButton>
<PrimaryButton onClick={this.handleSaveForm}>SAVE</PrimaryButton>
</div>
</Card>
)
}
}

export default connect()(LiquidEditForm)
function LiquidEditFormWrapper (props: WrapperProps) {
const {showForm, formKey, formProps} = props
return showForm
? <LiquidEditForm {...formProps} key={formKey} />
: null
}

function mapStateToProps (state: BaseState): SP {
const selectedLiquidGroupState = labwareIngredSelectors.getSelectedLiquidGroupState(state)
const _liquidGroupId = (selectedLiquidGroupState && selectedLiquidGroupState.liquidGroupId)
const allIngredientGroupFields = labwareIngredSelectors.allIngredientGroupFields(state)
const selectedIngredFields = _liquidGroupId ? allIngredientGroupFields[_liquidGroupId] : {}
const showForm = Boolean(selectedLiquidGroupState.liquidGroupId || selectedLiquidGroupState.newLiquidGroup)
assert(!(_liquidGroupId && !selectedIngredFields), `Expected selected liquid group "${String(_liquidGroupId)}" to have fields in allIngredientGroupFields`)

return {
_liquidGroupId,
showForm,
name: selectedIngredFields.name,
description: selectedIngredFields.description,
serialize: selectedIngredFields.serialize,
}
}

function mergeProps (stateProps: SP, dispatchProps: {dispatch: ThunkDispatch<*>}): WrapperProps {
const {dispatch} = dispatchProps
const {showForm, _liquidGroupId, ...passThruFormProps} = stateProps
return {
showForm,
formKey: _liquidGroupId || '__new_form__',
formProps: {
...passThruFormProps,
deleteLiquidGroup: () => window.alert('Deleting liquids is not yet implemented'), // TODO: Ian 2018-10-12 later ticket
cancelForm: () => dispatch(labwareIngredActions.deselectLiquidGroup()),
saveForm: (formData: IngredInputs) => dispatch(labwareIngredActions.editLiquidGroup({
...formData,
liquidGroupId: _liquidGroupId,
})),
},
}
}

export default connect(mapStateToProps, null, mergeProps)(LiquidEditFormWrapper)
29 changes: 23 additions & 6 deletions protocol-designer/src/components/LiquidsSidebar/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
// @flow
import * as React from 'react'
import {connect} from 'react-redux'
import {SidePanel, swatchColors} from '@opentrons/components'
import {
PrimaryButton,
SidePanel,
swatchColors,
} from '@opentrons/components'
import {PDTitledList} from '../lists'
import listButtonStyles from '../listButtons.css'

import {selectors as labwareIngredSelectors} from '../../labware-ingred/reducers'
import type {OrderedLiquids} from '../../labware-ingred/types'
import * as labwareIngredActions from '../../labware-ingred/actions'
import type {BaseState} from '../../types'

type Props = {
liquids: OrderedLiquids,
selectedLiquid: ?string,
handleClickLiquid: (liquidId: string) => () => mixed,
createNewLiquid: () => mixed,
selectLiquid: (liquidId: string) => mixed,
}

type SP = {
Expand All @@ -22,33 +29,43 @@ type SP = {
type DP = $Diff<Props, SP>

function LiquidsSidebar (props: Props) {
const {liquids, selectedLiquid, handleClickLiquid} = props
const {liquids, selectedLiquid, createNewLiquid, selectLiquid} = props
return (
<SidePanel title='Liquids'>
{liquids.map(({ingredientId, name}) => (
<PDTitledList
key={ingredientId}
selected={selectedLiquid === ingredientId}
onClick={handleClickLiquid(ingredientId)}
onClick={() => selectLiquid(ingredientId)}
iconName='circle'
iconProps={{style: {fill: swatchColors(Number(ingredientId))}}}
title={name || `Unnamed Ingredient ${ingredientId}`} // fallback, should not happen
/>
))}
<div className={listButtonStyles.list_item_button}>
<PrimaryButton
iconName='water'
onClick={createNewLiquid}>
New Liquid
</PrimaryButton>
</div>
</SidePanel>
)
}

function mapStateToProps (state: BaseState): SP {
const selectedLiquidGroup = labwareIngredSelectors.getSelectedLiquidGroupState(state)
return {
liquids: labwareIngredSelectors.allIngredientNamesIds(state),
selectedLiquid: '0', // TODO: Ian 2018-10-09 implement in #2427
selectedLiquid: selectedLiquidGroup && selectedLiquidGroup.liquidGroupId,
}
}

function mapDispatchToProps (dispatch: Dispatch<*>): DP {
return {
handleClickLiquid: (liquidId) => () => console.log('TODO: select liquid', liquidId), // TODO: Ian 2018-10-09 implement in #2427
selectLiquid: (liquidGroupId) =>
dispatch(labwareIngredActions.selectLiquidGroup(liquidGroupId)),
createNewLiquid: () => dispatch(labwareIngredActions.createNewLiquidGroup()),
}
}

Expand Down
4 changes: 2 additions & 2 deletions protocol-designer/src/components/StepCreationButton.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow
import * as React from 'react'
import styles from './StepCreationButton.css'
import styles from './listButtons.css'
import i18n from '../localization'

import {HoverTooltip, PrimaryButton} from '@opentrons/components'
Expand Down Expand Up @@ -38,7 +38,7 @@ function StepCreationButton (props: Props) {
)

return (
<div className={styles.step_creation_button} onMouseLeave={onClickAway}>
<div className={styles.list_item_button} onMouseLeave={onClickAway}>
<PrimaryButton
onClick={expanded ? onClickAway : onExpandClick}
>
Expand Down
4 changes: 2 additions & 2 deletions protocol-designer/src/components/WellToolTip.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type Props = {
wellContent: {
name: string,
volume: number,
individualize: boolean,
serialize: boolean,
wellName: string,

concentration?: string,
Expand All @@ -25,7 +25,7 @@ export default function WellToolTip (props: Props) {
<div>
{wellContent.wellName}
</div>
{wellContent.individualize && <div className={styles.instance_name}>
{wellContent.serialize && <div className={styles.instance_name}>
{wellContent.name || ''} {wellContent.ingredientNum}
</div>}
<div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.step_creation_button {
.list_item_button {
z-index: 5;
position: relative;
padding: 1rem 2rem 1.25rem 2rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('DELETE_INGREDIENT action', () => {
wellDetailsByLocation: null,
concentration: '50 mol/ng',
description: '',
individualize: false,
serialize: false,
},
'4': 'blah',
}
Expand Down Expand Up @@ -108,14 +108,14 @@ describe.skip('COPY_LABWARE action', () => {
wellDetailsByLocation: null,
concentration: '50 mol/ng',
description: '',
individualize: false,
serialize: false,
},
ingred4: {
name: 'Other Ingred',
wellDetailsByLocation: null,
concentration: '100%',
description: '',
individualize: false,
serialize: false,
},
}

Expand Down Expand Up @@ -168,7 +168,7 @@ describe('EDIT_INGREDIENT action', () => {
name: 'Cool Ingredient',
volume: 250,
description: 'far out!',
individualize: false,
serialize: false,
}

const resultingIngred = omit(ingredFields, ['volume'])
Expand Down Expand Up @@ -212,7 +212,7 @@ describe('EDIT_INGREDIENT action', () => {
name: 'Cool Ingredient',
volume: 250,
description: 'far out!',
individualize: false,
serialize: false,

containerId: 'container1Id',
groupId: 'newIngredId',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const baseIngredFields = {
groupId: '0',
name: 'Some Ingred',
description: null,
individualize: false,
serialize: false,
}

const allIngredientsXXSingleIngred = {
Expand Down
Loading

0 comments on commit f831b26

Please sign in to comment.