Skip to content

Commit 3c45d46

Browse files
authored
Merge branch 'develop' into feat/3450
2 parents 3e7657f + f9689fe commit 3c45d46

31 files changed

+1326
-130
lines changed

client/common/icons.jsx

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import CircleInfo from '../images/circle-info.svg';
2525
import Add from '../images/add.svg';
2626
import Filter from '../images/filter.svg';
2727
import Cross from '../images/cross.svg';
28+
import Copy from '../images/copy.svg';
2829

2930
// HOC that adds the right web accessibility props
3031
// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html
@@ -102,3 +103,4 @@ export const CircleFolderIcon = withLabel(CircleFolder);
102103
export const CircleInfoIcon = withLabel(CircleInfo);
103104
export const AddIcon = withLabel(Add);
104105
export const FilterIcon = withLabel(Filter);
106+
export const CopyIcon = withLabel(Copy);

client/components/Dropdown/DropdownMenu.jsx

+15-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ import { DropdownWrapper } from '../Dropdown';
88

99
const DropdownMenu = forwardRef(
1010
(
11-
{ children, anchor, 'aria-label': ariaLabel, align, className, classes },
11+
{
12+
children,
13+
anchor,
14+
'aria-label': ariaLabel,
15+
align,
16+
className,
17+
classes,
18+
maxHeight
19+
},
1220
ref
1321
) => {
1422
// Note: need to use a ref instead of a state to avoid stale closures.
@@ -32,7 +40,7 @@ const DropdownMenu = forwardRef(
3240
focusedRef.current = false;
3341
setTimeout(() => {
3442
if (!focusedRef.current) {
35-
close();
43+
// close();
3644
}
3745
}, 200);
3846
};
@@ -59,6 +67,7 @@ const DropdownMenu = forwardRef(
5967
}}
6068
onBlur={handleBlur}
6169
onFocus={handleFocus}
70+
style={maxHeight && { maxHeight, overflowY: 'auto' }}
6271
>
6372
{children}
6473
</DropdownWrapper>
@@ -84,14 +93,16 @@ DropdownMenu.propTypes = {
8493
classes: PropTypes.shape({
8594
button: PropTypes.string,
8695
list: PropTypes.string
87-
})
96+
}),
97+
maxHeight: PropTypes.string
8898
};
8999

90100
DropdownMenu.defaultProps = {
91101
anchor: null,
92102
align: 'right',
93103
className: '',
94-
classes: {}
104+
classes: {},
105+
maxHeight: undefined
95106
};
96107

97108
export default DropdownMenu;

client/constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const STOP_ACCESSIBLE_OUTPUT = 'STOP_ACCESSIBLE_OUTPUT';
1010

1111
export const OPEN_PREFERENCES = 'OPEN_PREFERENCES';
1212
export const CLOSE_PREFERENCES = 'CLOSE_PREFERENCES';
13+
export const SET_PREFERENCES_TAB = 'SET_PREFERENCES_TAB';
1314
export const SET_FONT_SIZE = 'SET_FONT_SIZE';
1415
export const SET_LINE_NUMBERS = 'SET_LINE_NUMBERS';
1516

client/images/copy.svg

+6
Loading

client/images/pencil.svg

+1-1
Loading

client/modules/IDE/actions/preferences.js

+7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ function updatePreferences(formParams, dispatch) {
1414
});
1515
}
1616

17+
export function setPreferencesTab(value) {
18+
return {
19+
type: ActionTypes.SET_PREFERENCES_TAB,
20+
value
21+
};
22+
}
23+
1724
export function setFontSize(value) {
1825
return (dispatch, getState) => {
1926
// eslint-disable-line
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
export default function Admonition({ children, title }) {
5+
return (
6+
<div className="admonition">
7+
<p className="admonition__title">
8+
<strong>{title}</strong>
9+
</p>
10+
{children}
11+
</div>
12+
);
13+
}
14+
15+
Admonition.propTypes = {
16+
title: PropTypes.string.isRequired,
17+
children: PropTypes.node
18+
};
19+
20+
Admonition.defaultProps = {
21+
children: undefined
22+
};

client/modules/IDE/components/Editor/index.jsx

+18-2
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ class Editor extends React.Component {
104104
this.showFind = this.showFind.bind(this);
105105
this.showReplace = this.showReplace.bind(this);
106106
this.getContent = this.getContent.bind(this);
107+
this.updateFileContent = this.updateFileContent.bind(this);
107108
}
108109

109110
componentDidMount() {
@@ -220,7 +221,8 @@ class Editor extends React.Component {
220221
tidyCode: this.tidyCode,
221222
showFind: this.showFind,
222223
showReplace: this.showReplace,
223-
getContent: this.getContent
224+
getContent: this.getContent,
225+
updateFileContent: this.updateFileContent
224226
});
225227
}
226228

@@ -254,6 +256,9 @@ class Editor extends React.Component {
254256
if (!prevProps.unsavedChanges) {
255257
setTimeout(() => this.props.setUnsavedChanges(false), 400);
256258
}
259+
} else if (this.getContent().content !== this.props.file.content) {
260+
// TODO: make this not break regular edits!
261+
// this._cm.setValue(this.props.file.content);
257262
}
258263
if (this.props.fontSize !== prevProps.fontSize) {
259264
this._cm.getWrapperElement().style[
@@ -329,7 +334,8 @@ class Editor extends React.Component {
329334
tidyCode: this.tidyCode,
330335
showFind: this.showFind,
331336
showReplace: this.showReplace,
332-
getContent: this.getContent
337+
getContent: this.getContent,
338+
updateFileContent: this.updateFileContent
333339
});
334340
}
335341

@@ -366,6 +372,16 @@ class Editor extends React.Component {
366372
return updatedFile;
367373
}
368374

375+
updateFileContent(id, src) {
376+
const file = this._docs[id];
377+
if (file) {
378+
this._docs[id] = CodeMirror.Doc(src, this._docs[id].modeOption);
379+
if (id === this.props.file.id) {
380+
this._cm.swapDoc(this._docs[id]);
381+
}
382+
}
383+
}
384+
369385
handleKeyUp = () => {
370386
const lineNumber = parseInt(this._cm.getCursor().line + 1, 10);
371387
this.setState({ currentLine: lineNumber });

client/modules/IDE/components/FileNode.jsx

+28-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import classNames from 'classnames';
33
import React, { useState, useRef } from 'react';
44
import { connect } from 'react-redux';
55
import { useTranslation } from 'react-i18next';
6+
import { useSelector } from 'react-redux';
67

78
import * as IDEActions from '../actions/ide';
89
import * as FileActions from '../actions/files';
@@ -88,6 +89,24 @@ const FileNode = ({
8889
const [isDeleting, setIsDeleting] = useState(false);
8990
const [updatedName, setUpdatedName] = useState(name);
9091

92+
const files = useSelector((state) => state.files);
93+
94+
const checkDuplicate = (newName) => {
95+
const parentFolder = files.find((f) => f.id === parentId);
96+
if (!parentFolder) return false;
97+
98+
const siblingFiles = parentFolder.children
99+
.map((childId) => files.find((f) => f.id === childId))
100+
.filter(Boolean)
101+
.filter((file) => file.id !== id);
102+
103+
const isDuplicate = siblingFiles.some(
104+
(f) => f.name.trim().toLowerCase() === newName.trim().toLowerCase()
105+
);
106+
107+
return isDuplicate;
108+
};
109+
91110
const { t } = useTranslation();
92111
const fileNameInput = useRef(null);
93112
const fileOptionsRef = useRef(null);
@@ -157,7 +176,7 @@ const FileNode = ({
157176
};
158177

159178
const saveUpdatedFileName = () => {
160-
if (updatedName !== name) {
179+
if (!checkDuplicate(updatedName) && updatedName !== name) {
161180
updateFileName(id, updatedName);
162181
}
163182
};
@@ -198,8 +217,13 @@ const FileNode = ({
198217
};
199218

200219
const handleFileNameBlur = () => {
201-
validateFileName();
202-
hideEditFileName();
220+
if (!checkDuplicate(updatedName)) {
221+
validateFileName();
222+
hideEditFileName();
223+
} else {
224+
setUpdatedName(name);
225+
hideEditFileName();
226+
}
203227
};
204228

205229
const toggleFileOptions = (event) => {
@@ -271,6 +295,7 @@ const FileNode = ({
271295
aria-label={updatedName}
272296
className="sidebar__file-item-name"
273297
onClick={handleFileClick}
298+
onDoubleClick={handleClickRename}
274299
data-testid="file-name"
275300
>
276301
<FileName name={updatedName} />

client/modules/IDE/components/FileNode.unit.test.jsx

+40-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import React from 'react';
2+
import { Provider } from 'react-redux';
3+
import configureStore from 'redux-mock-store';
4+
import thunk from 'redux-thunk';
25

36
import {
47
fireEvent,
@@ -9,6 +12,8 @@ import {
912
} from '../../../test-utils';
1013
import { FileNode } from './FileNode';
1114

15+
const mockStore = configureStore([thunk]);
16+
1217
describe('<FileNode />', () => {
1318
const changeName = (newFileName) => {
1419
const renameButton = screen.getByText(/Rename/i);
@@ -32,6 +37,7 @@ describe('<FileNode />', () => {
3237
fileType,
3338
canEdit: true,
3439
children: [],
40+
parentId: 'parent-folder-id',
3541
authenticated: false,
3642
setSelectedFile: jest.fn(),
3743
deleteFile: jest.fn(),
@@ -45,7 +51,40 @@ describe('<FileNode />', () => {
4551
setProjectName: jest.fn()
4652
};
4753

48-
render(<FileNode {...props} />);
54+
const mockFiles = [
55+
{
56+
id: '0',
57+
name: props.name,
58+
parentId: 'parent-folder-id'
59+
},
60+
{
61+
id: '1',
62+
name: 'sketch.js',
63+
parentId: '0',
64+
isSelectedFile: true
65+
},
66+
{
67+
id: 'parent-folder-id',
68+
name: 'parent',
69+
parentId: null,
70+
children: ['0', 'some-other-file-id']
71+
},
72+
{
73+
id: 'some-other-file-id',
74+
name: 'duplicate.js',
75+
parentId: 'parent-folder-id'
76+
}
77+
];
78+
79+
const store = mockStore({
80+
files: mockFiles
81+
});
82+
83+
render(
84+
<Provider store={store}>
85+
<FileNode {...props} />
86+
</Provider>
87+
);
4988

5089
return props;
5190
};

client/modules/IDE/components/Header/Toolbar.jsx

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import PlayIcon from '../../../../images/play.svg';
2020
import StopIcon from '../../../../images/stop.svg';
2121
import PreferencesIcon from '../../../../images/preferences.svg';
2222
import ProjectName from './ProjectName';
23+
import VersionIndicator from '../VersionIndicator';
2324

2425
const Toolbar = (props) => {
2526
const { isPlaying, infiniteLoop, preferencesIsVisible } = useSelector(
@@ -113,6 +114,8 @@ const Toolbar = (props) => {
113114
return null;
114115
})()}
115116
</div>
117+
<VersionIndicator />
118+
<div style={{ flex: 1 }} />
116119
<button
117120
className={preferencesButtonClass}
118121
onClick={() => dispatch(openPreferences())}

0 commit comments

Comments
 (0)