From aabb1b79ad580e572b2c7fcbcb6a002851d4fb93 Mon Sep 17 00:00:00 2001 From: Jethary Date: Mon, 22 Jul 2024 08:42:33 -0400 Subject: [PATCH] add groups actions and wire up --- .../ProtocolDetails/AnnotatedSteps.tsx | 9 ++- .../src/components/steplist/StepList.tsx | 61 ++++++++++++++++++- .../src/containers/ConnectedStepItem.tsx | 31 ++++++++-- .../src/file-data/selectors/fileCreator.ts | 44 +++++++++---- .../src/step-forms/actions/groups.ts | 47 ++++++++++++++ .../src/step-forms/reducers/index.ts | 52 ++++++++++++++++ .../src/step-forms/selectors/index.ts | 9 +++ .../src/ui/steps/actions/actions.ts | 17 ++++++ .../src/ui/steps/actions/types.ts | 8 +++ protocol-designer/src/ui/steps/reducers.ts | 2 +- protocol-designer/typings/reselect.d.ts | 10 ++- 11 files changed, 263 insertions(+), 27 deletions(-) create mode 100644 protocol-designer/src/step-forms/actions/groups.ts diff --git a/app/src/organisms/ProtocolDetails/AnnotatedSteps.tsx b/app/src/organisms/ProtocolDetails/AnnotatedSteps.tsx index 19236e9089a1..c19e32eb7d86 100644 --- a/app/src/organisms/ProtocolDetails/AnnotatedSteps.tsx +++ b/app/src/organisms/ProtocolDetails/AnnotatedSteps.tsx @@ -49,7 +49,7 @@ export function AnnotatedSteps(props: AnnotatedStepsProps): JSX.Element { const annotations = analysis.commandAnnotations ?? [ { annotationType: 'secondOrderCommand', - machineReadableName: 'pips and mods', + machineReadableName: 'pipettes and module load commands', params: {}, commandKeys: [ 'a1b95079-5b17-428d-b40c-a8236a9890c5', @@ -158,7 +158,12 @@ function AnnotatedGroup(props: AnnotatedGroupProps): JSX.Element { const [isExpanded, setIsExpanded] = React.useState(false) const backgroundColor = isHighlighted ? COLORS.blue30 : COLORS.grey20 return ( - setIsExpanded(!isExpanded)} cursor="pointer"> + { + setIsExpanded(!isExpanded) + }} + cursor="pointer" + > {isExpanded ? ( { const orderedStepIds = useSelector(stepFormSelectors.getOrderedStepIds) const isMultiSelectMode = useSelector(getIsMultiSelectMode) const dispatch = useDispatch>() + const [group, setGroup] = React.useState(false) + + const [groupName, setGroupName] = React.useState('') + const stepIds = useSelector(getUnsavedGroup) + + const handleCreateGroup = (): void => { + if (groupName && stepIds.length > 0) { + dispatch(createGroup({ groupName })) + dispatch(addStepToGroup({ groupName, stepIds })) + dispatch(clearGroup()) + setGroupName('') + } + } const handleKeyDown: (e: KeyboardEvent) => void = e => { const key = e.key @@ -59,8 +86,38 @@ export const StepList = (): JSX.Element => { } }, []) - return ( + return group ? ( + + + { + setGroup(false) + }} + > + close + + { + setGroupName(e.target.value) + }} + placeholder="Enter group name" + /> + + create group + + + + ) : ( + { + setGroup(true) + }} + > + make group + diff --git a/protocol-designer/src/containers/ConnectedStepItem.tsx b/protocol-designer/src/containers/ConnectedStepItem.tsx index c6d01af1a6fb..6826decfd380 100644 --- a/protocol-designer/src/containers/ConnectedStepItem.tsx +++ b/protocol-designer/src/containers/ConnectedStepItem.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useDispatch, useSelector } from 'react-redux' import uniq from 'lodash/uniq' import UAParser from 'ua-parser-js' -import { useConditionalConfirm } from '@opentrons/components' +import { Box, Btn, Icon, useConditionalConfirm } from '@opentrons/components' import { selectors as uiLabwareSelectors } from '../ui/labware' import * as timelineWarningSelectors from '../top-selectors/timelineWarnings' @@ -30,7 +30,9 @@ import { import { getAdditionalEquipmentEntities, getInitialDeckSetup, + getUnsavedGroup, } from '../step-forms/selectors' +import { selectStepForGroup } from '../step-forms/actions/groups' import type { ThunkDispatch } from 'redux-thunk' import type { @@ -73,7 +75,7 @@ export const ConnectedStepItem = ( props: ConnectedStepItemProps ): JSX.Element => { const { stepId, stepNumber } = props - + const unsavedGroup = useSelector(getUnsavedGroup) const step = useSelector(stepFormSelectors.getSavedStepForms)[stepId] const argsAndErrors = useSelector(stepFormSelectors.getArgsAndErrorsByStepId)[ stepId @@ -140,6 +142,10 @@ export const ConnectedStepItem = ( const unhighlightStep = (): HoverOnStepAction => dispatch(stepsActions.hoverOnStep(null)) + const addStep = (stepId: string): void => { + dispatch(selectStepForGroup({ stepId })) + } + const handleStepItemSelection = (event: React.MouseEvent): void => { const { isShiftKeyPressed, isMetaKeyPressed } = getMouseClickKeyInfo(event) let stepsToSelect: StepIdType[] = [] @@ -230,7 +236,8 @@ export const ConnectedStepItem = ( highlightSubstep, hoveredSubstep, } - + const name = unsavedGroup.includes(stepId) ? 'ot-checkbox' : 'minus-box' + console.log('unsavedGroup', unsavedGroup) const getModalType = (): DeleteModalType => { if (isMultiSelectMode) { return CLOSE_BATCH_EDIT_FORM @@ -249,9 +256,21 @@ export const ConnectedStepItem = ( onCancelClick={cancel} /> )} - - - + + { + addStep(stepId) + }} + > + + + + + + ) } diff --git a/protocol-designer/src/file-data/selectors/fileCreator.ts b/protocol-designer/src/file-data/selectors/fileCreator.ts index 14953db6bce0..45dc48ee69c6 100644 --- a/protocol-designer/src/file-data/selectors/fileCreator.ts +++ b/protocol-designer/src/file-data/selectors/fileCreator.ts @@ -28,6 +28,7 @@ import { DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, } from '../../constants' +import { getStepGroups } from '../../step-forms/selectors' import { getFileMetadata, getRobotType } from './fileFields' import { getInitialRobotState, getRobotStateTimeline } from './commands' @@ -57,7 +58,7 @@ import type { import type { LabwareDefByDefURI } from '../../labware-defs' import type { Selector } from '../../types' import type { DesignerApplicationData } from '../../load-file/migration/utils/getLoadLiquidCommands' -import { SecondOrderCommandAnnotation } from '@opentrons/shared-data/commandAnnotation/types' +import type { SecondOrderCommandAnnotation } from '@opentrons/shared-data/commandAnnotation/types' // TODO: BC: 2018-02-21 uncomment this assert, causes test failures // console.assert(!isEmpty(process.env.OT_PD_VERSION), 'Could not find application version!') @@ -112,6 +113,7 @@ export const createFile: Selector = createSelector( stepFormSelectors.getPipetteEntities, uiLabwareSelectors.getLabwareNicknamesById, labwareDefSelectors.getLabwareDefsByURI, + getStepGroups, ( fileMetadata, initialRobotState, @@ -126,9 +128,11 @@ export const createFile: Selector = createSelector( moduleEntities, pipetteEntities, labwareNicknamesById, - labwareDefsByURI + labwareDefsByURI, + stepGroups ) => { const { author, description, created } = fileMetadata + const name = fileMetadata.protocolName || 'untitled' const lastModified = fileMetadata.lastModified // TODO: Ian 2018-07-10 allow user to save steps in JSON file, even if those @@ -382,19 +386,33 @@ export const createFile: Selector = createSelector( commands, } - const annotationExample: SecondOrderCommandAnnotation = { - annotationType: 'secondOrderCommand', - machineReadableName: 'pips and mods', - params: {}, - commandKeys: [ - 'a1b95079-5b17-428d-b40c-a8236a9890c5', - '6f1e3ad3-8f03-4583-8031-be6be2fcd903', - '4997a543-7788-434f-8eae-1c4aa3a2a805', - ], - } + const commandAnnotations: SecondOrderCommandAnnotation[] = Object.entries( + stepGroups + ).map(([name, groupStepIds]) => { + // map stepIds from group to orderedStepIds and return indices from orderedStepIds + const stepIndices = groupStepIds + .map(groupStepId => orderedStepIds.indexOf(groupStepId)) + .filter(index => index !== -1) + + // return commands assosciated with the indices + const commands = stepIndices.flatMap( + index => robotStateTimeline.timeline[index].commands + ) + const commandKeys = commands.map(command => command.key ?? '') + + const annotation: SecondOrderCommandAnnotation = { + annotationType: 'secondOrderCommand', + machineReadableName: name, + params: {}, + commandKeys, + } + + return annotation + }) + const commandAnnotionaV1Mixin: CommandAnnotationV1Mixin = { commandAnnotationSchemaId: 'opentronsCommandAnnotationSchemaV1', - commandAnnotations: [annotationExample], + commandAnnotations, } const protocolBase: ProtocolBase = { diff --git a/protocol-designer/src/step-forms/actions/groups.ts b/protocol-designer/src/step-forms/actions/groups.ts new file mode 100644 index 000000000000..4b24fdc9eea1 --- /dev/null +++ b/protocol-designer/src/step-forms/actions/groups.ts @@ -0,0 +1,47 @@ +import { + ADD_STEPS_TO_GROUP, + CLEAR_GROUP, + CREATE_GROUP, + SELECT_STEP_FOR_GROUP, +} from '../reducers' + +export interface SaveGroupAction { + type: typeof CREATE_GROUP + payload: { groupName: string } +} +export interface AddStepToGroupAction { + type: typeof ADD_STEPS_TO_GROUP + payload: { groupName: string; stepIds: string[] } +} +export interface ClearGroupAction { + type: typeof CLEAR_GROUP +} +export interface SelectedStepForGroupAction { + type: typeof SELECT_STEP_FOR_GROUP + payload: { stepId: string } +} + +export const addStepToGroup = ( + args: AddStepToGroupAction['payload'] +): AddStepToGroupAction => ({ + type: ADD_STEPS_TO_GROUP, + payload: args, +}) + +export const createGroup = ( + args: SaveGroupAction['payload'] +): SaveGroupAction => ({ + type: CREATE_GROUP, + payload: args, +}) + +export const selectStepForGroup = ( + args: SelectedStepForGroupAction['payload'] +): SelectedStepForGroupAction => ({ + type: SELECT_STEP_FOR_GROUP, + payload: args, +}) + +export const clearGroup = (): ClearGroupAction => ({ + type: CLEAR_GROUP, +}) diff --git a/protocol-designer/src/step-forms/reducers/index.ts b/protocol-designer/src/step-forms/reducers/index.ts index 507084c0c041..c9bb54344bd5 100644 --- a/protocol-designer/src/step-forms/reducers/index.ts +++ b/protocol-designer/src/step-forms/reducers/index.ts @@ -1674,6 +1674,54 @@ export const additionalEquipmentInvariantProperties = handleActions +const initialStepGroupState = {} +const stepGroups: Reducer = handleActions< + StepGroupsState, + any +>( + { + CREATE_GROUP: (state, action) => { + return { + ...state, + [action.payload.groupName]: [], + } + }, + ADD_STEPS_TO_GROUP: (state, action) => { + return { + ...state, + [action.payload.groupName]: [ + ...state[action.payload.groupName], + ...action.payload.stepIds, + ], + } + }, + }, + initialStepGroupState +) +export type UnsavedGroupState = StepIdType[] +export const SELECT_STEP_FOR_GROUP = 'SELECT_STEP_FOR_GROUP' +export const CLEAR_GROUP = 'CLEAR_GROUP' +const initialUnsavedGroupState: StepIdType[] = [] +const unsavedGroup: Reducer = handleActions< + UnsavedGroupState, + any +>( + { + SELECT_STEP_FOR_GROUP: (state, action) => { + if (action.type === SELECT_STEP_FOR_GROUP) { + return [...state, action.payload.stepId] + } + return state + }, + CLEAR_GROUP: () => { + return [] + }, + }, + initialUnsavedGroupState +) export type OrderedStepIdsState = StepIdType[] const initialOrderedStepIdsState: string[] = [] @@ -1790,6 +1838,8 @@ export const presavedStepForm = ( } } export interface RootState { + unsavedGroup: UnsavedGroupState + stepGroups: StepGroupsState orderedStepIds: OrderedStepIdsState labwareDefs: LabwareDefsRootState labwareInvariantProperties: NormalizedLabwareById @@ -1806,6 +1856,8 @@ export interface RootState { // TODO: Ian 2018-12-13 remove this 'action: any' type export const rootReducer: Reducer = nestedCombineReducers( ({ action, state, prevStateFallback }) => ({ + unsavedGroup: unsavedGroup(prevStateFallback.unsavedGroup, action), + stepGroups: stepGroups(prevStateFallback.stepGroups, action), orderedStepIds: orderedStepIds(prevStateFallback.orderedStepIds, action), labwareInvariantProperties: labwareInvariantProperties( prevStateFallback.labwareInvariantProperties, diff --git a/protocol-designer/src/step-forms/selectors/index.ts b/protocol-designer/src/step-forms/selectors/index.ts index e67eabdcdb57..1476242a5b35 100644 --- a/protocol-designer/src/step-forms/selectors/index.ts +++ b/protocol-designer/src/step-forms/selectors/index.ts @@ -479,6 +479,15 @@ export const getModulesForEditModulesCard: Selector< } ) ) +export const getUnsavedGroup: Selector< + BaseState, + StepIdType[] +> = createSelector(rootSelector, state => state.unsavedGroup) +export const getStepGroups: Selector< + BaseState, + Record +> = createSelector(rootSelector, state => state.stepGroups) + export const getUnsavedForm: Selector< BaseState, FormData | null | undefined diff --git a/protocol-designer/src/ui/steps/actions/actions.ts b/protocol-designer/src/ui/steps/actions/actions.ts index bdd41e323aec..23fa6b64980a 100644 --- a/protocol-designer/src/ui/steps/actions/actions.ts +++ b/protocol-designer/src/ui/steps/actions/actions.ts @@ -24,6 +24,7 @@ import type { ClearWellSelectionLabwareKeyAction, SelectStepAction, SelectMultipleStepsAction, + SelectMultipleStepsForGroupAction, } from './types' // adds an incremental integer ID for Step reducers. // NOTE: if this is an "add step" directly performed by the user, @@ -135,6 +136,22 @@ export const selectMultipleSteps = ( } dispatch(selectStepAction) } +export const selectMultipleStepsForGroup = ( + stepIds: StepIdType[], + lastSelected: StepIdType +): ThunkAction => ( + dispatch: ThunkDispatch, + getState: GetState +) => { + const selectStepAction: SelectMultipleStepsForGroupAction = { + type: 'SELECT_MULTIPLE_STEPS_FOR_GROUP', + payload: { + stepIds, + lastSelected, + }, + } + dispatch(selectStepAction) +} export const selectAllSteps = (): ThunkAction< SelectMultipleStepsAction | AnalyticsEventAction > => ( diff --git a/protocol-designer/src/ui/steps/actions/types.ts b/protocol-designer/src/ui/steps/actions/types.ts index 0205c9eac52f..e6905f943be8 100644 --- a/protocol-designer/src/ui/steps/actions/types.ts +++ b/protocol-designer/src/ui/steps/actions/types.ts @@ -89,3 +89,11 @@ export interface SelectMultipleStepsAction { lastSelected: StepIdType } } + +export interface SelectMultipleStepsForGroupAction { + type: 'SELECT_MULTIPLE_STEPS_FOR_GROUP' + payload: { + stepIds: StepIdType[] + lastSelected: StepIdType + } +} diff --git a/protocol-designer/src/ui/steps/reducers.ts b/protocol-designer/src/ui/steps/reducers.ts index 6654a55b32c7..7b46ff1acedf 100644 --- a/protocol-designer/src/ui/steps/reducers.ts +++ b/protocol-designer/src/ui/steps/reducers.ts @@ -70,7 +70,7 @@ const collapsedSteps: Reducer = handleActions( (acc: CollapsedStepsState, stepId) => ({ ...acc, [stepId]: true }), {} ), - }, +}, {} ) export const SINGLE_STEP_SELECTION_TYPE: 'SINGLE_STEP_SELECTION_TYPE' = diff --git a/protocol-designer/typings/reselect.d.ts b/protocol-designer/typings/reselect.d.ts index 8a80c1fd5446..de43b405f6a2 100644 --- a/protocol-designer/typings/reselect.d.ts +++ b/protocol-designer/typings/reselect.d.ts @@ -1,6 +1,6 @@ import type { OutputSelector, Selector } from 'reselect' declare module 'reselect' { - // declaring type for createSelector with 14 selectors because the reselect types only support up to 12 selectors + // declaring type for createSelector with 15 selectors because the reselect types only support up to 12 selectors export function createSelector< S, R1, @@ -17,6 +17,7 @@ declare module 'reselect' { R12, R13, R14, + R15, T >( selector1: Selector, @@ -33,6 +34,7 @@ declare module 'reselect' { selector12: Selector, selector13: Selector, selector14: Selector, + selector15: Selector, combiner: ( res1: R1, res2: R2, @@ -47,7 +49,8 @@ declare module 'reselect' { res11: R11, res12: R12, res13: R13, - res14: R14 + res14: R14, + res15: R15 ) => T ): OutputSelector< S, @@ -66,7 +69,8 @@ declare module 'reselect' { res11: R11, res12: R12, res13: R13, - res14: R14 + res14: R14, + res15: R15 ) => T > }