Skip to content

Commit 0ec02d6

Browse files
committed
convert Sidebar to a function component & connect it to Redux
1 parent 7212f76 commit 0ec02d6

File tree

3 files changed

+116
-185
lines changed

3 files changed

+116
-185
lines changed
+108-152
Original file line numberDiff line numberDiff line change
@@ -1,178 +1,134 @@
1-
import PropTypes from 'prop-types';
2-
import React from 'react';
1+
import React, { useRef, useState } from 'react';
32
import classNames from 'classnames';
4-
import { withTranslation } from 'react-i18next';
3+
import { useTranslation } from 'react-i18next';
4+
import { useDispatch, useSelector } from 'react-redux';
5+
import {
6+
closeProjectOptions,
7+
newFile,
8+
newFolder,
9+
openProjectOptions,
10+
openUploadFileModal
11+
} from '../actions/ide';
12+
import { getAuthenticated, selectCanEditSketch } from '../selectors/users';
513

614
import ConnectedFileNode from './FileNode';
715

816
import DownArrowIcon from '../../../images/down-filled-triangle.svg';
917

10-
class Sidebar extends React.Component {
11-
constructor(props) {
12-
super(props);
13-
this.resetSelectedFile = this.resetSelectedFile.bind(this);
14-
this.toggleProjectOptions = this.toggleProjectOptions.bind(this);
15-
this.onBlurComponent = this.onBlurComponent.bind(this);
16-
this.onFocusComponent = this.onFocusComponent.bind(this);
18+
// TODO: use a generic Dropdown UI component
1719

18-
this.state = {
19-
isFocused: false
20-
};
21-
}
20+
export default function SideBar() {
21+
const { t } = useTranslation();
22+
const dispatch = useDispatch();
2223

23-
onBlurComponent() {
24-
this.setState({ isFocused: false });
24+
const [isFocused, setIsFocused] = useState(false);
25+
26+
const files = useSelector((state) => state.files);
27+
// TODO: use `selectRootFile` defined in another PR
28+
const rootFile = files.filter((file) => file.name === 'root')[0];
29+
const projectOptionsVisible = useSelector(
30+
(state) => state.ide.projectOptionsVisible
31+
);
32+
const isExpanded = useSelector((state) => state.ide.sidebarIsExpanded);
33+
const canEditProject = useSelector(selectCanEditSketch);
34+
const isAuthenticated = useSelector(getAuthenticated);
35+
36+
const sidebarOptionsRef = useRef(null);
37+
38+
const onBlurComponent = () => {
39+
setIsFocused(false);
2540
setTimeout(() => {
26-
if (!this.state.isFocused) {
27-
this.props.closeProjectOptions();
41+
if (!isFocused) {
42+
dispatch(closeProjectOptions());
2843
}
2944
}, 200);
30-
}
45+
};
3146

32-
onFocusComponent() {
33-
this.setState({ isFocused: true });
34-
}
47+
const onFocusComponent = () => {
48+
setIsFocused(true);
49+
};
3550

36-
resetSelectedFile() {
37-
this.props.setSelectedFile(this.props.files[1].id);
38-
}
39-
40-
toggleProjectOptions(e) {
51+
const toggleProjectOptions = (e) => {
4152
e.preventDefault();
42-
if (this.props.projectOptionsVisible) {
43-
this.props.closeProjectOptions();
53+
if (projectOptionsVisible) {
54+
dispatch(closeProjectOptions());
4455
} else {
45-
this.sidebarOptions.focus();
46-
this.props.openProjectOptions();
56+
sidebarOptionsRef.current?.focus();
57+
dispatch(openProjectOptions());
4758
}
48-
}
59+
};
4960

50-
userCanEditProject() {
51-
let canEdit;
52-
if (!this.props.owner) {
53-
canEdit = true;
54-
} else if (
55-
this.props.user.authenticated &&
56-
this.props.owner.id === this.props.user.id
57-
) {
58-
canEdit = true;
59-
} else {
60-
canEdit = false;
61-
}
62-
return canEdit;
63-
}
64-
65-
render() {
66-
const canEditProject = this.userCanEditProject();
67-
const sidebarClass = classNames({
68-
sidebar: true,
69-
'sidebar--contracted': !this.props.isExpanded,
70-
'sidebar--project-options': this.props.projectOptionsVisible,
71-
'sidebar--cant-edit': !canEditProject
72-
});
73-
const rootFile = this.props.files.filter((file) => file.name === 'root')[0];
61+
const sidebarClass = classNames({
62+
sidebar: true,
63+
'sidebar--contracted': !isExpanded,
64+
'sidebar--project-options': projectOptionsVisible,
65+
'sidebar--cant-edit': !canEditProject
66+
});
7467

75-
return (
76-
<section className={sidebarClass}>
77-
<header
78-
className="sidebar__header"
79-
onContextMenu={this.toggleProjectOptions}
80-
>
81-
<h3 className="sidebar__title">
82-
<span>{this.props.t('Sidebar.Title')}</span>
83-
</h3>
84-
<div className="sidebar__icons">
85-
<button
86-
aria-label={this.props.t('Sidebar.ToggleARIA')}
87-
className="sidebar__add"
88-
tabIndex="0"
89-
ref={(element) => {
90-
this.sidebarOptions = element;
91-
}}
92-
onClick={this.toggleProjectOptions}
93-
onBlur={this.onBlurComponent}
94-
onFocus={this.onFocusComponent}
95-
>
96-
<DownArrowIcon focusable="false" aria-hidden="true" />
97-
</button>
98-
<ul className="sidebar__project-options">
68+
return (
69+
<section className={sidebarClass}>
70+
<header className="sidebar__header" onContextMenu={toggleProjectOptions}>
71+
<h3 className="sidebar__title">
72+
<span>{t('Sidebar.Title')}</span>
73+
</h3>
74+
<div className="sidebar__icons">
75+
<button
76+
aria-label={t('Sidebar.ToggleARIA')}
77+
className="sidebar__add"
78+
tabIndex="0"
79+
ref={sidebarOptionsRef}
80+
onClick={toggleProjectOptions}
81+
onBlur={onBlurComponent}
82+
onFocus={onFocusComponent}
83+
>
84+
<DownArrowIcon focusable="false" aria-hidden="true" />
85+
</button>
86+
<ul className="sidebar__project-options">
87+
<li>
88+
<button
89+
aria-label={t('Sidebar.AddFolderARIA')}
90+
onClick={() => {
91+
dispatch(newFolder(rootFile.id));
92+
setTimeout(() => dispatch(closeProjectOptions()), 0);
93+
}}
94+
onBlur={onBlurComponent}
95+
onFocus={onFocusComponent}
96+
>
97+
{t('Sidebar.AddFolder')}
98+
</button>
99+
</li>
100+
<li>
101+
<button
102+
aria-label={t('Sidebar.AddFileARIA')}
103+
onClick={() => {
104+
dispatch(newFile(rootFile.id));
105+
setTimeout(() => dispatch(closeProjectOptions()), 0);
106+
}}
107+
onBlur={onBlurComponent}
108+
onFocus={onFocusComponent}
109+
>
110+
{t('Sidebar.AddFile')}
111+
</button>
112+
</li>
113+
{isAuthenticated && (
99114
<li>
100115
<button
101-
aria-label={this.props.t('Sidebar.AddFolderARIA')}
116+
aria-label={t('Sidebar.UploadFileARIA')}
102117
onClick={() => {
103-
this.props.newFolder(rootFile.id);
104-
setTimeout(this.props.closeProjectOptions, 0);
118+
dispatch(openUploadFileModal(rootFile.id));
119+
setTimeout(() => dispatch(closeProjectOptions()), 0);
105120
}}
106-
onBlur={this.onBlurComponent}
107-
onFocus={this.onFocusComponent}
121+
onBlur={onBlurComponent}
122+
onFocus={onFocusComponent}
108123
>
109-
{this.props.t('Sidebar.AddFolder')}
124+
{t('Sidebar.UploadFile')}
110125
</button>
111126
</li>
112-
<li>
113-
<button
114-
aria-label={this.props.t('Sidebar.AddFileARIA')}
115-
onClick={() => {
116-
this.props.newFile(rootFile.id);
117-
setTimeout(this.props.closeProjectOptions, 0);
118-
}}
119-
onBlur={this.onBlurComponent}
120-
onFocus={this.onFocusComponent}
121-
>
122-
{this.props.t('Sidebar.AddFile')}
123-
</button>
124-
</li>
125-
{this.props.user.authenticated && (
126-
<li>
127-
<button
128-
aria-label={this.props.t('Sidebar.UploadFileARIA')}
129-
onClick={() => {
130-
this.props.openUploadFileModal(rootFile.id);
131-
setTimeout(this.props.closeProjectOptions, 0);
132-
}}
133-
onBlur={this.onBlurComponent}
134-
onFocus={this.onFocusComponent}
135-
>
136-
{this.props.t('Sidebar.UploadFile')}
137-
</button>
138-
</li>
139-
)}
140-
</ul>
141-
</div>
142-
</header>
143-
<ConnectedFileNode id={rootFile.id} canEdit={canEditProject} />
144-
</section>
145-
);
146-
}
127+
)}
128+
</ul>
129+
</div>
130+
</header>
131+
<ConnectedFileNode id={rootFile.id} canEdit={canEditProject} />
132+
</section>
133+
);
147134
}
148-
149-
Sidebar.propTypes = {
150-
files: PropTypes.arrayOf(
151-
PropTypes.shape({
152-
name: PropTypes.string.isRequired,
153-
id: PropTypes.string.isRequired
154-
})
155-
).isRequired,
156-
setSelectedFile: PropTypes.func.isRequired,
157-
isExpanded: PropTypes.bool.isRequired,
158-
projectOptionsVisible: PropTypes.bool.isRequired,
159-
newFile: PropTypes.func.isRequired,
160-
openProjectOptions: PropTypes.func.isRequired,
161-
closeProjectOptions: PropTypes.func.isRequired,
162-
newFolder: PropTypes.func.isRequired,
163-
openUploadFileModal: PropTypes.func.isRequired,
164-
owner: PropTypes.shape({
165-
id: PropTypes.string
166-
}),
167-
user: PropTypes.shape({
168-
id: PropTypes.string,
169-
authenticated: PropTypes.bool.isRequired
170-
}).isRequired,
171-
t: PropTypes.func.isRequired
172-
};
173-
174-
Sidebar.defaultProps = {
175-
owner: undefined
176-
};
177-
178-
export default withTranslation()(Sidebar);

client/modules/IDE/pages/IDEView.jsx

+1-32
Original file line numberDiff line numberDiff line change
@@ -302,22 +302,7 @@ class IDEView extends React.Component {
302302
allowResize={this.props.ide.sidebarIsExpanded}
303303
minSize={125}
304304
>
305-
<Sidebar
306-
files={this.props.files}
307-
setSelectedFile={this.props.setSelectedFile}
308-
newFile={this.props.newFile}
309-
isExpanded={this.props.ide.sidebarIsExpanded}
310-
deleteFile={this.props.deleteFile}
311-
updateFileName={this.props.updateFileName}
312-
projectOptionsVisible={this.props.ide.projectOptionsVisible}
313-
openProjectOptions={this.props.openProjectOptions}
314-
closeProjectOptions={this.props.closeProjectOptions}
315-
newFolder={this.props.newFolder}
316-
user={this.props.user}
317-
owner={this.props.project.owner}
318-
openUploadFileModal={this.props.openUploadFileModal}
319-
closeUploadFileModal={this.props.closeUploadFileModal}
320-
/>
305+
<Sidebar />
321306
<SplitPane
322307
split="vertical"
323308
defaultSize="50%"
@@ -522,36 +507,22 @@ IDEView.propTypes = {
522507
setTextOutput: PropTypes.func.isRequired,
523508
setGridOutput: PropTypes.func.isRequired,
524509
setAllAccessibleOutput: PropTypes.func.isRequired,
525-
files: PropTypes.arrayOf(
526-
PropTypes.shape({
527-
id: PropTypes.string.isRequired,
528-
name: PropTypes.string.isRequired,
529-
content: PropTypes.string.isRequired
530-
})
531-
).isRequired,
532510
selectedFile: PropTypes.shape({
533511
id: PropTypes.string.isRequired,
534512
content: PropTypes.string.isRequired,
535513
name: PropTypes.string.isRequired
536514
}).isRequired,
537-
setSelectedFile: PropTypes.func.isRequired,
538515
htmlFile: PropTypes.shape({
539516
id: PropTypes.string.isRequired,
540517
name: PropTypes.string.isRequired,
541518
content: PropTypes.string.isRequired
542519
}).isRequired,
543-
newFile: PropTypes.func.isRequired,
544520
expandSidebar: PropTypes.func.isRequired,
545521
collapseSidebar: PropTypes.func.isRequired,
546522
cloneProject: PropTypes.func.isRequired,
547523
expandConsole: PropTypes.func.isRequired,
548524
collapseConsole: PropTypes.func.isRequired,
549-
deleteFile: PropTypes.func.isRequired,
550-
updateFileName: PropTypes.func.isRequired,
551525
updateFileContent: PropTypes.func.isRequired,
552-
openProjectOptions: PropTypes.func.isRequired,
553-
closeProjectOptions: PropTypes.func.isRequired,
554-
newFolder: PropTypes.func.isRequired,
555526
closeNewFolderModal: PropTypes.func.isRequired,
556527
closeNewFileModal: PropTypes.func.isRequired,
557528
closeShareModal: PropTypes.func.isRequired,
@@ -567,15 +538,13 @@ IDEView.propTypes = {
567538
hideErrorModal: PropTypes.func.isRequired,
568539
clearPersistedState: PropTypes.func.isRequired,
569540
startSketch: PropTypes.func.isRequired,
570-
openUploadFileModal: PropTypes.func.isRequired,
571541
closeUploadFileModal: PropTypes.func.isRequired,
572542
t: PropTypes.func.isRequired,
573543
isUserOwner: PropTypes.bool.isRequired
574544
};
575545

576546
function mapStateToProps(state) {
577547
return {
578-
files: state.files,
579548
selectedFile:
580549
state.files.find((file) => file.isSelectedFile) ||
581550
state.files.find((file) => file.name === 'sketch.js') ||

client/modules/IDE/selectors/users.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createSelector } from 'reselect';
22
import getConfig from '../../../utils/getConfig';
33

4-
const getAuthenticated = (state) => state.user.authenticated;
4+
export const getAuthenticated = (state) => state.user.authenticated;
55
const getTotalSize = (state) => state.user.totalSize;
66
const getAssetsTotalSize = (state) => state.assets.totalSize;
77
const getSketchOwner = (state) => state.project.owner;
@@ -39,3 +39,9 @@ export const getIsUserOwner = createSelector(
3939
return sketchOwner.id === userId;
4040
}
4141
);
42+
43+
export const selectCanEditSketch = createSelector(
44+
getSketchOwner,
45+
getIsUserOwner,
46+
(sketchOwner, isOwner) => !sketchOwner || isOwner
47+
);

0 commit comments

Comments
 (0)