Skip to content

Commit

Permalink
feat(protocol-designer): clarify editing file details
Browse files Browse the repository at this point in the history
feat(protocol-designer): clarify editing file details

Make file metadata form clearer about when you're altering the saved information and when you can
update it by submitting. Also added in key press handler render prop component to the component
library (used here for handling submission on pressing the enter key).

Closes #1504 and #1661
  • Loading branch information
b-cooper authored Jun 15, 2018
1 parent 70cd8b2 commit d03d42f
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 68 deletions.
10 changes: 10 additions & 0 deletions protocol-designer/src/components/FilePage.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,13 @@
.file_page section {
margin: 0 2.5rem;
}

.button_row {
display: flex;
justify-content: flex-end;
}

.update_button {
width: 8rem;
align-self: flex-start;
}
56 changes: 37 additions & 19 deletions protocol-designer/src/components/FilePage.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,55 @@
// @flow
import * as React from 'react'
import {FormGroup, InputField, InstrumentGroup} from '@opentrons/components'
import type {FilePageFields} from '../file-data'
import {
FormGroup,
InputField,
InstrumentGroup,
OutlineButton
} from '@opentrons/components'
import type {FileMetadataFields} from '../file-data'
import type {FormConnector} from '../utils'

import styles from './FilePage.css'
import formStyles from '../components/forms.css'

type Props = {
formConnector: FormConnector<FilePageFields>,
instruments: React.ElementProps<typeof InstrumentGroup>
export type FilePageProps = {
formConnector: FormConnector<FileMetadataFields>,
isFormAltered: boolean,
instruments: React.ElementProps<typeof InstrumentGroup>,
saveFileMetadata: () => void
}

export default function FilePage (props: Props) {
const {formConnector, instruments} = props
const FilePage = ({formConnector, isFormAltered, instruments, saveFileMetadata}: FilePageProps) => {
const handleSubmit = () => {
// blur focused field on submit
if (document && document.activeElement) document.activeElement.blur()
saveFileMetadata()
}
return (
<div className={styles.file_page}>
<section>
<h2>
Information
</h2>
<form onSubmit={handleSubmit}>
<div className={formStyles.row_wrapper}>
<FormGroup label='Protocol Name:' className={formStyles.column_1_2}>
<InputField placeholder='Untitled' {...formConnector('name')} />
</FormGroup>

<div className={formStyles.row_wrapper}>
<FormGroup label='Protocol Name:' className={formStyles.column_1_2}>
<InputField placeholder='Untitled' {...formConnector('name')} />
</FormGroup>
<FormGroup label='Organization/Author:' className={formStyles.column_1_2}>
<InputField {...formConnector('author')} />
</FormGroup>
</div>

<FormGroup label='Organization/Author:' className={formStyles.column_1_2}>
<InputField {...formConnector('author')} />
<FormGroup label='Description:'>
<InputField {...formConnector('description')}/>
</FormGroup>
</div>

<FormGroup label='Description:'>
<InputField {...formConnector('description')}/>
</FormGroup>
<div className={styles.button_row}>
<OutlineButton type="submit" className={styles.update_button} disabled={!isFormAltered}>
{isFormAltered ? 'UPDATE' : 'UPDATED'}
</OutlineButton>
</div>
</form>
</section>

<section>
Expand All @@ -45,3 +61,5 @@ export default function FilePage (props: Props) {
</div>
)
}

export default FilePage
51 changes: 28 additions & 23 deletions protocol-designer/src/containers/ConnectedFilePage.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,60 @@
// @flow
import * as React from 'react'
import type {Dispatch} from 'redux'
import {connect} from 'react-redux'
import type {BaseState} from '../types'

import FilePage from '../components/FilePage'

import type {FilePageProps} from '../components/FilePage'
import {actions, selectors} from '../file-data'
import type {FilePageFields} from '../file-data'
import type {FileMetadataFields} from '../file-data'
import {formConnectorFactory, type FormConnector} from '../utils'

export default connect(mapStateToProps, null, mergeProps)(FilePage)

type Props = React.ElementProps<typeof FilePage>
type StateProps = {
instruments: $PropertyType<Props, 'instruments'>,
type SP = {
instruments: $PropertyType<FilePageProps, 'instruments'>,
isFormAltered: boolean,
_values: {[string]: string}
}

function mapStateToProps (state: BaseState): StateProps {
const formValues = selectors.fileFormValues(state)
const pipetteData = selectors.pipettesForInstrumentGroup(state)
type DP = {
_updateFileMetadataFields: typeof actions.updateFileMetadataFields,
_saveFileMetadata: ({[string]: string}) => void
}

const mapStateToProps = (state: BaseState): SP => {
const pipetteData = selectors.pipettesForInstrumentGroup(state)
return {
_values: formValues,
_values: selectors.fileFormValues(state),
isFormAltered: selectors.isUnsavedMetadatFormAltered(state),
instruments: {
left: pipetteData.find(i => i.mount === 'left'),
right: pipetteData.find(i => i.mount === 'right')
}
}
}

function mergeProps (
stateProps: StateProps,
dispatchProps: {dispatch: Dispatch<*>}
): Props {
const {instruments, _values} = stateProps
const {dispatch} = dispatchProps
const mapDispatchToProps = {
_updateFileMetadataFields: actions.updateFileMetadataFields,
_saveFileMetadata: actions.saveFileMetadata
}

const mergeProps = (
{instruments, isFormAltered, _values}: SP,
{_updateFileMetadataFields, _saveFileMetadata}: DP
): FilePageProps => {
const onChange = (accessor) => (e: SyntheticInputEvent<*>) => {
if (accessor === 'name' || accessor === 'description' || accessor === 'author') {
dispatch(actions.updateFileFields({[accessor]: e.target.value}))
_updateFileMetadataFields({[accessor]: e.target.value})
} else {
console.warn('Invalid accessor in ConnectedFilePage:', accessor)
}
}

const formConnector: FormConnector<FilePageFields> = formConnectorFactory(onChange, _values)
const formConnector: FormConnector<FileMetadataFields> = formConnectorFactory(onChange, _values)

return {
formConnector,
instruments
isFormAltered,
instruments,
saveFileMetadata: () => _saveFileMetadata(_values)
}
}

export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(FilePage)
2 changes: 1 addition & 1 deletion protocol-designer/src/containers/ConnectedFileSidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type MP = {
export default connect(mapStateToProps, null, mergeProps)(FileSidebar)

function mapStateToProps (state: BaseState): SP & MP {
const protocolName = fileDataSelectors.fileFormValues(state).name || 'untitled'
const protocolName = fileDataSelectors.fileMetadata(state).name || 'untitled'
const fileData = fileDataSelectors.createFile(state)

return {
Expand Down
5 changes: 3 additions & 2 deletions protocol-designer/src/containers/ConnectedNewFileModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ function mapDispatchToProps (dispatch: Dispatch<*>): DispatchProps {
return {
onCancel: () => dispatch(navigationActions.toggleNewProtocolModal(false)),
onSave: fields => {
dispatch(fileActions.updateFileFields({
name: fields.name || ''
dispatch(fileActions.saveFileMetadata({
name: fields.name || '',
description: ''
}))

dispatch(fileActions.updatePipettes({
Expand Down
6 changes: 2 additions & 4 deletions protocol-designer/src/containers/ConnectedTitleBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,19 @@ import {TitleBar, humanizeLabwareType} from '@opentrons/components'

import {selectors as labwareIngredSelectors} from '../labware-ingred/reducers'
import {selectors as steplistSelectors} from '../steplist/reducers'
import {selectors as fileDataSelectors} from '../file-data'
import {closeIngredientSelector} from '../labware-ingred/actions'

import {selectors, type Page} from '../navigation'
import type {BaseState} from '../types'

type Props = React.ElementProps<typeof TitleBar>

type DP = { onBackClick: $PropertyType<Props, 'onBackClick'> }

type SP = $Diff<Props, DP> & {_page: Page}

function mapStateToProps (state: BaseState): SP {
const _page = selectors.currentPage(state)
// TODO: Ian 2018-02-22 fileName from file
const fileName = 'Protocol Name'
const fileName = fileDataSelectors.protocolName(state)

switch (_page) {
case 'file-splash':
Expand Down
13 changes: 8 additions & 5 deletions protocol-designer/src/file-data/actions.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// @flow
import type {FilePageFieldAccessors} from './types'
import type {FileMetadataFieldAccessors} from './types'
import type {PipetteName} from './pipetteData'

// NOTE: updateFileFields contains pipette name identifiers, though pipettes aren't file fields.
// This is because both pipettes and file fields can get changed in the same place
export const updateFileFields = (payload: {[accessor: FilePageFieldAccessors]: string}) => ({
type: 'UPDATE_FILE_FIELDS',
export const updateFileMetadataFields = (payload: {[accessor: FileMetadataFieldAccessors]: string}) => ({
type: 'UPDATE_FILE_METADATA_FIELDS',
payload
})

export const saveFileMetadata = (payload: {[accessor: FileMetadataFieldAccessors]: string}) => ({
type: 'SAVE_FILE_METADATA',
payload
})

Expand Down
25 changes: 19 additions & 6 deletions protocol-designer/src/file-data/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,32 @@
import {combineReducers} from 'redux'
import {handleActions, type ActionType} from 'redux-actions'

import {updateFileFields, updatePipettes} from '../actions'
import {updateFileMetadataFields, updatePipettes, saveFileMetadata} from '../actions'
import {pipetteDataByName, type PipetteName} from '../pipetteData'

import type {Mount} from '@opentrons/components'
import type {PipetteData} from '../../step-generation'
import type {FilePageFields} from '../types'
import type {FileMetadataFields} from '../types'

const defaultFields = {
name: '',
author: '',
description: ''
}

const metadataFields = handleActions({
UPDATE_FILE_FIELDS: (state: FilePageFields, action: ActionType<typeof updateFileFields>) => ({
const unsavedMetadataForm = handleActions({
UPDATE_FILE_METADATA_FIELDS: (state: FileMetadataFields, action: ActionType<typeof updateFileMetadataFields>) => ({
...state,
...action.payload
}),
SAVE_FILE_METADATA: (state: FileMetadataFields, action: ActionType<typeof saveFileMetadata>) => ({
...state,
...action.payload
})
}, defaultFields)

const fileMetadata = handleActions({
SAVE_FILE_METADATA: (state: FileMetadataFields, action: ActionType<typeof saveFileMetadata>) => ({
...state,
...action.payload
})
Expand Down Expand Up @@ -58,12 +69,14 @@ const pipettes = handleActions({
}, {left: null, right: null})

export type RootState = {
metadataFields: FilePageFields,
unsavedMetadataForm: FileMetadataFields,
fileMetadata: FileMetadataFields,
pipettes: PipetteState
}

const _allReducers = {
metadataFields,
unsavedMetadataForm,
fileMetadata,
pipettes
}

Expand Down
10 changes: 5 additions & 5 deletions protocol-designer/src/file-data/selectors/fileCreator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ import mapValues from 'lodash/mapValues'
import type {BaseState} from '../../types'
import type {ProtocolFile, FilePipette, FileLabware} from '../types'
import type {LabwareData, PipetteData} from '../../step-generation'
import {fileFormValues} from './fileFields'
import {fileMetadata} from './fileFields'
import {getInitialRobotState, robotStateTimeline} from './commands'

// TODO LATER Ian 2018-02-28 deal with versioning
const protocolSchemaVersion = '1.0.0'
const applicationVersion = '1.0.0'

export const createFile: BaseState => ?ProtocolFile = createSelector(
fileFormValues,
fileMetadata,
getInitialRobotState,
robotStateTimeline,
(fileFormValues, initialRobotState, _robotStateTimeline) => {
const {author, description} = fileFormValues
const name = fileFormValues.name || 'untitled'
(fileMetadata, initialRobotState, _robotStateTimeline) => {
const {author, description} = fileMetadata
const name = fileMetadata.name || 'untitled'
const isValidFile = true // TODO Ian 2018-02-28 this will be its own selector

if (!isValidFile) {
Expand Down
14 changes: 13 additions & 1 deletion protocol-designer/src/file-data/selectors/fileFields.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @flow
import _ from 'lodash'
import {createSelector} from 'reselect'
import type {BaseState} from '../../types'
import type {RootState} from '../reducers'
Expand All @@ -7,5 +8,16 @@ export const rootSelector = (state: BaseState): RootState => state.fileData

export const fileFormValues = createSelector(
rootSelector,
state => state.metadataFields
state => state.unsavedMetadataForm
)

export const isUnsavedMetadatFormAltered = createSelector(
rootSelector,
state => !_.isEqual(state.unsavedMetadataForm, state.fileMetadata)
)

export const protocolName = createSelector(rootSelector, state => state.fileMetadata.name)
export const fileMetadata = createSelector(
rootSelector,
state => state.fileMetadata
)
4 changes: 2 additions & 2 deletions protocol-designer/src/file-data/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import type {DeckSlot, Mount} from '@opentrons/components'
import type {Command} from '../step-generation/types'

export type FilePageFields = {
export type FileMetadataFields = {
name: string,
author: string,
description: string
}

export type FilePageFieldAccessors = $Keys<FilePageFields>
export type FileMetadataFieldAccessors = $Keys<FileMetadataFields>

type MsSinceEpoch = number
type VersionString = string // eg '1.0.0'
Expand Down

0 comments on commit d03d42f

Please sign in to comment.