Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export function ConnectedStepInfo(props: ConnectedStepInfoProps): JSX.Element {
const hoveredStep = useSelector(getHoveredStepId)
const selectedStepId = useSelector(getSelectedStepId)
const multiSelectItemIds = useSelector(getMultiSelectItemIds)
const orderedStepIds = useSelector(stepFormSelectors.getOrderedStepIds)
const stepHierarchy = useSelector(stepFormSelectors.getSavedStepHierarchy)
const lastMultiSelectedStepId = useSelector(getMultiSelectLastSelected)
const isMultiSelectMode = useSelector(getIsMultiSelectMode)
const selected: boolean =
Expand Down Expand Up @@ -155,7 +155,7 @@ export function ConnectedStepInfo(props: ConnectedStepInfoProps): JSX.Element {
if (isShiftKeyPressed) {
stepsToSelect = getShiftSelectedSteps(
selectedStepId,
orderedStepIds,
stepHierarchy,
stepId,
multiSelectItemIds,
lastMultiSelectedStepId
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { describe, expect, it } from 'vitest'

import { capitalizeFirstLetterAfterNumber } from '../utils'
import {
capitalizeFirstLetterAfterNumber,
getShiftSelectedSteps,
} from '../utils'

import type { StepHierarchy } from '/protocol-designer/steplist/utils/stepHierarchy'

describe('capitalizeFirstLetterAfterNumber', () => {
it('should capitalize the first letter of a step type', () => {
Expand All @@ -12,3 +17,79 @@ describe('capitalizeFirstLetterAfterNumber', () => {
)
})
})

describe('getShiftSelectedSteps', () => {
it('should return a single step if no steps were selected before', () => {
const stepHierarchy: StepHierarchy = {
topLevelItems: [
{ type: 'standaloneStep', stepId: 'step1' },
{ type: 'standaloneStep', stepId: 'step2' },
{ type: 'standaloneStep', stepId: 'step3' },
],
}
expect(
getShiftSelectedSteps(null, stepHierarchy, 'step2', null, null)
).toStrictEqual(['step2'])
})
it('should return a range if a single step was selected before', () => {
const stepHierarchy: StepHierarchy = {
topLevelItems: [
{ type: 'standaloneStep', stepId: 'step1' },
{
type: 'thermocyclerProfileGroup',
thermocyclerProfileStepId: 'step2',
concurrentSteps: [
{ type: 'standaloneStep', stepId: 'step3' },
{ type: 'standaloneStep', stepId: 'step4' },
],
waitForThermocyclerProfileStepId: 'step5',
},
{ type: 'standaloneStep', stepId: 'step6' },
{ type: 'standaloneStep', stepId: 'step7' },
],
}
expect(
getShiftSelectedSteps('step3', stepHierarchy, 'step7', null, null)
).toStrictEqual([
'step3',
'step4',
// step5 should be skipped because it's hidden in the UI
'step6',
'step7',
])
})
it('should return a range if multiple steps were selected before', () => {
const stepHierarchy: StepHierarchy = {
topLevelItems: [
{ type: 'standaloneStep', stepId: 'step1' },
{
type: 'thermocyclerProfileGroup',
thermocyclerProfileStepId: 'step2',
concurrentSteps: [
{ type: 'standaloneStep', stepId: 'step3' },
{ type: 'standaloneStep', stepId: 'step4' },
],
waitForThermocyclerProfileStepId: 'step5',
},
{ type: 'standaloneStep', stepId: 'step6' },
{ type: 'standaloneStep', stepId: 'step7' },
],
}
expect(
getShiftSelectedSteps(
null,
stepHierarchy,
'step7',
['step2', 'step3'],
'step3'
)
).toStrictEqual([
'step2',
'step3',
'step4',
// step5 should be skipped because it's hidden in the UI
'step6',
'step7',
])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import round from 'lodash/round'
import uniq from 'lodash/uniq'
import { UAParser } from 'ua-parser-js'

import { getStepVisibilities } from '/protocol-designer/steplist/utils/getStepVisibilities'
import { convertStepHierarchyToArray } from '/protocol-designer/steplist/utils/stepHierarchy'

import type { MouseEvent } from 'react'
import type { StepIdType } from '/protocol-designer/form-types'
import type { StepHierarchy } from '/protocol-designer/steplist/utils/stepHierarchy'

export const capitalizeFirstLetterAfterNumber = (title: string): string =>
title.replace(
Expand Down Expand Up @@ -36,84 +40,98 @@ export const formatPercentage = (part: number, total: number): string => {
}

export const getMetaSelectedSteps = (
multiSelectItemIds: StepIdType[] | null,
stepId: StepIdType,
selectedStepId: StepIdType | null
priorMultiSelectedItemIds: StepIdType[] | null,
newlySelectedStepId: StepIdType,
priorSingleSelectedStepId: StepIdType | null
): StepIdType[] => {
Comment on lines 42 to 46
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No behavioral changes in getMetaSelectedSteps(), just renaming parameters for clarity.

let stepsToSelect: StepIdType[]
if (multiSelectItemIds?.length) {
if (priorMultiSelectedItemIds?.length) {
// already have a selection, add/remove the meta-clicked item
stepsToSelect = multiSelectItemIds.includes(stepId)
? multiSelectItemIds.filter(id => id !== stepId)
: [...multiSelectItemIds, stepId]
} else if (selectedStepId && selectedStepId === stepId) {
stepsToSelect = priorMultiSelectedItemIds.includes(newlySelectedStepId)
? priorMultiSelectedItemIds.filter(id => id !== newlySelectedStepId)
: [...priorMultiSelectedItemIds, newlySelectedStepId]
} else if (
priorSingleSelectedStepId &&
priorSingleSelectedStepId === newlySelectedStepId
) {
// meta-clicked on the selected single step
stepsToSelect = [selectedStepId]
} else if (selectedStepId) {
stepsToSelect = [priorSingleSelectedStepId]
} else if (priorSingleSelectedStepId) {
// meta-clicked on a different step, multi-select both
stepsToSelect = [selectedStepId, stepId]
stepsToSelect = [priorSingleSelectedStepId, newlySelectedStepId]
} else {
// meta-clicked on a step when a terminal item was selected
stepsToSelect = [stepId]
stepsToSelect = [newlySelectedStepId]
}
return stepsToSelect
}

export const getShiftSelectedSteps = (
selectedStepId: StepIdType | null,
orderedStepIds: StepIdType[],
stepId: StepIdType,
multiSelectItemIds: StepIdType[] | null,
priorSingleSelectedStepId: StepIdType | null,
stepHierarchy: StepHierarchy,
newlySelectedStepId: StepIdType,
priorMultiSelectedItemIds: StepIdType[] | null,
lastMultiSelectedStepId: StepIdType | null
): StepIdType[] => {
let stepsToSelect: StepIdType[]
if (selectedStepId) {
stepsToSelect = getOrderedStepsInRange(
selectedStepId,
stepId,
orderedStepIds
if (priorSingleSelectedStepId) {
stepsToSelect = getOrderedVisibleStepsInRange(
priorSingleSelectedStepId,
newlySelectedStepId,
stepHierarchy
)
} else if (multiSelectItemIds?.length && lastMultiSelectedStepId) {
const potentialStepsToSelect = getOrderedStepsInRange(
} else if (priorMultiSelectedItemIds?.length && lastMultiSelectedStepId) {
const potentialStepsToSelect = getOrderedVisibleStepsInRange(
lastMultiSelectedStepId,
stepId,
orderedStepIds
newlySelectedStepId,
stepHierarchy
)

const allSelected: boolean = potentialStepsToSelect
.slice(1)
.every(stepId => multiSelectItemIds.includes(stepId))
.every(stepId => priorMultiSelectedItemIds.includes(stepId))

if (allSelected) {
// if they're all selected, deselect them all
if (multiSelectItemIds.length - potentialStepsToSelect.length > 0) {
stepsToSelect = multiSelectItemIds.filter(
if (
priorMultiSelectedItemIds.length - potentialStepsToSelect.length >
0
) {
stepsToSelect = priorMultiSelectedItemIds.filter(
(id: StepIdType) => !potentialStepsToSelect.includes(id)
)
} else {
// unless deselecting them all results in none being selected
stepsToSelect = [potentialStepsToSelect[0]]
}
} else {
stepsToSelect = uniq([...multiSelectItemIds, ...potentialStepsToSelect])
stepsToSelect = uniq([
...priorMultiSelectedItemIds,
...potentialStepsToSelect,
])
}
} else {
stepsToSelect = [stepId]
stepsToSelect = [newlySelectedStepId]
}
return stepsToSelect
}

const getOrderedStepsInRange = (
const getOrderedVisibleStepsInRange = (
lastSelectedStepId: StepIdType,
stepId: StepIdType,
orderedStepIds: StepIdType[]
stepHierarchy: StepHierarchy
): StepIdType[] => {
const orderedStepIds = convertStepHierarchyToArray(stepHierarchy)
const stepVisibilities = getStepVisibilities(stepHierarchy)

const prevIndex: number = orderedStepIds.indexOf(lastSelectedStepId)
const currentIndex: number = orderedStepIds.indexOf(stepId)

const [startIndex, endIndex] = [prevIndex, currentIndex].sort((a, b) => a - b)
const orderedSteps = orderedStepIds.slice(startIndex, endIndex + 1)
return orderedSteps

const orderedVisibleSteps = orderedStepIds
.slice(startIndex, endIndex + 1)
.filter(stepId => stepVisibilities[stepId].isVisibleToUser)
return orderedVisibleSteps
Comment on lines +131 to +134
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new .filter() is the actual behavioral change of this PR.

}

export const nonePressed = (keysPressed: boolean[]): boolean =>
Expand Down
37 changes: 18 additions & 19 deletions protocol-designer/src/ui/steps/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,18 @@ import type {
selectDropdownItemAction,
Selection,
SelectMultipleStepsAction,
SelectMultipleStepsForGroupAction,
SelectStepAction,
SelectTerminalItemAction,
SetWellSelectionLabwareKeyAction,
ToggleViewSubstepAction,
ViewSubstep,
} from './types'

// adds an incremental integer ID for Step reducers.
// NOTE: if this is an "add step" directly performed by the user,
// addAndSelectStepWithHints is probably what you want
/**
* adds an incremental integer ID for Step reducers.
* NOTE: if this is an "add step" directly performed by the user,
* addAndSelectStepWithHints is probably what you want
*/
export const addStep = (args: {
stepType: StepType
robotStateTimeline: Timeline
Expand All @@ -54,10 +55,12 @@ export const addStep = (args: {
},
}
}

export const hoverSelection = (args: Selection): hoverSelectionAction => ({
type: 'HOVER_DROPDOWN_ITEM',
payload: { id: args.id, text: args.text },
})

export const selectDropdownItem = (args: {
selection: Selection | null
mode: Mode
Expand All @@ -82,35 +85,41 @@ export const hoverOnSubstep = (
type: 'HOVER_ON_SUBSTEP',
payload: payload,
})

export const selectTerminalItem = (
terminalId: TerminalItemId
): SelectTerminalItemAction => ({
type: 'SELECT_TERMINAL_ITEM',
payload: terminalId,
})

export const hoverOnStep = (
stepId: StepIdType | null | undefined
): HoverOnStepAction => ({
type: 'HOVER_ON_STEP',
payload: stepId,
})

export const hoverOnTerminalItem = (
terminalId: TerminalItemId | null | undefined
): HoverOnTerminalItemAction => ({
type: 'HOVER_ON_TERMINAL_ITEM',
payload: terminalId,
})

export const setWellSelectionLabwareKey = (
labwareName: string | null | undefined
): SetWellSelectionLabwareKeyAction => ({
type: 'SET_WELL_SELECTION_LABWARE_KEY',
payload: labwareName,
})

export const clearWellSelectionLabwareKey =
(): ClearWellSelectionLabwareKeyAction => ({
type: 'CLEAR_WELL_SELECTION_LABWARE_KEY',
payload: null,
})

export const resetSelectStep =
(stepId: StepIdType): ThunkAction<any> =>
(dispatch: ThunkDispatch<any>, getState: GetState) => {
Expand Down Expand Up @@ -227,6 +236,7 @@ export const populateForm =
setSelection(formData, dispatch)
resetScrollElements()
}

export const selectStep =
(stepId: StepIdType): ThunkAction<any> =>
(dispatch: ThunkDispatch<any>, getState: GetState) => {
Expand All @@ -243,6 +253,7 @@ export const selectStep =
})
setSelection(formData, dispatch)
}

// NOTE(sa, 2020-12-11): this is a thunk so that we can populate the batch edit form with things later
export const selectMultipleSteps =
(
Expand All @@ -259,21 +270,7 @@ export const selectMultipleSteps =
}
dispatch(selectStepAction)
}
export const selectMultipleStepsForGroup =
(
stepIds: StepIdType[],
lastSelected: StepIdType
): ThunkAction<SelectMultipleStepsForGroupAction> =>
(dispatch: ThunkDispatch<any>, getState: GetState) => {
const selectStepAction: SelectMultipleStepsForGroupAction = {
type: 'SELECT_MULTIPLE_STEPS_FOR_GROUP',
payload: {
stepIds,
lastSelected,
},
}
dispatch(selectStepAction)
}

export const selectAllSteps =
(): ThunkAction<SelectMultipleStepsAction | AnalyticsEventAction> =>
(
Expand All @@ -300,8 +297,10 @@ export const selectAllSteps =
dispatch(analyticsEvent(selectAllStepsEvent))
}
}

export const EXIT_BATCH_EDIT_MODE_BUTTON_PRESS: 'EXIT_BATCH_EDIT_MODE_BUTTON_PRESS' =
'EXIT_BATCH_EDIT_MODE_BUTTON_PRESS'

// todo(mm, 2025-10-31): "deselectAllSteps" is a bit of a misnomer, since this also selects a step.
export const deselectAllSteps =
(
Expand Down
7 changes: 0 additions & 7 deletions protocol-designer/src/ui/steps/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,6 @@ export interface SelectMultipleStepsAction {
}
}

export interface SelectMultipleStepsForGroupAction {
type: 'SELECT_MULTIPLE_STEPS_FOR_GROUP'
payload: {
stepIds: StepIdType[]
lastSelected: StepIdType
}
}
export type ViewSubstep = StepIdType | null
export interface ToggleViewSubstepAction {
type: 'TOGGLE_VIEW_SUBSTEP'
Expand Down
Loading