Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion rtl-spec/components/sidebar-file-tree.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<SidebarFileTree appState={store} />);
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 () => {
Expand Down Expand Up @@ -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(<SidebarFileTree appState={store} />);

// 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';
Expand Down
35 changes: 10 additions & 25 deletions src/renderer/components/sidebar-file-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -108,7 +108,7 @@ export const SidebarFileTree = observer(
<input
className={classNames(Classes.INPUT, Classes.FILL, Classes.SMALL)}
style={{ width: `100%`, padding: 0 }}
onKeyDown={(e) => {
onKeyDown={async (e) => {
if (e.key === 'Escape') {
e.currentTarget.blur();
} else if (e.key === 'Enter') {
Expand Down Expand Up @@ -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<void>((r) => setTimeout(() => r(), 100));
await appState.showErrorDialog(err.message);
}
};

Expand All @@ -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<void>((r) => setTimeout(() => r(), 100));
await appState.showErrorDialog(err.message);
}
};

Expand Down
33 changes: 33 additions & 0 deletions src/renderer/editor-mosaic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
6 changes: 4 additions & 2 deletions src/renderer/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1023,14 +1023,16 @@ export class AppState {
});
}

public async showErrorDialog(label: string | JSX.Element): Promise<void> {
public showErrorDialog = async (
label: string | JSX.Element,
): Promise<void> => {
await this.showGenericDialog({
label,
ok: 'Close',
type: GenericDialogType.warning,
wantsInput: false,
});
}
};

/**
* Ensure that any buffered console output is
Expand Down