From 6f1a375e5930eab73ee0ff0bd213499232d8a118 Mon Sep 17 00:00:00 2001 From: MohamedAli00949 <76850143+MohamedAli00949@users.noreply.github.com> Date: Mon, 26 Jan 2026 09:39:14 +0200 Subject: [PATCH 1/5] fix: adding files inside subdir --- src/renderer/components/sidebar-file-tree.tsx | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/sidebar-file-tree.tsx b/src/renderer/components/sidebar-file-tree.tsx index 634b055920..b7c61fec26 100644 --- a/src/renderer/components/sidebar-file-tree.tsx +++ b/src/renderer/components/sidebar-file-tree.tsx @@ -41,7 +41,7 @@ export const SidebarFileTree = observer( } public render() { - const { editorMosaic } = this.props.appState; + const { editorMosaic, showErrorDialog } = this.props.appState; const { files, focusedFile } = editorMosaic; const fileList: TreeNodeInfo[] = Array.from(files) @@ -108,10 +108,23 @@ export const SidebarFileTree = observer( { + onKeyDown={async (e) => { if (e.key === 'Escape') { e.currentTarget.blur(); } else if (e.key === 'Enter') { + const filename = e.currentTarget.value.trim(); + if (filename.includes('/') || filename.includes('\\')) { + await showErrorDialog( + `Invalid filename "${filename}": inside subdir not supported yet`, + ); + return; + } + if (!isSupportedFile(filename)) { + await showErrorDialog( + `Invalid filename "${filename}": Must be a file ending in .cjs, .js, .mjs, .html, .css, or .json`, + ); + return; + } this.createEditor(e.currentTarget.value as EditorId); e.currentTarget.blur(); } @@ -189,6 +202,13 @@ export const SidebarFileTree = observer( if (!id) return; + if (id.includes('/') || id.includes('\\')) { + await appState.showErrorDialog( + `Invalid filename "${id}": inside subdir not supported yet`, + ); + return; + } + if ( id.endsWith('.json') && [PACKAGE_NAME, 'package-lock.json'].includes(id) From 2e7bb1d43b03708c2f067624e3c1b9148930abbf Mon Sep 17 00:00:00 2001 From: MohamedAli00949 <76850143+MohamedAli00949@users.noreply.github.com> Date: Mon, 26 Jan 2026 09:46:22 +0200 Subject: [PATCH 2/5] fix: refactor from normal func to arrow func --- src/renderer/state.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/renderer/state.ts b/src/renderer/state.ts index c421af435f..a2ee654b42 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -1023,14 +1023,16 @@ export class AppState { }); } - public async showErrorDialog(label: string | JSX.Element): Promise { + public showErrorDialog = async ( + label: string | JSX.Element, + ): Promise => { await this.showGenericDialog({ label, ok: 'Close', type: GenericDialogType.warning, wantsInput: false, }); - } + }; /** * Ensure that any buffered console output is From 33df8a0b18f4ac23ed4d37c4b0f5e741c8369ea3 Mon Sep 17 00:00:00 2001 From: MohamedAli00949 <76850143+MohamedAli00949@users.noreply.github.com> Date: Mon, 26 Jan 2026 09:59:58 +0200 Subject: [PATCH 3/5] test: write tests to test prevent adding files inside subdir --- .../components/sidebar-file-tree.spec.tsx | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/rtl-spec/components/sidebar-file-tree.spec.tsx b/rtl-spec/components/sidebar-file-tree.spec.tsx index 922df2eb12..4b9c31bb6e 100644 --- a/rtl-spec/components/sidebar-file-tree.spec.tsx +++ b/rtl-spec/components/sidebar-file-tree.spec.tsx @@ -99,6 +99,34 @@ describe('SidebarFileTree component', () => { expect(editorMosaic.files.get('tester.js')).toBe(EditorPresence.Pending); }); + it('fails to create new editors (file name inside subdir)', async () => { + const user = userEvent.setup(); + const { container } = render(); + const EDITOR_NEW_NAME = 'subdir/tester.js'; + + expect(editorMosaic.files.get(EDITOR_NEW_NAME)).toBe(undefined); + + // Click the "Add New File" button (by icon) + const addButton = container.querySelector( + 'button .bp3-icon-add', + )?.parentElement; + await user.click(addButton!); + + // Type the filename and press Enter + const input = container.querySelector( + '#new-file-input', + ) as HTMLInputElement; + await user.type(input, `${EDITOR_NEW_NAME}{Enter}`); + + // Wait for error dialog to be called + await waitFor(() => { + console.log('store.showErrorDialog: ', store.showErrorDialog); + expect(store.showErrorDialog).toHaveBeenCalledWith( + `Invalid filename "${EDITOR_NEW_NAME}": inside subdir not supported yet`, + ); + }); + }); + it('can delete editors', async () => { const user = userEvent.setup(); const { container } = render(); @@ -237,6 +265,36 @@ describe('SidebarFileTree component', () => { expect(editorMosaic.files.get(EDITOR_NAME)).toBe(EditorPresence.Pending); }); + it('fails if trying to rename an editor to an unsupported name (file inside subdir)', async () => { + const user = userEvent.setup(); + const EDITOR_NAME = 'index.html'; + const EDITOR_NEW_NAME = 'msg/data.json'; + + store.showInputDialog = vi.fn().mockResolvedValueOnce(EDITOR_NEW_NAME); + store.showErrorDialog = vi.fn().mockResolvedValueOnce(true); + + const { container } = render(); + + // Right-click on index.html to open context menu + const fileLabel = Array.from(container.querySelectorAll('.pointer')).find( + (el) => el.textContent === EDITOR_NAME, + ) as HTMLElement; + fireEvent.contextMenu(fileLabel); + + // Click the "Rename" menu item + const renameItem = await screen.findByText('Rename'); + await user.click(renameItem); + + // Wait for error dialog to be called + await waitFor(() => { + expect(store.showErrorDialog).toHaveBeenCalledWith( + `Invalid filename "${EDITOR_NEW_NAME}": inside subdir not supported yet`, + ); + }); + + expect(editorMosaic.files.get(EDITOR_NAME)).toBe(EditorPresence.Pending); + }); + it('fails if trying to rename an editor to an existing name', async () => { const user = userEvent.setup(); const EXISTED_NAME = 'styles.css'; From 55041ec6b2782750a3fc1400854cb091634e88d1 Mon Sep 17 00:00:00 2001 From: MohamedAli00949 <76850143+MohamedAli00949@users.noreply.github.com> Date: Tue, 27 Jan 2026 04:06:20 +0200 Subject: [PATCH 4/5] fix: refactor file naming error handling --- .../components/sidebar-file-tree.spec.tsx | 10 ++-- src/renderer/components/sidebar-file-tree.tsx | 51 +++---------------- src/renderer/editor-mosaic.ts | 33 ++++++++++++ 3 files changed, 45 insertions(+), 49 deletions(-) diff --git a/rtl-spec/components/sidebar-file-tree.spec.tsx b/rtl-spec/components/sidebar-file-tree.spec.tsx index 4b9c31bb6e..5b5cb07686 100644 --- a/rtl-spec/components/sidebar-file-tree.spec.tsx +++ b/rtl-spec/components/sidebar-file-tree.spec.tsx @@ -96,10 +96,10 @@ describe('SidebarFileTree component', () => { ) as HTMLInputElement; await user.type(input, 'tester.js{Enter}'); - expect(editorMosaic.files.get('tester.js')).toBe(EditorPresence.Pending); + expect(editorMosaic.files.get('tester.js')).toBe(EditorPresence.Hidden); }); - it('fails to create new editors (file name inside subdir)', async () => { + it('fails to create new editors (file name with path separator)', async () => { const user = userEvent.setup(); const { container } = render(); const EDITOR_NEW_NAME = 'subdir/tester.js'; @@ -122,7 +122,7 @@ describe('SidebarFileTree component', () => { await waitFor(() => { console.log('store.showErrorDialog: ', store.showErrorDialog); expect(store.showErrorDialog).toHaveBeenCalledWith( - `Invalid filename "${EDITOR_NEW_NAME}": inside subdir not supported yet`, + `Invalid filename "${EDITOR_NEW_NAME}": filenames cannot include path separators`, ); }); }); @@ -265,7 +265,7 @@ describe('SidebarFileTree component', () => { expect(editorMosaic.files.get(EDITOR_NAME)).toBe(EditorPresence.Pending); }); - it('fails if trying to rename an editor to an unsupported name (file inside subdir)', async () => { + it('fails if trying to rename an editor to an unsupported name (file name with path separator)', async () => { const user = userEvent.setup(); const EDITOR_NAME = 'index.html'; const EDITOR_NEW_NAME = 'msg/data.json'; @@ -288,7 +288,7 @@ describe('SidebarFileTree component', () => { // Wait for error dialog to be called await waitFor(() => { expect(store.showErrorDialog).toHaveBeenCalledWith( - `Invalid filename "${EDITOR_NEW_NAME}": inside subdir not supported yet`, + `Invalid filename "${EDITOR_NEW_NAME}": filenames cannot include path separators`, ); }); diff --git a/src/renderer/components/sidebar-file-tree.tsx b/src/renderer/components/sidebar-file-tree.tsx index b7c61fec26..c6ff4e08f7 100644 --- a/src/renderer/components/sidebar-file-tree.tsx +++ b/src/renderer/components/sidebar-file-tree.tsx @@ -14,10 +14,10 @@ import { ContextMenu2, Tooltip2 } from '@blueprintjs/popover2'; import classNames from 'classnames'; import { observer } from 'mobx-react'; -import { EditorId, PACKAGE_NAME } from '../../interfaces'; +import { EditorId } from '../../interfaces'; import { EditorPresence } from '../editor-mosaic'; import { AppState } from '../state'; -import { isMainEntryPoint, isSupportedFile } from '../utils/editor-utils'; +import { isMainEntryPoint } from '../utils/editor-utils'; interface FileTreeProps { appState: AppState; @@ -41,7 +41,7 @@ export const SidebarFileTree = observer( } public render() { - const { editorMosaic, showErrorDialog } = this.props.appState; + const { editorMosaic } = this.props.appState; const { files, focusedFile } = editorMosaic; const fileList: TreeNodeInfo[] = Array.from(files) @@ -112,19 +112,6 @@ export const SidebarFileTree = observer( if (e.key === 'Escape') { e.currentTarget.blur(); } else if (e.key === 'Enter') { - const filename = e.currentTarget.value.trim(); - if (filename.includes('/') || filename.includes('\\')) { - await showErrorDialog( - `Invalid filename "${filename}": inside subdir not supported yet`, - ); - return; - } - if (!isSupportedFile(filename)) { - await showErrorDialog( - `Invalid filename "${filename}": Must be a file ending in .cjs, .js, .mjs, .html, .css, or .json`, - ); - return; - } this.createEditor(e.currentTarget.value as EditorId); e.currentTarget.blur(); } @@ -202,30 +189,6 @@ export const SidebarFileTree = observer( if (!id) return; - if (id.includes('/') || id.includes('\\')) { - await appState.showErrorDialog( - `Invalid filename "${id}": inside subdir not supported yet`, - ); - return; - } - - if ( - id.endsWith('.json') && - [PACKAGE_NAME, 'package-lock.json'].includes(id) - ) { - await appState.showErrorDialog( - `Cannot add ${PACKAGE_NAME} or package-lock.json as custom files`, - ); - return; - } - - if (!isSupportedFile(id)) { - await appState.showErrorDialog( - `Invalid filename "${id}": Must be a file ending in .cjs, .js, .mjs, .html, .css, or .json`, - ); - return; - } - try { await appState.editorMosaic.renameFile(editorId, id); @@ -240,13 +203,13 @@ export const SidebarFileTree = observer( editorMosaic.remove(editorId); }; - public createEditor = (editorId: EditorId) => { + public createEditor = async (editorId: EditorId) => { const { appState } = this.props; try { - appState.editorMosaic.addNewFile(editorId); - appState.editorMosaic.show(editorId); + await appState.editorMosaic.addNewFile(editorId); + await appState.editorMosaic.show(editorId); } catch (err: any) { - appState.showErrorDialog(err.message); + await appState.showErrorDialog(err.message); } }; diff --git a/src/renderer/editor-mosaic.ts b/src/renderer/editor-mosaic.ts index 325196fd52..df4181d223 100644 --- a/src/renderer/editor-mosaic.ts +++ b/src/renderer/editor-mosaic.ts @@ -287,6 +287,18 @@ export class EditorMosaic { throw new Error(`Cannot add file "${id}": File already exists`); } + if (id.includes('/') || id.includes('\\')) { + throw new Error( + `Invalid filename "${id}": filenames cannot include path separators`, + ); + } + + if (!isSupportedFile(id)) { + throw new Error( + `Invalid filename "${id}": Must be a file ending in .cjs, .js, .mjs, .html, .css, or .json`, + ); + } + const entryPoint = this.mainEntryPointFile(); if (isMainEntryPoint(id) && entryPoint) { @@ -308,6 +320,27 @@ export class EditorMosaic { throw new Error(`Cannot rename file to "${newId}": File already exists`); } + if (newId.includes('/') || newId.includes('\\')) { + throw new Error( + `Invalid filename "${newId}": filenames cannot include path separators`, + ); + } + + if ( + newId.endsWith('.json') && + [PACKAGE_NAME, 'package-lock.json'].includes(newId) + ) { + throw new Error( + `Cannot add ${PACKAGE_NAME} or package-lock.json as custom files`, + ); + } + + if (!isSupportedFile(newId)) { + throw new Error( + `Invalid filename "${newId}": Must be a file ending in .cjs, .js, .mjs, .html, .css, or .json`, + ); + } + const entryPoint = this.mainEntryPointFile(); if (isMainEntryPoint(newId) && entryPoint !== oldId) { From f92d624cda467c7d7d1111de49f59f1b09ccbceb Mon Sep 17 00:00:00 2001 From: MohamedAli00949 <76850143+MohamedAli00949@users.noreply.github.com> Date: Sat, 31 Jan 2026 06:54:32 +0200 Subject: [PATCH 5/5] fix: refactor displaying the error dialog from sidebar file operations --- src/renderer/components/sidebar-file-tree.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/sidebar-file-tree.tsx b/src/renderer/components/sidebar-file-tree.tsx index c6ff4e08f7..d7148541e1 100644 --- a/src/renderer/components/sidebar-file-tree.tsx +++ b/src/renderer/components/sidebar-file-tree.tsx @@ -194,7 +194,8 @@ export const SidebarFileTree = observer( if (visible) appState.editorMosaic.show(id); } catch (err: any) { - appState.showErrorDialog(err.message); + await new Promise((r) => setTimeout(() => r(), 100)); + await appState.showErrorDialog(err.message); } }; @@ -209,6 +210,7 @@ export const SidebarFileTree = observer( await appState.editorMosaic.addNewFile(editorId); await appState.editorMosaic.show(editorId); } catch (err: any) { + await new Promise((r) => setTimeout(() => r(), 100)); await appState.showErrorDialog(err.message); } };