Skip to content

Commit 0da8f98

Browse files
vabruzzoVincent Abruzzokoji
authored
fix(app, app-shell): fix python override path select on windows (#11257)
Re: RAUT-61 Co-authored-by: Vincent Abruzzo <[email protected]> Co-authored-by: koji <[email protected]>
1 parent 503dc95 commit 0da8f98

File tree

14 files changed

+195
-97
lines changed

14 files changed

+195
-97
lines changed

app-shell/src/config/index.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import Store from 'electron-store'
44
import get from 'lodash/get'
55
import mergeOptions from 'merge-options'
6-
import { shell } from 'electron'
76
import yargsParser from 'yargs-parser'
87

98
import { UI_INITIALIZED } from '@opentrons/app/src/redux/shell/actions'
@@ -119,17 +118,3 @@ export function handleConfigChange(
119118
): void {
120119
store().onDidChange(path, changeHandler)
121120
}
122-
123-
export function registerPythonPath(): Dispatch {
124-
return function handleActionForPython(action: Action) {
125-
switch (action.type) {
126-
case Cfg.OPEN_PYTHON_DIRECTORY: {
127-
const dir = getFullConfig().python.pathToPythonOverride
128-
if (dir != null) {
129-
shell.openPath(dir)
130-
}
131-
break
132-
}
133-
}
134-
}
135-
}

app-shell/src/dialogs/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { dialog } from 'electron'
1+
import { dialog, shell } from 'electron'
22
import type {
33
BrowserWindow,
44
OpenDialogOptions,
@@ -61,3 +61,10 @@ export function showOpenFileDialog(
6161
return result.canceled ? [] : (result.filePaths as string[])
6262
})
6363
}
64+
65+
export function openDirectoryInFileExplorer(
66+
directory: string | null
67+
): Promise<string | null> {
68+
if (directory == null) return Promise.resolve(null)
69+
return shell.openPath(directory)
70+
}

app-shell/src/main.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,16 @@ import contextMenu from 'electron-context-menu'
44

55
import { createUi } from './ui'
66
import { initializeMenu } from './menu'
7-
import { initializePython } from './protocol-analysis'
87
import { createLogger } from './log'
8+
import { registerProtocolAnalysis } from './protocol-analysis'
99
import { registerDiscovery } from './discovery'
1010
import { registerLabware } from './labware'
1111
import { registerRobotLogs } from './robot-logs'
1212
import { registerUpdate } from './update'
1313
import { registerBuildrootUpdate } from './buildroot'
1414
import { registerSystemInfo } from './system-info'
1515
import { registerProtocolStorage } from './protocol-storage'
16-
import {
17-
getConfig,
18-
getStore,
19-
getOverrides,
20-
registerConfig,
21-
registerPythonPath,
22-
} from './config'
16+
import { getConfig, getStore, getOverrides, registerConfig } from './config'
2317

2418
import type { BrowserWindow } from 'electron'
2519
import type { Dispatch, Logger } from './types'
@@ -76,7 +70,6 @@ function startUp(): void {
7670
})
7771

7872
initializeMenu()
79-
initializePython()
8073

8174
// wire modules to UI dispatches
8275
const dispatch: Dispatch = action => {
@@ -90,11 +83,11 @@ function startUp(): void {
9083
const actionHandlers: Dispatch[] = [
9184
registerConfig(dispatch),
9285
registerDiscovery(dispatch),
86+
registerProtocolAnalysis(dispatch, mainWindow),
9387
registerRobotLogs(dispatch, mainWindow),
9488
registerUpdate(dispatch),
9589
registerBuildrootUpdate(dispatch),
9690
registerLabware(dispatch, mainWindow),
97-
registerPythonPath(),
9891
registerSystemInfo(dispatch),
9992
registerProtocolStorage(dispatch),
10093
]

app-shell/src/protocol-analysis/__tests__/protocolAnalysis.test.ts

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
import { when, resetAllWhenMocks } from 'jest-when'
2+
import electron from 'electron'
3+
import * as ProtocolAnalysis from '@opentrons/app/src/redux/protocol-analysis'
4+
import * as Cfg from '@opentrons/app/src/redux/config'
25

6+
import * as Dialogs from '../../dialogs'
37
import { Config, getConfig, handleConfigChange } from '../../config'
48
import { getValidLabwareFilePaths } from '../../labware'
59
import { selectPythonPath, getPythonPath } from '../getPythonPath'
610
import { executeAnalyzeCli } from '../executeAnalyzeCli'
711
import { writeFailedAnalysis } from '../writeFailedAnalysis'
812

9-
import { initializePython, analyzeProtocolSource } from '..'
13+
import {
14+
registerProtocolAnalysis,
15+
analyzeProtocolSource,
16+
CONFIG_PYTHON_PATH_TO_PYTHON_OVERRIDE,
17+
} from '..'
18+
import { Dispatch } from '../../types'
1019

1120
jest.mock('../../labware')
21+
jest.mock('../../dialogs')
1222
jest.mock('../getPythonPath')
1323
jest.mock('../executeAnalyzeCli')
1424
jest.mock('../writeFailedAnalysis')
@@ -32,21 +42,37 @@ const mockGetValidLabwareFilePaths = getValidLabwareFilePaths as jest.MockedFunc
3242
const mockHandleConfigChange = handleConfigChange as jest.MockedFunction<
3343
typeof handleConfigChange
3444
>
45+
const mockShowOpenDirectoryDialog = Dialogs.showOpenDirectoryDialog as jest.MockedFunction<
46+
typeof Dialogs.showOpenDirectoryDialog
47+
>
48+
const mockOpenDirectoryInFileExplorer = Dialogs.openDirectoryInFileExplorer as jest.MockedFunction<
49+
typeof Dialogs.openDirectoryInFileExplorer
50+
>
51+
52+
// wait a few ticks to let the mock Promises clear
53+
const flush = (): Promise<void> =>
54+
new Promise(resolve => setTimeout(resolve, 0))
3555

3656
describe('analyzeProtocolSource', () => {
57+
const mockMainWindow = ({
58+
browserWindow: true,
59+
} as unknown) as electron.BrowserWindow
60+
let dispatch: jest.MockedFunction<Dispatch>
61+
let handleAction: Dispatch
62+
63+
beforeEach(() => {
64+
dispatch = jest.fn()
65+
mockGetConfig.mockReturnValue({
66+
python: { pathToPythonOverride: '/some/override/python' },
67+
} as Config)
68+
handleAction = registerProtocolAnalysis(dispatch, mockMainWindow)
69+
})
70+
3771
afterEach(() => {
3872
resetAllWhenMocks()
3973
})
4074

4175
it('should be able to initialize the Python path', () => {
42-
when(mockGetConfig)
43-
.calledWith()
44-
.mockReturnValue({
45-
python: { pathToPythonOverride: '/some/override/python' },
46-
} as Config)
47-
48-
initializePython()
49-
5076
expect(mockSelectPythonPath).toHaveBeenCalledWith('/some/override/python')
5177
expect(mockHandleConfigChange).toHaveBeenCalledWith(
5278
'python.pathToPythonOverride',
@@ -111,4 +137,46 @@ describe('analyzeProtocolSource', () => {
111137
expect(mockWriteFailedAnalysis).toHaveBeenCalledWith(outputPath, 'oh no')
112138
})
113139
})
140+
141+
it('should open file picker in response to CHANGE_PYTHON_PATH_OVERRIDE and not call dispatch if no directory is returned from showOpenDirectoryDialog', () => {
142+
when(mockShowOpenDirectoryDialog)
143+
.calledWith(mockMainWindow)
144+
.mockResolvedValue([])
145+
handleAction(ProtocolAnalysis.changePythonPathOverrideConfig())
146+
147+
return flush().then(() => {
148+
expect(mockShowOpenDirectoryDialog).toHaveBeenCalledWith(mockMainWindow)
149+
expect(dispatch).not.toHaveBeenCalled()
150+
})
151+
})
152+
153+
it('should open file picker in response to CHANGE_PYTHON_PATH_OVERRIDE and call dispatch with directory returned from showOpenDirectoryDialog', () => {
154+
when(mockShowOpenDirectoryDialog)
155+
.calledWith(mockMainWindow)
156+
.mockResolvedValue(['path/to/override'])
157+
handleAction(ProtocolAnalysis.changePythonPathOverrideConfig())
158+
159+
return flush().then(() => {
160+
expect(mockShowOpenDirectoryDialog).toHaveBeenCalledWith(mockMainWindow)
161+
expect(dispatch).toHaveBeenCalledWith(
162+
Cfg.updateConfigValue(
163+
CONFIG_PYTHON_PATH_TO_PYTHON_OVERRIDE,
164+
'path/to/override'
165+
)
166+
)
167+
})
168+
})
169+
170+
it('should call openDirectoryInFileExplorer in response to OPEN_PYTHON_DIRECTORY', () => {
171+
when(mockOpenDirectoryInFileExplorer)
172+
.calledWith('/some/override/python')
173+
.mockResolvedValue(null)
174+
handleAction(ProtocolAnalysis.openPythonInterpreterDirectory())
175+
176+
return flush().then(() => {
177+
expect(mockOpenDirectoryInFileExplorer).toHaveBeenCalledWith(
178+
'/some/override/python'
179+
)
180+
})
181+
})
114182
})

app-shell/src/protocol-analysis/index.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,65 @@
1+
import * as ProtocolAnalysis from '@opentrons/app/src/redux/protocol-analysis'
2+
import * as Cfg from '@opentrons/app/src/redux/config'
3+
14
import { createLogger } from '../log'
25
import { getConfig, handleConfigChange } from '../config'
3-
46
import { getValidLabwareFilePaths } from '../labware'
7+
import {
8+
showOpenDirectoryDialog,
9+
openDirectoryInFileExplorer,
10+
} from '../dialogs'
511
import { selectPythonPath, getPythonPath } from './getPythonPath'
612
import { executeAnalyzeCli } from './executeAnalyzeCli'
713
import { writeFailedAnalysis } from './writeFailedAnalysis'
814

15+
import type { BrowserWindow } from 'electron'
16+
import type { Action, Dispatch } from '../types'
17+
918
const log = createLogger('protocol-analysis')
1019

11-
export function initializePython(): void {
20+
export const CONFIG_PYTHON_PATH_TO_PYTHON_OVERRIDE =
21+
'python.pathToPythonOverride'
22+
23+
export function registerProtocolAnalysis(
24+
dispatch: Dispatch,
25+
mainWindow: BrowserWindow
26+
): (action: Action) => void {
1227
const pathToPythonOverride = getConfig().python.pathToPythonOverride
1328
selectPythonPath(pathToPythonOverride)
1429

15-
handleConfigChange('python.pathToPythonOverride', newValue => {
30+
handleConfigChange(CONFIG_PYTHON_PATH_TO_PYTHON_OVERRIDE, newValue => {
1631
selectPythonPath(newValue)
1732
})
33+
34+
return function handleIncomingAction(action: Action): void {
35+
switch (action.type) {
36+
case ProtocolAnalysis.OPEN_PYTHON_DIRECTORY: {
37+
const dir = getConfig().python.pathToPythonOverride
38+
openDirectoryInFileExplorer(dir).catch(err => {
39+
log.debug('Error opening python directory', err.message)
40+
})
41+
break
42+
}
43+
case ProtocolAnalysis.CHANGE_PYTHON_PATH_OVERRIDE: {
44+
showOpenDirectoryDialog(mainWindow)
45+
.then(filePaths => {
46+
if (filePaths.length > 0) {
47+
const nextValue = filePaths[0]
48+
dispatch(
49+
Cfg.updateConfigValue(
50+
CONFIG_PYTHON_PATH_TO_PYTHON_OVERRIDE,
51+
nextValue
52+
)
53+
)
54+
}
55+
})
56+
.catch(err => {
57+
log.debug('Error changing python path override', err.message)
58+
})
59+
break
60+
}
61+
}
62+
}
1863
}
1964

2065
export function analyzeProtocolSource(

app/src/pages/AppSettings/AdvancedSettings.tsx

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as React from 'react'
22
import { useTranslation } from 'react-i18next'
3-
import path from 'path'
43
import { useSelector, useDispatch } from 'react-redux'
54
import { css } from 'styled-components'
65

@@ -27,6 +26,7 @@ import {
2726
} from '@opentrons/components'
2827

2928
import * as Config from '../../redux/config'
29+
import * as ProtocolAnalysis from '../../redux/protocol-analysis'
3030
import * as Calibration from '../../redux/calibration'
3131
import * as CustomLabware from '../../redux/custom-labware'
3232
import {
@@ -60,19 +60,13 @@ const ALWAYS_TRASH: 'always-trash' = 'always-trash'
6060
const ALWAYS_PROMPT: 'always-prompt' = 'always-prompt'
6161
const REALTEK_URL = 'https://www.realtek.com/en/'
6262

63-
const INPUT_STYLES = css`
64-
position: fixed;
65-
clip: rect(1px 1px 1px 1px);
66-
`
67-
6863
type BlockSelection =
6964
| typeof ALWAYS_BLOCK
7065
| typeof ALWAYS_TRASH
7166
| typeof ALWAYS_PROMPT
7267

7368
export function AdvancedSettings(): JSX.Element {
7469
const { t } = useTranslation(['app_settings', 'shared'])
75-
const pythonDirectoryFileInput = React.useRef<HTMLInputElement>(null)
7670
const useTrashSurfaceForTipCal = useSelector((state: State) =>
7771
Config.getUseTrashSurfaceForTipCal(state)
7872
)
@@ -157,30 +151,14 @@ export function AdvancedSettings(): JSX.Element {
157151
)
158152
)
159153

160-
// TODO(jr, 5/6/22): find another way to get webkitdirectory to work in the input DOM https://github.com/facebook/react/issues/3468
161-
React.useEffect(() => {
162-
if (pythonDirectoryFileInput.current !== null) {
163-
pythonDirectoryFileInput.current.setAttribute('directory', 'true')
164-
pythonDirectoryFileInput.current.setAttribute('webkitdirectory', 'true')
165-
}
166-
}, [pythonDirectoryFileInput])
167-
168154
const handleClickPythonDirectoryChange: React.MouseEventHandler<HTMLButtonElement> = _event => {
169-
pythonDirectoryFileInput.current?.click()
155+
dispatch(ProtocolAnalysis.changePythonPathOverrideConfig())
170156
trackEvent({
171157
name: 'changePathToPythonDirectory',
172158
properties: {},
173159
})
174160
}
175161

176-
const setPythonInterpreterDirectory: React.ChangeEventHandler<HTMLInputElement> = event => {
177-
const { files = [] } = event.target ?? {}
178-
const dirName =
179-
files?.[0]?.path != null ? path.dirname(files?.[0]?.path) : null
180-
dispatch(Config.updateConfigValue('python.pathToPythonOverride', dirName))
181-
event.target.value = ''
182-
}
183-
184162
const toggleDevtools = (): unknown => dispatch(Config.toggleDevtools())
185163
const handleChannel: React.ChangeEventHandler<HTMLSelectElement> = event =>
186164
dispatch(Config.updateConfigValue('update.channel', event.target.value))
@@ -558,7 +536,7 @@ export function AdvancedSettings(): JSX.Element {
558536
css={TYPOGRAPHY.pRegular}
559537
color={COLORS.darkBlack}
560538
onClick={() =>
561-
dispatch(Config.openPythonInterpreterDirectory())
539+
dispatch(ProtocolAnalysis.openPythonInterpreterDirectory())
562540
}
563541
id="AdvancedSettings_sourceFolderLinkPython"
564542
>
@@ -592,14 +570,6 @@ export function AdvancedSettings(): JSX.Element {
592570
{t('add_override_path')}
593571
</TertiaryButton>
594572
)}
595-
<input
596-
id="AdvancedSetting_pythonPathDirectoryInput"
597-
data-testid="AdvancedSetting_pythonPathDirectoryInput"
598-
type="file"
599-
css={INPUT_STYLES}
600-
ref={pythonDirectoryFileInput}
601-
onChange={setPythonInterpreterDirectory}
602-
/>
603573
</Flex>
604574
<Divider marginY={SPACING.spacing5} />
605575
<Flex alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_SPACE_BETWEEN}>

0 commit comments

Comments
 (0)