Skip to content

Commit ed848f4

Browse files
authored
Fix/manage solution file picker requestid (#43)
* Enhance file selection functionality with relative/absolute path configuration and update copyright notices * Fix copyright formatting in manage-solution-custom-editor and manage-solution-webview-main files * Refactor context selection tests to include configuration provider and update file selection data format * fix merge down conflicts * Add type declaration for CSS modules and update imports in ManageSolution component * Refactor file selection handling to use requestId instead of targetElementId * Fix copyright notice formatting in style.d.ts * Add handling for empty file selection in ManageSolutionWebviewMain and corresponding test
1 parent 3873534 commit ed848f4

7 files changed

Lines changed: 155 additions & 57 deletions

File tree

src/style.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright 2025-2026 Arm Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
declare module '*.css';

src/views/manage-solution/manage-solution-webview-main.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@ describe('ContextSelectionWebviewMain', () => {
6464
});
6565

6666
it('sends selected context data on GET_CONTEXT_SELECTION_DATA', async () => {
67+
const configurationProvider = configurationProviderFactory();
6768
const main = manageSolutionWebviewMainFactory({
68-
webviewManager
69+
webviewManager,
70+
configurationProvider
6971
});
7072
await main.activate(context as unknown as vscode.ExtensionContext);
7173

@@ -546,7 +548,7 @@ describe('ContextSelectionWebviewMain', () => {
546548
]);
547549

548550
await fireAndWait('SELECT_FILE', {
549-
targetElementId: 'image-path',
551+
requestId: 'image-path',
550552
options: {
551553
defaultUri: defaultPath,
552554
canSelectMany: false,
@@ -561,7 +563,7 @@ describe('ContextSelectionWebviewMain', () => {
561563
expect.objectContaining({
562564
type: 'FILE_SELECTED',
563565
data: ['images/app.axf'],
564-
for: 'image-path'
566+
requestId: 'image-path'
565567
})
566568
);
567569
});

src/views/manage-solution/manage-solution-webview-main.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ export class ManageSolutionWebviewMain {
239239
await this.updateDebuggerParameter(message.service, message.key, message.value, message.pname);
240240
break;
241241
case 'SELECT_FILE':
242-
await this.selectFileDialog(message.targetElementId, message.options);
242+
await this.selectFileDialog(message.requestId, message.options);
243243
break;
244244
case 'SET_AUTO_UPDATE':
245245
await this.configurationProvider.setConfigVariable(manifest.CONFIG_AUTO_DEBUG_LAUNCH, message.value, undefined, true);
@@ -413,7 +413,7 @@ export class ManageSolutionWebviewMain {
413413
});
414414
}
415415

416-
private async selectFileDialog(targetElementId: string, options?: FileSelectorOptionsType): Promise<void> {
416+
private async selectFileDialog(requestId: string, options?: FileSelectorOptionsType): Promise<void> {
417417
const solutionDir = this.getSolutionDir();
418418
if (options?.defaultUri) {
419419
options.defaultUri = URI.file(dirname(options.defaultUri)).toString();
@@ -437,7 +437,9 @@ export class ManageSolutionWebviewMain {
437437
)
438438
);
439439

440-
await this.webviewManager.sendMessage({ type: 'FILE_SELECTED', data: paths, for: targetElementId });
440+
await this.webviewManager.sendMessage({ type: 'FILE_SELECTED', data: paths, requestId });
441+
} else {
442+
await this.webviewManager.sendMessage({ type: 'FILE_SELECTED', data: [], requestId });
441443
}
442444
}
443445

src/views/manage-solution/messages.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export type OutgoingMessage
3636
| { type: 'SAVE_CONTEXT_SELECTION' }
3737
| { type: 'OPEN_HELP' }
3838
| { type: 'SET_DEBUG_ADAPTER_PROPERTY', service: string | undefined, key: string, value: string | number, pname?: string }
39-
| { type: 'SELECT_FILE', targetElementId: string, options?: FileSelectorOptionsType }
39+
| { type: 'SELECT_FILE', requestId: string, options?: FileSelectorOptionsType }
4040
| { type: 'SET_AUTO_UPDATE', value: boolean }
4141
| { type: 'TOGGLE_DEBUGGER', value: boolean }
4242
| { type: 'TOGGLE_DEBUG_ADAPTER_SECTION', section: string }
@@ -52,6 +52,6 @@ export type IncomingMessage
5252
| { type: 'ACTIVE_TARGET_SET', data: ActiveTargetSet }
5353
| { type: 'IS_DIRTY', data: boolean }
5454
| { type: 'IS_BUSY', data: boolean }
55-
| { type: 'FILE_SELECTED', data: string[], for: string }
55+
| { type: 'FILE_SELECTED', data: string[], requestId: string }
5656
| { type: 'AUTO_UPDATE', data: boolean }
5757
;

src/views/manage-solution/view/components/manage-solution.test.tsx

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -584,37 +584,78 @@ describe('ContextSelection', () => {
584584

585585
describe('selectFile', () => {
586586

587-
it('does not update input value if FILE_SELECTED message has no data', () => {
587+
it('emits SELECT_FILE on Browse and resolves FILE_SELECTED into adapter update', () => {
588588
createContextSelectionComponent();
589+
postGenericDataContext();
590+
listener.mockClear();
589591

590-
const mockInput = document.createElement('input');
591-
mockInput.setAttribute('id', 'test-input');
592-
container.appendChild(mockInput);
592+
const filePathInput = container.querySelector('input[data-yml-node="telnet-prop_d"]') as HTMLInputElement;
593+
expect(filePathInput).toBeDefined();
594+
expect(filePathInput.value).toBe('');
595+
596+
const browseButton = container.querySelector('.file-button');
597+
expect(browseButton).toBeDefined();
598+
599+
React.act(() => {
600+
fireEvent.click(browseButton as Element);
601+
});
602+
603+
const selectFileMessage = listener.mock.calls
604+
.map(([message]) => message)
605+
.find(message => message.type === 'SELECT_FILE');
606+
607+
expect(selectFileMessage).toBeDefined();
608+
expect(selectFileMessage).toEqual(expect.objectContaining({
609+
type: 'SELECT_FILE',
610+
requestId: expect.stringMatching(/^manage-solution-file-/),
611+
}));
612+
613+
const selectedPath = '/path/to/selected/file.axf';
614+
React.act(() => {
615+
messageHandler.postWindowMessage({
616+
type: 'FILE_SELECTED',
617+
requestId: selectFileMessage!.requestId,
618+
data: [selectedPath]
619+
});
620+
});
621+
622+
expect(listener).toHaveBeenCalledWith({
623+
type: 'SET_DEBUG_ADAPTER_PROPERTY',
624+
service: undefined,
625+
key: 'telnet-prop_d',
626+
value: selectedPath,
627+
});
628+
expect(filePathInput.value).toBe(selectedPath);
629+
});
630+
631+
it('ignores FILE_SELECTED message with empty data', () => {
632+
createContextSelectionComponent();
633+
listener.mockClear();
593634

594635
React.act(() => {
595636
messageHandler.postWindowMessage({
596637
type: 'FILE_SELECTED',
597-
for: 'test-input',
638+
requestId: 'test-input',
598639
data: []
599640
});
600641
});
601642

602-
expect(mockInput.value).toBe('');
643+
expect(listener).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'SET_DEBUG_ADAPTER_PROPERTY' }));
603644
});
604645

605-
it('does not update input value if FILE_SELECTED message is for a non-existent element', () => {
646+
it('ignores FILE_SELECTED message for unknown request id', () => {
606647
createContextSelectionComponent();
648+
listener.mockClear();
607649

608650
React.act(() => {
609651
messageHandler.postWindowMessage({
610652
type: 'FILE_SELECTED',
611-
for: 'non-existent-input',
653+
requestId: 'non-existent-input',
612654
data: ['/path/to/selected/file.exe']
613655
});
614656
});
615657

616-
const nonExistentInput = container.querySelector('#non-existent-input');
617-
expect(nonExistentInput).toBeNull();
658+
expect(listener).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'SET_DEBUG_ADAPTER_PROPERTY' }));
618659
});
619660
});
620661
});

src/views/manage-solution/view/components/manage-solution.tsx

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,18 @@
1414
* limitations under the License.
1515
*/
1616

17+
import './manage-solution.css';
18+
import '../../../common/style/antd-overrides.css';
1719
import { LoadingOutlined } from '@ant-design/icons';
1820
import { Button, Checkbox, CheckboxChangeEvent, Col, ConfigProvider, Flex, Input, InputNumber, Row, Spin, Tabs, theme } from 'antd';
19-
import { debounce } from 'lodash';
2021
import * as React from 'react';
2122
import { UISection, UISectionChildren } from '../../../../debug/debug-adapters-yaml-file';
2223
import { CompactDropdown } from '../../../common/components/compact-dropdown';
23-
import '../../../common/style/antd-overrides.css';
2424
import { useVSCodeTheme } from '../../../hooks/use-vscode-theme';
2525
import { MessageHandler } from '../../../message-handler';
2626
import { IncomingMessage, OutgoingMessage } from '../../messages';
2727
import { GenericPropertyList } from '../state/manage-solution-state';
2828
import { SolutionUpdateAction, contextUpdateReducer, initialState, manageSolutionReducer } from '../state/reducer';
29-
import './manage-solution.css';
3029
import { ProjectsTable } from './projects-table';
3130
import { TargetsTable } from './targets-table';
3231
import { PathType } from '../../types';
@@ -35,10 +34,26 @@ export interface ManageSolutionProps {
3534
messageHandler: MessageHandler<IncomingMessage, OutgoingMessage>;
3635
}
3736

37+
type PendingFileSelection = {
38+
service: string | undefined;
39+
key: string;
40+
localValueKey: string;
41+
};
42+
43+
type SelectFileContext = {
44+
service: string | undefined;
45+
key: string;
46+
localValueKey: string;
47+
title?: string;
48+
defaultUri?: string;
49+
pathType?: PathType;
50+
};
51+
3852
export const ManageSolution = (props: ManageSolutionProps) => {
3953
const [state, dispatch] = React.useReducer(manageSolutionReducer, initialState);
4054
// Eager local editable values snapshot (display-layer values). Numbers are stored scaled for user editing.
4155
const [localValues, setLocalValues] = React.useState<Record<string, string | number>>({});
56+
const pendingFileSelections = React.useRef<Map<string, PendingFileSelection>>(new Map());
4257

4358
const adapter = React.useMemo(
4459
() => state.debugAdapters.find(adapter => adapter.name === state.debugger),
@@ -74,14 +89,28 @@ export const ManageSolution = (props: ManageSolutionProps) => {
7489

7590
React.useEffect(() => {
7691
const handleFileSelected = (message: IncomingMessage) => {
77-
if (message.type === 'FILE_SELECTED' && message.data && message.data.length > 0) {
78-
const element = document.getElementById(message.for || '');
79-
if (element) {
80-
const ymlNode = element.getAttribute('data-yml-node') || 'config';
81-
props.messageHandler.push({ type: 'SET_DEBUG_ADAPTER_PROPERTY', service: undefined, key: ymlNode, value: message.data[0] });
82-
(element as HTMLInputElement).value = message.data[0];
83-
}
92+
if (message.type !== 'FILE_SELECTED') {
93+
return;
94+
}
95+
96+
const pendingSelection = pendingFileSelections.current.get(message.requestId);
97+
if (!pendingSelection) {
98+
return;
99+
}
100+
pendingFileSelections.current.delete(message.requestId);
101+
102+
if (!message.data || message.data.length === 0) {
103+
return;
84104
}
105+
106+
const selectedPath = message.data[0];
107+
setLocalValues(prev => ({ ...prev, [pendingSelection.localValueKey]: selectedPath }));
108+
props.messageHandler.push({
109+
type: 'SET_DEBUG_ADAPTER_PROPERTY',
110+
service: pendingSelection.service,
111+
key: pendingSelection.key,
112+
value: selectedPath
113+
});
85114
};
86115

87116
const unsubscribe = props.messageHandler.subscribe(handleFileSelected);
@@ -203,34 +232,26 @@ export const ManageSolution = (props: ManageSolutionProps) => {
203232

204233
const hasDebugger = !!state.debugger;
205234

206-
const selectFile = React.useMemo(() => {
207-
const handleSelectFile = (e: React.MouseEvent<HTMLElement>) => {
208-
const input = (e.target as HTMLElement)?.parentElement?.parentNode?.parentNode?.querySelector('input');
209-
let id = input?.getAttribute('id');
210-
if (id === null || id === undefined) {
211-
// eslint-disable-next-line react-hooks/purity
212-
id = Math.random().toString(36).substring(2, 15);
213-
input?.setAttribute('id', id);
214-
}
215-
const title = input?.getAttribute('title') || 'Select File';
216-
const dataOptionPathType = input?.getAttribute('data-option-path-type');
217-
const optionPathType: PathType = dataOptionPathType === 'absolute' ? 'absolute' : 'relative';
218-
const currentValue = input?.value || '';
219-
props.messageHandler.push({
220-
type: 'SELECT_FILE',
221-
targetElementId: id,
222-
options: {
223-
canSelectMany: false,
224-
defaultUri: currentValue,
225-
openLabel: 'Select File', // title of the dialog button to choose the file
226-
title: title, // dialog title
227-
filters: { 'All Files': ['*'] },
228-
pathType: optionPathType
229-
}
230-
});
231-
};
235+
const selectFile = React.useCallback((context: SelectFileContext) => {
236+
const requestId = `manage-solution-file-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
237+
pendingFileSelections.current.set(requestId, {
238+
service: context.service,
239+
key: context.key,
240+
localValueKey: context.localValueKey,
241+
});
232242

233-
return debounce(handleSelectFile, 300);
243+
props.messageHandler.push({
244+
type: 'SELECT_FILE',
245+
requestId,
246+
options: {
247+
canSelectMany: false,
248+
defaultUri: context.defaultUri,
249+
openLabel: 'Select File',
250+
title: context.title || 'Select File',
251+
filters: { 'All Files': ['*'] },
252+
pathType: context.pathType ?? 'relative'
253+
}
254+
});
234255
}, [props.messageHandler]);
235256

236257
// Eager initialization/reset of localValues whenever adapter context changes.
@@ -434,7 +455,22 @@ export const ManageSolution = (props: ManageSolutionProps) => {
434455
return (
435456
<Input
436457
addonBefore={o.name}
437-
addonAfter={<Button type="primary" className='file-button' onClick={selectFile}> Browse</Button>}
458+
addonAfter={
459+
<Button
460+
type="primary"
461+
className='file-button'
462+
onClick={() => selectFile({
463+
service: section['yml-node'],
464+
key: o['yml-node'],
465+
localValueKey: k,
466+
title: o.description || 'Select File',
467+
defaultUri: (localValues[k] as string) ?? '',
468+
pathType: o['path-type'],
469+
})}
470+
>
471+
Browse
472+
</Button>
473+
}
438474
value={(localValues[k] as string) ?? ''}
439475
data-yml-node={o['yml-node']}
440476
data-option-path-type={o['path-type']}

src/views/manage-solution/view/state/reducer.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ describe('contextSelectionReducer', () => {
125125
type: 'INCOMING_MESSAGE', message: {
126126
type: 'FILE_SELECTED',
127127
data: ['C:/path/to/my/file.txt'],
128-
for: 'some-id',
128+
requestId: 'some-id',
129129
}
130130
});
131131

0 commit comments

Comments
 (0)