diff --git a/rtl-spec/components/sidebar-file-tree.spec.tsx b/rtl-spec/components/sidebar-file-tree.spec.tsx
index 922df2eb12..5b5cb07686 100644
--- a/rtl-spec/components/sidebar-file-tree.spec.tsx
+++ b/rtl-spec/components/sidebar-file-tree.spec.tsx
@@ -96,7 +96,35 @@ 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 with path separator)', 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}": filenames cannot include path separators`,
+ );
+ });
});
it('can delete editors', async () => {
@@ -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 name with path separator)', 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}": filenames cannot include path separators`,
+ );
+ });
+
+ 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';
diff --git a/src/renderer/components/sidebar-file-tree.tsx b/src/renderer/components/sidebar-file-tree.tsx
index 634b055920..d7148541e1 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;
@@ -108,7 +108,7 @@ export const SidebarFileTree = observer(
{
+ onKeyDown={async (e) => {
if (e.key === 'Escape') {
e.currentTarget.blur();
} else if (e.key === 'Enter') {
@@ -189,29 +189,13 @@ export const SidebarFileTree = observer(
if (!id) 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);
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);
}
};
@@ -220,13 +204,14 @@ 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 new Promise((r) => setTimeout(() => r(), 100));
+ 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) {
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