diff --git a/protocol-designer/src/components/IngredientPropertiesForm.css b/protocol-designer/src/components/IngredientPropertiesForm.css index b9839a95144..8456bb24802 100644 --- a/protocol-designer/src/components/IngredientPropertiesForm.css +++ b/protocol-designer/src/components/IngredientPropertiesForm.css @@ -18,7 +18,7 @@ lost-column: 4/16; } -.individualize { +.serialize { lost-column: 6/16; } diff --git a/protocol-designer/src/components/IngredientPropertiesForm.js b/protocol-designer/src/components/IngredientPropertiesForm.js index 6da9eae778d..303fe98237f 100644 --- a/protocol-designer/src/components/IngredientPropertiesForm.js +++ b/protocol-designer/src/components/IngredientPropertiesForm.js @@ -112,7 +112,7 @@ class IngredientPropertiesForm extends React.Component { name: null, volume: null, description: null, - individualize: false, + serialize: false, }, commonIngredGroupId: null, } @@ -127,7 +127,7 @@ class IngredientPropertiesForm extends React.Component { : this.state.commonIngredGroupId, }) }, - getSubstate: (inputKey) => this.state.input[inputKey], + getSubstate: (inputKey) => this.state.input[inputKey] || null, }) } @@ -143,7 +143,7 @@ class IngredientPropertiesForm extends React.Component { 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, @@ -151,7 +151,7 @@ class IngredientPropertiesForm extends React.Component { name: newIngredFields.name || name, volume: newIngredFields.volume || volume, description: newIngredFields.description || description, - individualize: newIngredFields.individualize || individualize, + serialize: newIngredFields.serialize || serialize, }, }, cb) } else { @@ -166,7 +166,7 @@ class IngredientPropertiesForm extends React.Component { name: null, volume: null, description: null, - individualize: false, + serialize: false, }, }, cb) } @@ -290,11 +290,11 @@ class IngredientPropertiesForm extends React.Component { diff --git a/protocol-designer/src/components/IngredientsList.js b/protocol-designer/src/components/IngredientsList.js index ba988d65a13..217ae114962 100644 --- a/protocol-designer/src/components/IngredientsList.js +++ b/protocol-designer/src/components/IngredientsList.js @@ -50,7 +50,7 @@ class IngredGroupCard extends React.Component { groupId, labwareWellContents, } = this.props - const {individualize, description, name} = ingredGroup + const {serialize, description, name} = ingredGroup const {isExpanded} = this.state const wellsWithIngred = Object.keys(labwareWellContents) @@ -91,7 +91,7 @@ class IngredGroupCard extends React.Component { } return 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 { +class LiquidEditForm extends React.Component { + constructor (props: Props) { + super(props) + this.state = { + name: props.name, + description: props.description, + serialize: props.serialize || false, + } + } + + updateForm = (fieldName: $Keys) => (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 (
Details
- + - +
@@ -37,17 +79,59 @@ class LiquidEditForm extends React.Component {
Serialization

{'Each placement of the liquid will get its own number. ("Sample 1", "Sample 2", "Sample 3")'}

- console.log('TODO IMMEDIATELY')} /> +
- DELETE - CANCEL - SAVE + DELETE + CANCEL + SAVE
) } } -export default connect()(LiquidEditForm) +function LiquidEditFormWrapper (props: WrapperProps) { + const {showForm, formKey, formProps} = props + return showForm + ? + : 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) diff --git a/protocol-designer/src/components/LiquidsSidebar/index.js b/protocol-designer/src/components/LiquidsSidebar/index.js index 3cc598e9282..1cff751f5d0 100644 --- a/protocol-designer/src/components/LiquidsSidebar/index.js +++ b/protocol-designer/src/components/LiquidsSidebar/index.js @@ -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 = { @@ -22,33 +29,43 @@ type SP = { type DP = $Diff function LiquidsSidebar (props: Props) { - const {liquids, selectedLiquid, handleClickLiquid} = props + const {liquids, selectedLiquid, createNewLiquid, selectLiquid} = props return ( {liquids.map(({ingredientId, name}) => ( selectLiquid(ingredientId)} iconName='circle' iconProps={{style: {fill: swatchColors(Number(ingredientId))}}} title={name || `Unnamed Ingredient ${ingredientId}`} // fallback, should not happen /> ))} +
+ + New Liquid + +
) } 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()), } } diff --git a/protocol-designer/src/components/StepCreationButton.js b/protocol-designer/src/components/StepCreationButton.js index 5d9fd68a9a6..f78767d1fb6 100644 --- a/protocol-designer/src/components/StepCreationButton.js +++ b/protocol-designer/src/components/StepCreationButton.js @@ -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' @@ -38,7 +38,7 @@ function StepCreationButton (props: Props) { ) return ( -
+
diff --git a/protocol-designer/src/components/WellToolTip.js b/protocol-designer/src/components/WellToolTip.js index 67b0e8ed88e..e165bf4511a 100644 --- a/protocol-designer/src/components/WellToolTip.js +++ b/protocol-designer/src/components/WellToolTip.js @@ -7,7 +7,7 @@ type Props = { wellContent: { name: string, volume: number, - individualize: boolean, + serialize: boolean, wellName: string, concentration?: string, @@ -25,7 +25,7 @@ export default function WellToolTip (props: Props) {
{wellContent.wellName}
- {wellContent.individualize &&
+ {wellContent.serialize &&
{wellContent.name || ''} {wellContent.ingredientNum}
}
diff --git a/protocol-designer/src/components/StepCreationButton.css b/protocol-designer/src/components/listButtons.css similarity index 85% rename from protocol-designer/src/components/StepCreationButton.css rename to protocol-designer/src/components/listButtons.css index c09ec01aee0..e66c519f975 100644 --- a/protocol-designer/src/components/StepCreationButton.css +++ b/protocol-designer/src/components/listButtons.css @@ -1,4 +1,4 @@ -.step_creation_button { +.list_item_button { z-index: 5; position: relative; padding: 1rem 2rem 1.25rem 2rem; diff --git a/protocol-designer/src/labware-ingred/__tests__/ingredients.test.js b/protocol-designer/src/labware-ingred/__tests__/ingredients.test.js index ceef52610b9..b32ebbb2d9e 100644 --- a/protocol-designer/src/labware-ingred/__tests__/ingredients.test.js +++ b/protocol-designer/src/labware-ingred/__tests__/ingredients.test.js @@ -32,7 +32,7 @@ describe('DELETE_INGREDIENT action', () => { wellDetailsByLocation: null, concentration: '50 mol/ng', description: '', - individualize: false, + serialize: false, }, '4': 'blah', } @@ -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, }, } @@ -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']) @@ -212,7 +212,7 @@ describe('EDIT_INGREDIENT action', () => { name: 'Cool Ingredient', volume: 250, description: 'far out!', - individualize: false, + serialize: false, containerId: 'container1Id', groupId: 'newIngredId', diff --git a/protocol-designer/src/labware-ingred/__tests__/selectors.test.js b/protocol-designer/src/labware-ingred/__tests__/selectors.test.js index 53d7990e7d5..d5ff5c55371 100644 --- a/protocol-designer/src/labware-ingred/__tests__/selectors.test.js +++ b/protocol-designer/src/labware-ingred/__tests__/selectors.test.js @@ -6,7 +6,7 @@ const baseIngredFields = { groupId: '0', name: 'Some Ingred', description: null, - individualize: false, + serialize: false, } const allIngredientsXXSingleIngred = { diff --git a/protocol-designer/src/labware-ingred/actions.js b/protocol-designer/src/labware-ingred/actions.js index f3408968246..81b047863f9 100644 --- a/protocol-designer/src/labware-ingred/actions.js +++ b/protocol-designer/src/labware-ingred/actions.js @@ -1,7 +1,6 @@ // @flow import {createAction} from 'redux-actions' import type {Dispatch} from 'redux' -import max from 'lodash/max' import {selectors} from './reducers' import wellSelectionSelectors from '../well-selection/selectors' @@ -166,10 +165,10 @@ export type EditIngredientPayload = { groupId: string | null, // null indicates new ingredient is being created } +// TODO: Ian 2018-10-12 this is deprecated, remove when "add liquids to deck" modal is redone export const editIngredient = (payload: EditIngredientPayload) => (dispatch: Dispatch, getState: GetState) => { const state = getState() const container = selectors.getSelectedContainer(state) - const allIngredients = selectors.getIngredientGroups(state) const {groupId, ...inputFields} = payload @@ -190,17 +189,59 @@ export const editIngredient = (payload: EditIngredientPayload) => (dispatch: Dis }) } - // TODO: Ian 2018-02-19 make selector - const nextGroupId: string = ((max(Object.keys(allIngredients).map(id => parseInt(id))) + 1) || 0).toString() - return dispatch({ type: 'EDIT_INGREDIENT', payload: { ...inputFields, // if it matches the name of the clone parent, append "copy" to that name containerId: container.id, - groupId: nextGroupId, + groupId: selectors.getNextLiquidGroupId(state), wells: wellSelectionSelectors.selectedWellNames(state), }, }) } + +export type SelectLiquidAction = { + type: 'SELECT_LIQUID_GROUP', + payload: string, +} + +export function selectLiquidGroup (liquidGroupId: string): SelectLiquidAction { + return { + type: 'SELECT_LIQUID_GROUP', + payload: liquidGroupId, + } +} + +export function deselectLiquidGroup () { + return {type: 'DESELECT_LIQUID_GROUP'} +} + +export function createNewLiquidGroup () { + return {type: 'CREATE_NEW_LIQUID_GROUP_FORM'} +} + +export type EditLiquidGroupAction = { + type: 'EDIT_LIQUID_GROUP', + payload: { + liquidGroupId: string, + ...IngredInputFields, + }, +} + +// NOTE: with no ID, a new one is assigned +export const editLiquidGroup = ( + args: {liquidGroupId: ?string, ...IngredInputFields} +) => (dispatch: Dispatch, getState: GetState +) => { + // TODO: Ian 2018-10-12 flow doesn't understand unpacking in: {...args, liquidGroupId: args.id || 'blahId'} + dispatch({ + type: 'EDIT_LIQUID_GROUP', + payload: { + name: args.name, + serialize: args.serialize, + description: args.description, + liquidGroupId: args.liquidGroupId || selectors.getNextLiquidGroupId(getState()), + }, + }) +} diff --git a/protocol-designer/src/labware-ingred/reducers/index.js b/protocol-designer/src/labware-ingred/reducers/index.js index f8b3f43ec37..d1b68f0a837 100644 --- a/protocol-designer/src/labware-ingred/reducers/index.js +++ b/protocol-designer/src/labware-ingred/reducers/index.js @@ -6,6 +6,7 @@ import {createSelector} from 'reselect' import omit from 'lodash/omit' import mapValues from 'lodash/mapValues' +import max from 'lodash/max' import pickBy from 'lodash/pickBy' import reduce from 'lodash/reduce' import isEmpty from 'lodash/isEmpty' @@ -31,7 +32,13 @@ import * as actions from '../actions' import {getPDMetadata} from '../../file-types' import type {BaseState, Selector, Options} from '../../types' import type {LoadFileAction} from '../../load-file' -import type {MoveLabware, DeleteIngredient, EditIngredient} from '../actions' +import type { + DeleteIngredient, + EditIngredient, + EditLiquidGroupAction, + MoveLabware, + SelectLiquidAction, +} from '../actions' // external actions (for types) import typeof {openWellSelectionModal} from '../../well-selection/actions' @@ -92,6 +99,20 @@ type ContainersState = { [id: string]: ?Labware, } +export type SelectedLiquidGroupState = {liquidGroupId: ?string, newLiquidGroup?: true} +const unselectedLiquidGroupState = {liquidGroupId: null} +// This is only a concern of the liquid page. +// null = nothing selected, newLiquidGroup: true means user is creating new liquid +const selectedLiquidGroup = handleActions({ + SELECT_LIQUID_GROUP: (state: SelectedLiquidGroupState, action: SelectLiquidAction): SelectedLiquidGroupState => + ({liquidGroupId: action.payload}), + DESELECT_LIQUID_GROUP: () => unselectedLiquidGroupState, + CREATE_NEW_LIQUID_GROUP_FORM: (): SelectedLiquidGroupState => + ({liquidGroupId: null, newLiquidGroup: true}), + NAVIGATE_TO_PAGE: () => unselectedLiquidGroupState, // clear selection on navigate + EDIT_LIQUID_GROUP: () => unselectedLiquidGroupState, // clear on form save +}, unselectedLiquidGroupState) + const initialLabwareState: ContainersState = { [FIXED_TRASH_ID]: { id: FIXED_TRASH_ID, @@ -192,11 +213,19 @@ export const savedLabware = handleActions({ type IngredientsState = IngredientGroups export const ingredients = handleActions({ + EDIT_LIQUID_GROUP: (state: IngredientsState, action: EditLiquidGroupAction): IngredientsState => { + const {liquidGroupId} = action.payload + return { + ...state, + [liquidGroupId]: {...state[liquidGroupId], ...action.payload}, + } + }, EDIT_INGREDIENT: (state, action: EditIngredient) => { - const {groupId, description, individualize, name} = action.payload + // TODO: Ian 2018-10-12 this is deprecated, remove when "add liquids to deck" modal is redone + const {groupId, description, serialize, name} = action.payload const ingredFields: IngredientInstance = { description, - individualize, + serialize, name, } @@ -274,6 +303,7 @@ export type RootState = {| drillDownLabwareId: DrillDownLabwareId, containers: ContainersState, savedLabware: SavedLabwareState, + selectedLiquidGroup: SelectedLiquidGroupState, ingredients: IngredientsState, ingredLocations: LocationsState, renameLabwareFormMode: RenameLabwareFormModeState, @@ -284,6 +314,7 @@ const rootReducer = combineReducers({ modeLabwareSelection, moveLabwareMode, selectedContainerId, + selectedLiquidGroup, drillDownLabwareId, containers, savedLabware, @@ -318,6 +349,11 @@ const getLabwareTypes: Selector = createSelector( const getIngredientGroups = (state: BaseState) => rootSelector(state).ingredients const getIngredientLocations = (state: BaseState) => rootSelector(state).ingredLocations +const getNextLiquidGroupId: Selector = createSelector( + getIngredientGroups, + (_ingredGroups) => ((max(Object.keys(_ingredGroups).map(id => parseInt(id))) + 1) || 0).toString() +) + const getIngredientNames: Selector<{[ingredId: string]: string}> = createSelector( getIngredientGroups, ingredGroups => mapValues(ingredGroups, (ingred: IngredientInstance) => ingred.name) @@ -382,6 +418,11 @@ const getSelectedContainerId: Selector = createSelector( rootState => rootState.selectedContainerId ) +const getSelectedLiquidGroupState: Selector = createSelector( + rootSelector, + rootState => rootState.selectedLiquidGroup +) + const getSelectedContainer: Selector = createSelector( getSelectedContainerId, getLabware, @@ -472,9 +513,11 @@ export const selectors = { getLabware, getLabwareNames, getLabwareTypes, + getNextLiquidGroupId, getSavedLabware, getSelectedContainer, getSelectedContainerId, + getSelectedLiquidGroupState, getDrillDownLabwareId, activeModals, diff --git a/protocol-designer/src/labware-ingred/types.js b/protocol-designer/src/labware-ingred/types.js index 4de8f67b6ef..99cea829314 100644 --- a/protocol-designer/src/labware-ingred/types.js +++ b/protocol-designer/src/labware-ingred/types.js @@ -44,11 +44,11 @@ export type OrderedLiquids = Array<{ name: ?string, }> -export type IngredInputs = { - name: string | null, - volume: number | null, - description: string | null, - individualize: boolean, +export type IngredInputs = { // TODO: Ian 2018-10-12 rename to 'LiquidFormFields' + name: ?string, + volume?: ?number, + description: ?string, + serialize: boolean, } export type IngredInputFields = $Exact diff --git a/protocol-designer/src/top-selectors/well-contents/__tests__/wellContentsAllLabware.test.js b/protocol-designer/src/top-selectors/well-contents/__tests__/wellContentsAllLabware.test.js index 3dc9dfe06f6..a7a54fa4fae 100644 --- a/protocol-designer/src/top-selectors/well-contents/__tests__/wellContentsAllLabware.test.js +++ b/protocol-designer/src/top-selectors/well-contents/__tests__/wellContentsAllLabware.test.js @@ -5,7 +5,7 @@ const baseIngredFields = { groupId: '0', name: 'Some Ingred', description: null, - individualize: false, + serialize: false, } const containerState = {