Skip to content

Commit 2c92035

Browse files
authoredDec 2, 2024··
Merge pull request #3289 from processing/fix-errors
Fix Console Errors and Update Hooks in FileNode
2 parents 5ab73a7 + 12c4d3f commit 2c92035

File tree

5 files changed

+132
-147
lines changed

5 files changed

+132
-147
lines changed
 

Diff for: ‎client/modules/IDE/components/ConsoleInput.jsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import PropTypes from 'prop-types';
22
import React, { useRef, useEffect, useState } from 'react';
3+
import { useDispatch } from 'react-redux';
34
import CodeMirror from 'codemirror';
45
import { Encode } from 'console-feed';
56

@@ -15,6 +16,7 @@ function ConsoleInput({ theme, fontSize }) {
1516
const [commandCursor, setCommandCursor] = useState(-1);
1617
const codemirrorContainer = useRef(null);
1718
const cmInstance = useRef(null);
19+
const dispatch = useDispatch();
1820

1921
useEffect(() => {
2022
cmInstance.current = CodeMirror(codemirrorContainer.current, {
@@ -45,7 +47,7 @@ function ConsoleInput({ theme, fontSize }) {
4547
payload: { source: 'console', messages }
4648
});
4749

48-
dispatchConsoleEvent(consoleEvent);
50+
dispatch(dispatchConsoleEvent(consoleEvent));
4951
cm.setValue('');
5052
setCommandHistory([value, ...commandHistory]);
5153
setCommandCursor(-1);

Diff for: ‎client/modules/IDE/components/FileNode.jsx

+31-100
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,17 @@
11
import PropTypes from 'prop-types';
22
import classNames from 'classnames';
33
import React, { useState, useRef } from 'react';
4-
import { connect } from 'react-redux';
4+
import { useDispatch, useSelector } from 'react-redux';
55
import { useTranslation } from 'react-i18next';
66

77
import * as IDEActions from '../actions/ide';
88
import * as FileActions from '../actions/files';
9+
import parseFileName from '../utils/parseFileName';
910
import DownArrowIcon from '../../../images/down-filled-triangle.svg';
1011
import FolderRightIcon from '../../../images/triangle-arrow-right.svg';
1112
import FolderDownIcon from '../../../images/triangle-arrow-down.svg';
1213
import FileTypeIcon from './FileTypeIcon';
1314

14-
function parseFileName(name) {
15-
const nameArray = name.split('.');
16-
if (nameArray.length > 1) {
17-
const extension = `.${nameArray[nameArray.length - 1]}`;
18-
const baseName = nameArray.slice(0, -1).join('.');
19-
const firstLetter = baseName[0];
20-
const lastLetter = baseName[baseName.length - 1];
21-
const middleText = baseName.slice(1, -1);
22-
return {
23-
baseName,
24-
firstLetter,
25-
lastLetter,
26-
middleText,
27-
extension
28-
};
29-
}
30-
const firstLetter = name[0];
31-
const lastLetter = name[name.length - 1];
32-
const middleText = name.slice(1, -1);
33-
return {
34-
baseName: name,
35-
firstLetter,
36-
lastLetter,
37-
middleText
38-
};
39-
}
40-
4115
function FileName({ name }) {
4216
const {
4317
baseName,
@@ -62,40 +36,35 @@ FileName.propTypes = {
6236
name: PropTypes.string.isRequired
6337
};
6438

65-
const FileNode = ({
66-
id,
67-
parentId,
68-
children,
69-
name,
70-
fileType,
71-
isSelectedFile,
72-
isFolderClosed,
73-
setSelectedFile,
74-
deleteFile,
75-
updateFileName,
76-
resetSelectedFile,
77-
newFile,
78-
newFolder,
79-
showFolderChildren,
80-
hideFolderChildren,
81-
canEdit,
82-
openUploadFileModal,
83-
authenticated,
84-
onClickFile
85-
}) => {
39+
const FileNode = ({ id, canEdit, onClickFile }) => {
40+
const dispatch = useDispatch();
41+
const { t } = useTranslation();
42+
43+
const fileNode =
44+
useSelector((state) => state.files.find((file) => file.id === id)) || {};
45+
const authenticated = useSelector((state) => state.user.authenticated);
46+
47+
const {
48+
name = '',
49+
parentId = null,
50+
children = [],
51+
fileType = 'file',
52+
isSelectedFile = false,
53+
isFolderClosed = false
54+
} = fileNode;
55+
8656
const [isOptionsOpen, setIsOptionsOpen] = useState(false);
8757
const [isEditingName, setIsEditingName] = useState(false);
8858
const [isDeleting, setIsDeleting] = useState(false);
8959
const [updatedName, setUpdatedName] = useState(name);
9060

91-
const { t } = useTranslation();
9261
const fileNameInput = useRef(null);
9362
const fileOptionsRef = useRef(null);
9463

9564
const handleFileClick = (event) => {
9665
event.stopPropagation();
9766
if (name !== 'root' && !isDeleting) {
98-
setSelectedFile(id);
67+
dispatch(IDEActions.setSelectedFile(id));
9968
}
10069
if (onClickFile) {
10170
onClickFile();
@@ -122,17 +91,17 @@ const FileNode = ({
12291
};
12392

12493
const handleClickAddFile = () => {
125-
newFile(id);
94+
dispatch(IDEActions.newFile(id));
12695
setTimeout(() => hideFileOptions(), 0);
12796
};
12897

12998
const handleClickAddFolder = () => {
130-
newFolder(id);
99+
dispatch(IDEActions.newFolder(id));
131100
setTimeout(() => hideFileOptions(), 0);
132101
};
133102

134103
const handleClickUploadFile = () => {
135-
openUploadFileModal(id);
104+
dispatch(IDEActions.openUploadFileModal(id));
136105
setTimeout(hideFileOptions, 0);
137106
};
138107

@@ -141,8 +110,8 @@ const FileNode = ({
141110

142111
if (window.confirm(prompt)) {
143112
setIsDeleting(true);
144-
resetSelectedFile(id);
145-
setTimeout(() => deleteFile(id, parentId), 100);
113+
dispatch(IDEActions.resetSelectedFile(id));
114+
setTimeout(() => dispatch(FileActions.deleteFile(id, parentId), 100));
146115
}
147116
};
148117

@@ -158,7 +127,7 @@ const FileNode = ({
158127

159128
const saveUpdatedFileName = () => {
160129
if (updatedName !== name) {
161-
updateFileName(id, updatedName);
130+
dispatch(FileActions.updateFileName(id, updatedName));
162131
}
163132
};
164133

@@ -243,7 +212,7 @@ const FileNode = ({
243212
<div className="sidebar__file-item--folder">
244213
<button
245214
className="sidebar__file-item-closed"
246-
onClick={() => showFolderChildren(id)}
215+
onClick={() => dispatch(FileActions.showFolderChildren(id))}
247216
aria-label={t('FileNode.OpenFolderARIA')}
248217
title={t('FileNode.OpenFolderARIA')}
249218
>
@@ -255,7 +224,7 @@ const FileNode = ({
255224
</button>
256225
<button
257226
className="sidebar__file-item-open"
258-
onClick={() => hideFolderChildren(id)}
227+
onClick={() => dispatch(FileActions.hideFolderChildren(id))}
259228
aria-label={t('FileNode.CloseFolderARIA')}
260229
title={t('FileNode.CloseFolderARIA')}
261230
>
@@ -353,7 +322,7 @@ const FileNode = ({
353322
<ul className="file-item__children">
354323
{children.map((childId) => (
355324
<li key={childId}>
356-
<ConnectedFileNode
325+
<FileNode
357326
id={childId}
358327
parentId={id}
359328
canEdit={canEdit}
@@ -369,50 +338,12 @@ const FileNode = ({
369338

370339
FileNode.propTypes = {
371340
id: PropTypes.string.isRequired,
372-
parentId: PropTypes.string,
373-
children: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
374-
name: PropTypes.string.isRequired,
375-
fileType: PropTypes.string.isRequired,
376-
isSelectedFile: PropTypes.bool,
377-
isFolderClosed: PropTypes.bool,
378-
setSelectedFile: PropTypes.func.isRequired,
379-
deleteFile: PropTypes.func.isRequired,
380-
updateFileName: PropTypes.func.isRequired,
381-
resetSelectedFile: PropTypes.func.isRequired,
382-
newFile: PropTypes.func.isRequired,
383-
newFolder: PropTypes.func.isRequired,
384-
showFolderChildren: PropTypes.func.isRequired,
385-
hideFolderChildren: PropTypes.func.isRequired,
386341
canEdit: PropTypes.bool.isRequired,
387-
openUploadFileModal: PropTypes.func.isRequired,
388-
authenticated: PropTypes.bool.isRequired,
389342
onClickFile: PropTypes.func
390343
};
391344

392345
FileNode.defaultProps = {
393-
onClickFile: null,
394-
parentId: '0',
395-
isSelectedFile: false,
396-
isFolderClosed: false
346+
onClickFile: null
397347
};
398348

399-
function mapStateToProps(state, ownProps) {
400-
// this is a hack, state is updated before ownProps
401-
const fileNode = state.files.find((file) => file.id === ownProps.id) || {
402-
name: 'test',
403-
fileType: 'file'
404-
};
405-
return Object.assign({}, fileNode, {
406-
authenticated: state.user.authenticated
407-
});
408-
}
409-
410-
const mapDispatchToProps = { ...FileActions, ...IDEActions };
411-
412-
const ConnectedFileNode = connect(
413-
mapStateToProps,
414-
mapDispatchToProps
415-
)(FileNode);
416-
417-
export { FileNode };
418-
export default ConnectedFileNode;
349+
export default FileNode;

Diff for: ‎client/modules/IDE/components/FileNode.unit.test.jsx

+68-44
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import React from 'react';
2+
import { Provider } from 'react-redux';
3+
import configureStore from 'redux-mock-store';
4+
import { useDispatch } from 'react-redux';
5+
import * as FileActions from '../actions/files';
26

37
import {
48
fireEvent,
@@ -7,9 +11,27 @@ import {
711
waitFor,
812
within
913
} from '../../../test-utils';
10-
import { FileNode } from './FileNode';
14+
import FileNode from './FileNode';
15+
16+
jest.mock('react-redux', () => ({
17+
...jest.requireActual('react-redux'),
18+
useDispatch: jest.fn()
19+
}));
20+
21+
jest.mock('../actions/files', () => ({
22+
updateFileName: jest.fn()
23+
}));
24+
25+
const mockStore = configureStore([]);
1126

1227
describe('<FileNode />', () => {
28+
const mockDispatch = jest.fn();
29+
30+
beforeEach(() => {
31+
useDispatch.mockReturnValue(mockDispatch);
32+
jest.clearAllMocks();
33+
});
34+
1335
const changeName = (newFileName) => {
1436
const renameButton = screen.getByText(/Rename/i);
1537
fireEvent.click(renameButton);
@@ -24,121 +46,123 @@ describe('<FileNode />', () => {
2446
await waitFor(() => within(name).queryByText(expectedName));
2547
};
2648

27-
const renderFileNode = (fileType, extraProps = {}) => {
28-
const props = {
29-
...extraProps,
30-
id: '0',
31-
name: fileType === 'folder' ? 'afolder' : 'test.jsx',
32-
fileType,
33-
canEdit: true,
34-
children: [],
35-
authenticated: false,
36-
setSelectedFile: jest.fn(),
37-
deleteFile: jest.fn(),
38-
updateFileName: jest.fn(),
39-
resetSelectedFile: jest.fn(),
40-
newFile: jest.fn(),
41-
newFolder: jest.fn(),
42-
showFolderChildren: jest.fn(),
43-
hideFolderChildren: jest.fn(),
44-
openUploadFileModal: jest.fn(),
45-
setProjectName: jest.fn()
49+
const renderFileNode = (fileType, extraState = {}) => {
50+
const initialState = {
51+
files: [
52+
{
53+
id: '0',
54+
name: fileType === 'folder' ? 'afolder' : 'test.jsx',
55+
fileType,
56+
parentId: 'root',
57+
children: [],
58+
isSelectedFile: false,
59+
isFolderClosed: false
60+
}
61+
],
62+
user: { authenticated: false },
63+
...extraState
4664
};
4765

48-
render(<FileNode {...props} />);
66+
const store = mockStore(initialState);
4967

50-
return props;
68+
render(
69+
<Provider store={store}>
70+
<FileNode id="0" canEdit />
71+
</Provider>
72+
);
73+
74+
return { store };
5175
};
5276

5377
describe('fileType: file', () => {
5478
it('cannot change to an empty name', async () => {
55-
const props = renderFileNode('file');
79+
renderFileNode('file');
5680

5781
changeName('');
5882

59-
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
60-
await expectFileNameToBe(props.name);
83+
await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
84+
await expectFileNameToBe('test.jsx');
6185
});
6286

6387
it('can change to a valid filename', async () => {
6488
const newName = 'newname.jsx';
65-
const props = renderFileNode('file');
89+
renderFileNode('file');
6690

6791
changeName(newName);
6892

6993
await waitFor(() =>
70-
expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName)
94+
expect(FileActions.updateFileName).toHaveBeenCalledWith('0', newName)
7195
);
7296
await expectFileNameToBe(newName);
7397
});
7498

7599
it('must have an extension', async () => {
76100
const newName = 'newname';
77-
const props = renderFileNode('file');
101+
renderFileNode('file');
78102

79103
changeName(newName);
80104

81-
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
82-
await expectFileNameToBe(props.name);
105+
await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
106+
await expectFileNameToBe('test.jsx');
83107
});
84108

85109
it('can change to a different extension', async () => {
86110
const mockConfirm = jest.fn(() => true);
87111
window.confirm = mockConfirm;
88112

89113
const newName = 'newname.gif';
90-
const props = renderFileNode('file');
114+
renderFileNode('file');
91115

92116
changeName(newName);
93117

94118
expect(mockConfirm).toHaveBeenCalled();
95119
await waitFor(() =>
96-
expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName)
120+
expect(FileActions.updateFileName).toHaveBeenCalledWith('0', newName)
97121
);
98-
await expectFileNameToBe(props.name);
122+
await expectFileNameToBe(newName);
99123
});
100124

101125
it('cannot be just an extension', async () => {
102126
const newName = '.jsx';
103-
const props = renderFileNode('file');
127+
renderFileNode('file');
104128

105129
changeName(newName);
106130

107-
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
108-
await expectFileNameToBe(props.name);
131+
await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
132+
await expectFileNameToBe('test.jsx');
109133
});
110134
});
111135

112136
describe('fileType: folder', () => {
113137
it('cannot change to an empty name', async () => {
114-
const props = renderFileNode('folder');
138+
renderFileNode('folder');
115139

116140
changeName('');
117141

118-
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
119-
await expectFileNameToBe(props.name);
142+
await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
143+
await expectFileNameToBe('afolder');
120144
});
121145

122146
it('can change to another name', async () => {
123147
const newName = 'foldername';
124-
const props = renderFileNode('folder');
148+
renderFileNode('folder');
125149

126150
changeName(newName);
127151

128152
await waitFor(() =>
129-
expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName)
153+
expect(FileActions.updateFileName).toHaveBeenCalledWith('0', newName)
130154
);
131155
await expectFileNameToBe(newName);
132156
});
133157

134158
it('cannot have a file extension', async () => {
135159
const newName = 'foldername.jsx';
136-
const props = renderFileNode('folder');
160+
renderFileNode('folder');
137161

138162
changeName(newName);
139163

140-
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
141-
await expectFileNameToBe(props.name);
164+
await waitFor(() => expect(mockDispatch).not.toHaveBeenCalled());
165+
await expectFileNameToBe('afolder');
142166
});
143167
});
144168
});

Diff for: ‎client/modules/IDE/components/Sidebar.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { selectRootFile } from '../selectors/files';
1414
import { getAuthenticated, selectCanEditSketch } from '../selectors/users';
1515

16-
import ConnectedFileNode from './FileNode';
16+
import FileNode from './FileNode';
1717
import { PlusIcon } from '../../../common/icons';
1818
import { FileDrawer } from './Editor/MobileEditor';
1919

@@ -130,7 +130,7 @@ export default function SideBar() {
130130
</ul>
131131
</div>
132132
</header>
133-
<ConnectedFileNode id={rootFile.id} canEdit={canEditProject} />
133+
<FileNode id={rootFile.id} canEdit={canEditProject} />
134134
</section>
135135
</FileDrawer>
136136
);

Diff for: ‎client/modules/IDE/utils/parseFileName.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
function parseFileName(name) {
2+
const nameArray = name.split('.');
3+
if (nameArray.length > 1) {
4+
const extension = `.${nameArray[nameArray.length - 1]}`;
5+
const baseName = nameArray.slice(0, -1).join('.');
6+
const firstLetter = baseName[0];
7+
const lastLetter = baseName[baseName.length - 1];
8+
const middleText = baseName.slice(1, -1);
9+
return {
10+
baseName,
11+
firstLetter,
12+
lastLetter,
13+
middleText,
14+
extension
15+
};
16+
}
17+
const firstLetter = name[0];
18+
const lastLetter = name[name.length - 1];
19+
const middleText = name.slice(1, -1);
20+
return {
21+
baseName: name,
22+
firstLetter,
23+
lastLetter,
24+
middleText
25+
};
26+
}
27+
28+
export default parseFileName;

0 commit comments

Comments
 (0)
Please sign in to comment.