Skip to content

Commit a102faf

Browse files
feature(protocol-designer): Add stacker step form skeleton (#20277)
1 parent a4fa526 commit a102faf

File tree

21 files changed

+678
-20
lines changed

21 files changed

+678
-20
lines changed

components/src/assets/localization/en/protocol_command_text.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"pickup_tip": "Picking up tip(s) from {{well_range}} of {{labware}} in {{labware_location}}",
9494
"prepare_to_aspirate": "Preparing {{pipette}} to aspirate",
9595
"pressurizing_to_dispense": "Pressurize pipette to dispense {{volume}} µL from resin tip at {{flow_rate}} µL/sec",
96+
"quantity": "Quantity: {{count}}",
9697
"reloading_labware": "Reloading {{labware}}",
9798
"return_tip": "Returning tip to {{well_name}} of {{labware}} in {{labware_location}}",
9899
"right": "Right",
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
div.container {
2+
width: 318px;
3+
padding: var(--spacing-16) var(--spacing-8);
4+
border-radius: var(--border-radius-4);
5+
background-color: var(--grey-20);
6+
}
7+
8+
div.sub_title {
9+
display: flex;
10+
width: 100%;
11+
flex-direction: column;
12+
color: var(--grey-60);
13+
gap: var(--spacing-8);
14+
}
15+
16+
div.label {
17+
display: flex;
18+
width: 88px;
19+
height: 24px;
20+
align-items: center;
21+
justify-content: center;
22+
border-radius: var(--border-radius-4);
23+
background-color: var(--transparent-black-80);
24+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { LabwareDetailsWithCount } from './index'
2+
3+
import type { Meta, StoryObj } from '@storybook/react'
4+
5+
const meta: Meta<typeof LabwareDetailsWithCount> = {
6+
title: 'Helix/Organisms/LabwareDetailsWithCount',
7+
component: LabwareDetailsWithCount,
8+
decorators: [
9+
Story => (
10+
<div>
11+
<Story />
12+
</div>
13+
),
14+
],
15+
}
16+
export default meta
17+
18+
type Story = StoryObj<typeof LabwareDetailsWithCount>
19+
20+
export const LabwareDetailsWithCountStory: Story = {
21+
args: {
22+
title: 'Opentrons Flex 96 Tip Rack 1000 µL',
23+
subTitle: 'With tip rack lid',
24+
quantity: 1,
25+
},
26+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useTranslation } from 'react-i18next'
2+
import { screen } from '@testing-library/react'
3+
import { beforeEach, describe, expect, it, vi } from 'vitest'
4+
5+
import { LabwareDetailsWithCount } from '..'
6+
import { renderWithProviders } from '../../../testing/utils'
7+
8+
import type { ComponentProps } from 'react'
9+
10+
vi.mock('react-i18next', () => ({
11+
useTranslation: vi.fn(),
12+
initReactI18next: vi.fn(),
13+
}))
14+
15+
vi.mock('i18next', () => {
16+
return {
17+
default: {
18+
use: () => ({ init: vi.fn() }),
19+
createInstance: () => ({
20+
use: () => ({ init: vi.fn() }),
21+
init: vi.fn(),
22+
t: (k: string) => k,
23+
}),
24+
init: vi.fn(),
25+
t: (k: string) => k,
26+
},
27+
}
28+
})
29+
const render = (props: ComponentProps<typeof LabwareDetailsWithCount>) => {
30+
return renderWithProviders(<LabwareDetailsWithCount {...props} />)
31+
}
32+
describe('LabwareDetailsWithCount', () => {
33+
let props: ComponentProps<typeof LabwareDetailsWithCount>
34+
const t = vi.fn(key => key)
35+
beforeEach(() => {
36+
props = {
37+
title: 'Title',
38+
subTitle: 'SubTitle',
39+
quantity: 1,
40+
}
41+
vi.mocked(useTranslation).mockReturnValue({ t } as any)
42+
})
43+
44+
it('should render title, subTitle and label', () => {
45+
render(props)
46+
expect(screen.getByText('Title')).toBeInTheDocument()
47+
expect(screen.getByText('SubTitle')).toBeInTheDocument()
48+
expect(screen.getByText('quantity')).toBeInTheDocument()
49+
})
50+
51+
it('should render title without subTitle and label', () => {
52+
props.subTitle = undefined
53+
props.quantity = undefined
54+
render(props)
55+
expect(screen.getByText('Title')).toBeInTheDocument()
56+
expect(screen.queryByText('SubTitle')).not.toBeInTheDocument()
57+
expect(screen.queryByText('quantity')).not.toBeInTheDocument()
58+
})
59+
})
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useTranslation } from 'react-i18next'
2+
3+
import { StyledText, Tag } from '../../atoms'
4+
import styles from './LabwareDetailsWithCount.module.css'
5+
6+
interface LabwareDetailsWithCountProps {
7+
title: string
8+
subTitle?: string
9+
quantity?: number
10+
}
11+
12+
export function LabwareDetailsWithCount({
13+
title,
14+
subTitle,
15+
quantity: label,
16+
}: LabwareDetailsWithCountProps): JSX.Element {
17+
const { t } = useTranslation('protocol_command_text')
18+
return (
19+
<div className={styles.container}>
20+
<StyledText desktopStyle="bodyDefaultRegular">{title}</StyledText>
21+
<div className={styles.subTitle}>
22+
<StyledText desktopStyle="bodyDefaultRegular">{subTitle}</StyledText>
23+
</div>
24+
{label != null ? (
25+
<div className={styles.label}>
26+
<Tag type="default" text={t('quantity', { count: label })} />
27+
</div>
28+
) : null}
29+
</div>
30+
)
31+
}

components/src/organisms/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './CommandText'
22
export * from './DeckLabelSet'
33
export * from './FixtureOption'
4+
export * from './LabwareDetailsWithCount'
45
export * from './LabwareInfoOverlay'
56
export * from './ProtocolDeck'
67
export * from './Toolbox'

protocol-designer/src/assets/localization/en/application.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,26 @@
3131
"pipettes": "Pipettes",
3232
"protocol_name": "Protocol Name",
3333
"save": "save",
34+
"select": "Select",
35+
"selected": "Selected",
36+
"source": "Source",
3437
"stepType": {
3538
"absorbanceReader": "absorbance plate reader",
3639
"camera": "camera",
3740
"comment": "comment",
3841
"ending_hold": "ending hold",
42+
"flexStacker": "flex stacker",
3943
"heaterShaker": "heater-shaker",
4044
"magnet": "magnet",
4145
"mix": "mix",
4246
"moveLabware": "move",
4347
"moveLiquid": "transfer",
4448
"pause": "pause",
45-
"profile_steps": "profile steps",
4649
"profile": "Program a Thermocycler profile",
50+
"profile_steps": "profile steps",
4751
"temperature": "temperature",
4852
"thermocycler": "thermocycler"
4953
},
50-
"select": "Select",
51-
"selected": "Selected",
52-
"source": "Source",
5354
"temperature": "Temperature (°C)",
5455
"time": "Time",
5556
"units": {
@@ -60,8 +61,8 @@
6061
"microliterPerSec": "µL/s",
6162
"millimeter": "mm",
6263
"millimeterPerSec": "mm/s",
63-
"nanometer": "nm",
6464
"minutes": "m",
65+
"nanometer": "nm",
6566
"rpm": "rpm",
6667
"seconds": "s",
6768
"seconds_long": "seconds",

protocol-designer/src/assets/localization/en/protocol_steps.json

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
"blowout_location": "Blowout location",
2626
"blowout_position": "Blowout position from top",
2727
"bottom_of_stack": "Bottom of stack",
28+
"camera": {
29+
"capture_image": "Capture image of the deck"
30+
},
2831
"captions_for_fields": {
2932
"blockTargetTemp": "Valid range between 4 and 99 °C",
3033
"blockTargetTempHold": "Valid range between 4 and 99 °C",
@@ -36,9 +39,6 @@
3639
"targetTemperature": "Valid range between 4 and 95 °C",
3740
"volume": "Recommended between 0.1 and {{max}}"
3841
},
39-
"camera": {
40-
"capture_image": "Capture image of the deck"
41-
},
4242
"change_tips": "Change tips",
4343
"column": "Column",
4444
"comfirm_reset_settings": {
@@ -67,6 +67,29 @@
6767
"edit_step": "Edit step",
6868
"ending_deck": "Ending deck",
6969
"engage_height": "Engage height",
70+
"flex_stacker": {
71+
"label": "Flex Stacker",
72+
"module_controls": {
73+
"empty_label": "Empty",
74+
"empty_sublabel": "Manually empty all labware from the stacker",
75+
"label": "Module controls",
76+
"refill_label": "Refill",
77+
"refill_sublabel": "Refill the stacker with labware. Manually fill the stacker with more labware",
78+
"retrieve_label": "Retrieve",
79+
"retrieve_sublabel": "Retrieve labware from the stacker onto the shuttle"
80+
},
81+
"shuttle": {
82+
"label": "Shuttle",
83+
"no_labware": "No labware on shuttle"
84+
},
85+
"stacker": {
86+
"label": "Stacker",
87+
"labware_filled": "{{amount}}/{{total}} labware filled",
88+
"no_labware": "No labware stored on stacker",
89+
"quantity": "Quantity: {{count}}"
90+
}
91+
},
92+
"flexStacker": "Stacker",
7093
"flow_rate_builder": "Flow rate builder",
7194
"flow_type_title": "{{type}} flow rate",
7295
"from": "from",
@@ -97,8 +120,8 @@
97120
},
98121
"heater_shaker_state": "Heater-Shaker state",
99122
"in": "in",
100-
"into": "into",
101123
"individual_wells": "Individual wells",
124+
"into": "into",
102125
"labware_in": "Labware in",
103126
"labware_to": "{{labware}} to",
104127
"liquids": "{{num}} liquids",
@@ -127,12 +150,12 @@
127150
"of": "of",
128151
"off_deck": "Off-Deck",
129152
"pause": {
153+
"forDuration": "For {{duration}}",
154+
"pausingForDuration": "<text>Pausing for</text><tag/>",
130155
"pausingUntilResume": "Pausing until manually told to resume",
131156
"pausingUntilTemperature": "<text>Pausing until</text><semiBoldText>{{module}}</semiBoldText><text>reaches</text><tag/>",
132-
"pausingForDuration": "<text>Pausing for</text><tag/>",
133157
"untilResume": "Until told to resume",
134-
"untilTemperature": "Until {{temperature}} °C reached",
135-
"forDuration": "For {{duration}}"
158+
"untilTemperature": "Until {{temperature}} °C reached"
136159
},
137160
"pipette": "Pipette",
138161
"pipette_path": "Pipette path",
@@ -198,11 +221,15 @@
198221
"block_value": "{{value}} °C",
199222
"block_value_off": "Off",
200223
"lid_label": "Lid set to",
201-
"lid_value": "{{value}} °C",
202-
"lid_value_off": "Off",
203224
"lid_position_label": "Lid position",
225+
"lid_position_value_closed": "Closed",
204226
"lid_position_value_open": "Open",
205-
"lid_position_value_closed": "Closed"
227+
"lid_value": "{{value}} °C",
228+
"lid_value_off": "Off"
229+
},
230+
"profile_timeline": {
231+
"start": "Start profile",
232+
"wait_for_complete": "Wait for profile to complete"
206233
},
207234
"repeat": "Repeat {{repetitions}} times",
208235
"substep_settings": "<text>Set block temperature to</text><tagTemperature/><text>for</text><tagDuration/>",
@@ -218,10 +245,6 @@
218245
"block": "<text>Set thermocycler block to</text><tag/>",
219246
"lid_position": "<text>Lid position</text><tag/>",
220247
"lid_temperature": "<text>Set thermocycler lid to</text><tag/>"
221-
},
222-
"profile_timeline": {
223-
"start": "Start profile",
224-
"wait_for_complete": "Wait for profile to complete"
225248
}
226249
},
227250
"time": "Time",

protocol-designer/src/form-types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export type StepType =
172172
| 'pause'
173173
| 'temperature'
174174
| 'thermocycler'
175+
| 'flexStacker'
175176

176177
export const stepIconsByType: Record<StepType, IconName> = {
177178
absorbanceReader: 'ot-absorbance',
@@ -186,6 +187,7 @@ export const stepIconsByType: Record<StepType, IconName> = {
186187
temperature: 'ot-temperature-v2',
187188
thermocycler: 'ot-thermocycler',
188189
heaterShaker: 'ot-heater-shaker',
190+
flexStacker: 'ot-flex-stacker',
189191
}
190192
// ===== Unprocessed form types =====
191193
export interface AnnotationFields {
@@ -498,6 +500,13 @@ export interface HydratedAbsorbanceReaderFormData extends AnnotationFields {
498500
wavelengths: string[]
499501
}
500502

503+
// TODO(TZ, 2025-12-03): not fully flushed out, but this is the initial hydrated form data for the flex stacker form
504+
export interface HydratedFlexStackerFormData extends AnnotationFields {
505+
stepType: 'flexStacker'
506+
id: string
507+
moduleId: string
508+
}
509+
501510
// fields used in TipPositionInput
502511
export type TipZOffsetFields =
503512
| 'aspirate_mmFromBottom'
@@ -616,3 +625,4 @@ export type HydratedFormData =
616625
| HydratedPauseFormData
617626
| HydratedTemperatureFormData
618627
| HydratedThermocyclerFormData
628+
| HydratedFlexStackerFormData

protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import {
6262
AbsorbanceReaderTools,
6363
CameraTools,
6464
CommentTools,
65+
FlexStackerToolsContainer,
6566
HeaterShakerTools,
6667
MagnetTools,
6768
MixTools,
@@ -107,6 +108,7 @@ const STEP_FORM_MAP: StepFormMap = {
107108
comment: CommentTools,
108109
camera: CameraTools,
109110
absorbanceReader: AbsorbanceReaderTools,
111+
flexStacker: FlexStackerToolsContainer,
110112
}
111113

112114
// used to inform StepFormToolbox when to prompt user confirmation for overriding advanced settings
@@ -535,7 +537,10 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
535537
desktopStyle="bodyLargeSemiBold"
536538
css={LINE_CLAMP_TEXT_STYLE(2, true)}
537539
>
538-
{capitalizeFirstLetter(String(formData.stepName))}
540+
{/* TODO: use module object from form.json instead */}
541+
{formData.stepType === 'flexStacker'
542+
? t(`protocol_steps:${formData.stepType}`)
543+
: capitalizeFirstLetter(String(formData.stepName))}
539544
</StyledText>
540545
</Flex>
541546
}

0 commit comments

Comments
 (0)