{
- this.codemirrorContainer = element;
+ return (
+
+ );
}
ConsoleInput.propTypes = {
diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx
index de19d664fc..f893d246af 100644
--- a/client/modules/IDE/components/FileNode.jsx
+++ b/client/modules/IDE/components/FileNode.jsx
@@ -1,10 +1,9 @@
import PropTypes from 'prop-types';
-import React from 'react';
-import { bindActionCreators } from 'redux';
-import { connect } from 'react-redux';
import classNames from 'classnames';
-import { withTranslation } from 'react-i18next';
-
+import React, { useState, useRef } from 'react';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import { useTranslation } from 'react-i18next';
import * as IDEActions from '../actions/ide';
import * as FileActions from '../actions/files';
import DownArrowIcon from '../../../images/down-filled-triangle.svg';
@@ -63,120 +62,113 @@ FileName.propTypes = {
name: PropTypes.string.isRequired
};
-class FileNode extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- isOptionsOpen: false,
- isEditingName: false,
- isFocused: false,
- isDeleting: false,
- updatedName: this.props.name
- };
- }
-
- onFocusComponent = () => {
- this.setState({ isFocused: true });
- };
-
- onBlurComponent = () => {
- this.setState({ isFocused: false });
- setTimeout(() => {
- if (!this.state.isFocused) {
- this.hideFileOptions();
- }
- }, 200);
- };
-
- setUpdatedName = (updatedName) => {
- this.setState({ updatedName });
- };
-
- saveUpdatedFileName = () => {
- const { updatedName } = this.state;
- const { name, updateFileName, id } = this.props;
-
- if (updatedName !== name) {
- updateFileName(id, updatedName);
- }
- };
-
- handleFileClick = (event) => {
+const FileNode = ({
+ id,
+ parentId,
+ children,
+ name,
+ fileType,
+ isSelectedFile,
+ isFolderClosed,
+ setSelectedFile,
+ deleteFile,
+ updateFileName,
+ resetSelectedFile,
+ newFile,
+ newFolder,
+ showFolderChildren,
+ hideFolderChildren,
+ canEdit,
+ openUploadFileModal,
+ authenticated,
+ onClickFile
+}) => {
+ const [isOptionsOpen, setIsOptionsOpen] = useState(false);
+ const [isEditingName, setIsEditingName] = useState(false);
+ const [isDeleting, setIsDeleting] = useState(false);
+ const [updatedName, setUpdatedName] = useState(name);
+
+ const { t } = useTranslation();
+ const fileNameInput = useRef(null);
+ const fileOptionsRef = useRef(null);
+
+ const handleFileClick = (event) => {
event.stopPropagation();
- const { isDeleting } = this.state;
- const { id, setSelectedFile, name, onClickFile } = this.props;
if (name !== 'root' && !isDeleting) {
setSelectedFile(id);
}
-
- // debugger; // eslint-disable-line
if (onClickFile) {
onClickFile();
}
};
- handleFileNameChange = (event) => {
- const newName = event.target.value;
- this.setUpdatedName(newName);
+ const handleFileNameChange = (event) => {
+ setUpdatedName(event.target.value);
};
- handleFileNameBlur = () => {
- this.validateFileName();
- this.hideEditFileName();
+ const showEditFileName = () => {
+ setIsEditingName(true);
};
- handleClickRename = () => {
- this.setUpdatedName(this.props.name);
- this.showEditFileName();
- setTimeout(() => this.fileNameInput.focus(), 0);
- setTimeout(() => this.hideFileOptions(), 0);
+ const hideFileOptions = () => {
+ setIsOptionsOpen(false);
};
- handleClickAddFile = () => {
- this.props.newFile(this.props.id);
- setTimeout(() => this.hideFileOptions(), 0);
+ const handleClickRename = () => {
+ setUpdatedName(name);
+ showEditFileName();
+ setTimeout(() => fileNameInput.current.focus(), 0);
+ setTimeout(() => hideFileOptions(), 0);
};
- handleClickAddFolder = () => {
- this.props.newFolder(this.props.id);
- setTimeout(() => this.hideFileOptions(), 0);
+ const handleClickAddFile = () => {
+ newFile(id);
+ setTimeout(() => hideFileOptions(), 0);
};
- handleClickUploadFile = () => {
- this.props.openUploadFileModal(this.props.id);
- setTimeout(this.hideFileOptions, 0);
+ const handleClickAddFolder = () => {
+ newFolder(id);
+ setTimeout(() => hideFileOptions(), 0);
};
- handleClickDelete = () => {
- const prompt = this.props.t('Common.DeleteConfirmation', {
- name: this.props.name
- });
+ const handleClickUploadFile = () => {
+ openUploadFileModal(id);
+ setTimeout(hideFileOptions, 0);
+ };
+
+ const handleClickDelete = () => {
+ const prompt = t('Common.DeleteConfirmation', { name });
if (window.confirm(prompt)) {
- this.setState({ isDeleting: true });
- this.props.resetSelectedFile(this.props.id);
- setTimeout(
- () => this.props.deleteFile(this.props.id, this.props.parentId),
- 100
- );
+ setIsDeleting(true);
+ resetSelectedFile(id);
+ setTimeout(() => deleteFile(id, parentId), 100);
}
};
- handleKeyPress = (event) => {
+ const hideEditFileName = () => {
+ setIsEditingName(false);
+ };
+
+ const handleKeyPress = (event) => {
if (event.key === 'Enter') {
- this.hideEditFileName();
+ hideEditFileName();
+ }
+ };
+
+ const saveUpdatedFileName = () => {
+ if (updatedName !== name) {
+ updateFileName(id, updatedName);
}
};
- validateFileName = () => {
- const currentName = this.props.name;
- const { updatedName } = this.state;
+ const validateFileName = () => {
+ const currentName = name;
const oldFileExtension = currentName.match(/\.[0-9a-z]+$/i);
const newFileExtension = updatedName.match(/\.[0-9a-z]+$/i);
const hasPeriod = updatedName.match(/\.+/);
const hasNoExtension = oldFileExtension && !newFileExtension;
- const hasExtensionIfFolder = this.props.fileType === 'folder' && hasPeriod;
+ const hasExtensionIfFolder = fileType === 'folder' && hasPeriod;
const notSameExtension =
oldFileExtension &&
newFileExtension &&
@@ -190,234 +182,185 @@ class FileNode extends React.Component {
hasOnlyExtension ||
hasExtensionIfFolder
) {
- this.setUpdatedName(currentName);
+ setUpdatedName(currentName);
} else if (notSameExtension) {
const userResponse = window.confirm(
'Are you sure you want to change the file extension?'
);
if (userResponse) {
- this.saveUpdatedFileName();
+ saveUpdatedFileName();
} else {
- this.setUpdatedName(currentName);
+ setUpdatedName(currentName);
}
} else {
- this.saveUpdatedFileName();
+ saveUpdatedFileName();
}
};
- toggleFileOptions = (event) => {
+ const handleFileNameBlur = () => {
+ validateFileName();
+ hideEditFileName();
+ };
+
+ const toggleFileOptions = (event) => {
event.preventDefault();
- if (!this.props.canEdit) {
+ if (!canEdit) {
return;
}
- if (this.state.isOptionsOpen) {
- this.setState({ isOptionsOpen: false });
- } else {
- this[`fileOptions-${this.props.id}`].focus();
- this.setState({ isOptionsOpen: true });
- }
- };
-
- hideFileOptions = () => {
- this.setState({ isOptionsOpen: false });
- };
-
- showEditFileName = () => {
- this.setState({ isEditingName: true });
+ setIsOptionsOpen(!isOptionsOpen);
};
- hideEditFileName = () => {
- this.setState({ isEditingName: false });
- };
-
- showFolderChildren = () => {
- this.props.showFolderChildren(this.props.id);
- };
-
- hideFolderChildren = () => {
- this.props.hideFolderChildren(this.props.id);
- };
-
- renderChild = (childId) => (
-
-
-
- );
-
- render() {
- const itemClass = classNames({
- 'sidebar__root-item': this.props.name === 'root',
- 'sidebar__file-item': this.props.name !== 'root',
- 'sidebar__file-item--selected': this.props.isSelectedFile,
- 'sidebar__file-item--open': this.state.isOptionsOpen,
- 'sidebar__file-item--editing': this.state.isEditingName,
- 'sidebar__file-item--closed': this.props.isFolderClosed
- });
+ const itemClass = classNames({
+ 'sidebar__root-item': name === 'root',
+ 'sidebar__file-item': name !== 'root',
+ 'sidebar__file-item--selected': isSelectedFile,
+ 'sidebar__file-item--open': isOptionsOpen,
+ 'sidebar__file-item--editing': isEditingName,
+ 'sidebar__file-item--closed': isFolderClosed
+ });
- const isFile = this.props.fileType === 'file';
- const isFolder = this.props.fileType === 'folder';
- const isRoot = this.props.name === 'root';
+ const isFile = fileType === 'file';
+ const isFolder = fileType === 'folder';
+ const isRoot = name === 'root';
- const { t } = this.props;
- const { extension } = parseFileName(this.props.name);
+ const { extension } = parseFileName(name);
- return (
-
- {!isRoot && (
-
-
- {isFile && (
-
-
+ {!isRoot && (
+
+
+ {isFile && (
+
+
+
+ )}
+ {isFolder && (
+
- )}
- {this.props.children && (
-
- {this.props.children.map(this.renderChild)}
-
- )}
-
- );
- }
-}
+
+ )}
+ {children && (
+
+ {children.map((childId) => (
+ -
+
+
+ ))}
+
+ )}
+
+ );
+};
FileNode.propTypes = {
id: PropTypes.string.isRequired,
@@ -438,7 +381,6 @@ FileNode.propTypes = {
canEdit: PropTypes.bool.isRequired,
openUploadFileModal: PropTypes.func.isRequired,
authenticated: PropTypes.bool.isRequired,
- t: PropTypes.func.isRequired,
onClickFile: PropTypes.func
};
@@ -464,11 +406,9 @@ function mapDispatchToProps(dispatch) {
return bindActionCreators(Object.assign(FileActions, IDEActions), dispatch);
}
-const TranslatedFileNode = withTranslation()(FileNode);
-
const ConnectedFileNode = connect(
mapStateToProps,
mapDispatchToProps
-)(TranslatedFileNode);
+)(FileNode);
-export { TranslatedFileNode as FileNode, ConnectedFileNode as default };
+export { FileNode, ConnectedFileNode as default };
diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx
index 6092cd686f..3846fe278b 100644
--- a/client/modules/IDE/components/SketchList.jsx
+++ b/client/modules/IDE/components/SketchList.jsx
@@ -1,433 +1,180 @@
import PropTypes from 'prop-types';
-import React from 'react';
+import classNames from 'classnames';
+import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { Helmet } from 'react-helmet';
-import { withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
-import { Link } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
import { bindActionCreators } from 'redux';
-import classNames from 'classnames';
-import slugify from 'slugify';
-import MenuItem from '../../../components/Dropdown/MenuItem';
-import TableDropdown from '../../../components/Dropdown/TableDropdown';
-import dates from '../../../utils/formatDate';
-import * as ProjectActions from '../actions/project';
import * as ProjectsActions from '../actions/projects';
import * as CollectionsActions from '../actions/collections';
import * as ToastActions from '../actions/toast';
import * as SortingActions from '../actions/sorting';
-import * as IdeActions from '../actions/ide';
import getSortedSketches from '../selectors/projects';
import Loader from '../../App/components/loader';
import Overlay from '../../App/components/Overlay';
import AddToCollectionList from './AddToCollectionList';
-import getConfig from '../../../utils/getConfig';
-
+import SketchListRowBase from './SketchListRowBase';
import ArrowUpIcon from '../../../images/sort-arrow-up.svg';
import ArrowDownIcon from '../../../images/sort-arrow-down.svg';
-const ROOT_URL = getConfig('API_URL');
-
-const formatDateCell = (date, mobile = false) =>
- dates.format(date, { showTime: !mobile });
-
-class SketchListRowBase extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- renameOpen: false,
- renameValue: props.sketch.name
- };
- this.renameInput = React.createRef();
- }
-
- openRename = () => {
- this.setState(
- {
- renameOpen: true,
- renameValue: this.props.sketch.name
- },
- () => this.renameInput.current.focus()
- );
- };
-
- closeRename = () => {
- this.setState({
- renameOpen: false
- });
- };
-
- handleRenameChange = (e) => {
- this.setState({
- renameValue: e.target.value
- });
- };
-
- handleRenameEnter = (e) => {
- if (e.key === 'Enter') {
- e.preventDefault();
- this.updateName();
- this.closeRename();
- }
- };
-
- handleRenameBlur = () => {
- this.updateName();
- this.closeRename();
- };
-
- updateName = () => {
- const isValid = this.state.renameValue.trim().length !== 0;
- if (isValid) {
- this.props.changeProjectName(
- this.props.sketch.id,
- this.state.renameValue.trim()
- );
- }
- };
-
- handleSketchDownload = () => {
- const { sketch } = this.props;
- const downloadLink = document.createElement('a');
- downloadLink.href = `${ROOT_URL}/projects/${sketch.id}/zip`;
- downloadLink.download = `${sketch.name}.zip`;
- document.body.appendChild(downloadLink);
- downloadLink.click();
- document.body.removeChild(downloadLink);
- };
-
- handleSketchDuplicate = () => {
- this.props.cloneProject(this.props.sketch);
- };
-
- handleSketchShare = () => {
- this.props.showShareModal(
- this.props.sketch.id,
- this.props.sketch.name,
- this.props.username
- );
- };
-
- handleSketchDelete = () => {
- if (
- window.confirm(
- this.props.t('Common.DeleteConfirmation', {
- name: this.props.sketch.name
- })
- )
- ) {
- this.props.deleteProject(this.props.sketch.id);
- }
- };
-
- renderDropdown = () => {
- const userIsOwner = this.props.user.username === this.props.username;
-
- return (
-
-
-
-
-
-
-
- {/*
-
- */}
-
-
- |
- );
- };
-
- render() {
- const { sketch, username, mobile } = this.props;
- const { renameOpen, renameValue } = this.state;
- let url = `/${username}/sketches/${sketch.id}`;
- if (username === 'p5') {
- url = `/${username}/sketches/${slugify(sketch.name, '_')}`;
+const SketchList = ({
+ user,
+ getProjects,
+ sketches,
+ username,
+ loading,
+ sorting,
+ toggleDirectionForField,
+ resetSorting,
+ mobile
+}) => {
+ const [isInitialDataLoad, setIsInitialDataLoad] = useState(true);
+ const [sketchToAddToCollection, setSketchToAddToCollection] = useState(null);
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ getProjects(username);
+ resetSorting();
+ }, [getProjects, username, resetSorting]);
+
+ useEffect(() => {
+ if (Array.isArray(sketches)) {
+ setIsInitialDataLoad(false);
}
-
- const name = (
-
- {renameOpen ? '' : sketch.name}
- {renameOpen && (
- e.stopPropagation()}
- ref={this.renameInput}
- maxLength={128}
- />
- )}
-
- );
-
- return (
-
-
- {name} |
- {formatDateCell(sketch.createdAt, mobile)} |
- {formatDateCell(sketch.updatedAt, mobile)} |
- {this.renderDropdown()}
-
-
- );
- }
-}
-
-SketchListRowBase.propTypes = {
- sketch: PropTypes.shape({
- id: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired,
- createdAt: PropTypes.string.isRequired,
- updatedAt: PropTypes.string.isRequired
- }).isRequired,
- username: PropTypes.string.isRequired,
- user: PropTypes.shape({
- username: PropTypes.string,
- authenticated: PropTypes.bool.isRequired
- }).isRequired,
- deleteProject: PropTypes.func.isRequired,
- showShareModal: PropTypes.func.isRequired,
- cloneProject: PropTypes.func.isRequired,
- changeProjectName: PropTypes.func.isRequired,
- onAddToCollection: PropTypes.func.isRequired,
- mobile: PropTypes.bool,
- t: PropTypes.func.isRequired
-};
-
-SketchListRowBase.defaultProps = {
- mobile: false
-};
-
-function mapDispatchToPropsSketchListRow(dispatch) {
- return bindActionCreators(
- Object.assign({}, ProjectActions, IdeActions),
- dispatch
+ }, [sketches]);
+
+ const getSketchesTitle = useMemo(
+ () =>
+ username === user.username
+ ? t('SketchList.Title')
+ : t('SketchList.AnothersTitle', { anotheruser: username }),
+ [username, user.username, t]
);
-}
-
-const SketchListRow = connect(
- null,
- mapDispatchToPropsSketchListRow
-)(SketchListRowBase);
-class SketchList extends React.Component {
- constructor(props) {
- super(props);
- this.props.getProjects(this.props.username);
- this.props.resetSorting();
+ const isLoading = () => loading && isInitialDataLoad;
- this.state = {
- isInitialDataLoad: true
- };
- }
+ const hasSketches = () => !isLoading() && sketches.length > 0;
- componentDidUpdate(prevProps) {
- if (
- this.props.sketches !== prevProps.sketches &&
- Array.isArray(this.props.sketches)
- ) {
- // eslint-disable-next-line react/no-did-update-set-state
- this.setState({
- isInitialDataLoad: false
- });
- }
- }
-
- getSketchesTitle() {
- if (this.props.username === this.props.user.username) {
- return this.props.t('SketchList.Title');
- }
- return this.props.t('SketchList.AnothersTitle', {
- anotheruser: this.props.username
- });
- }
-
- hasSketches() {
- return !this.isLoading() && this.props.sketches.length > 0;
- }
-
- isLoading() {
- return this.props.loading && this.state.isInitialDataLoad;
- }
+ const renderLoader = () => isLoading() &&
;
- _renderLoader() {
- if (this.isLoading()) return
;
- return null;
- }
-
- _renderEmptyTable() {
- if (!this.isLoading() && this.props.sketches.length === 0) {
+ const renderEmptyTable = () => {
+ if (!isLoading() && sketches.length === 0) {
return (
-
- {this.props.t('SketchList.NoSketches')}
-
+
{t('SketchList.NoSketches')}
);
}
return null;
- }
-
- _getButtonLabel = (fieldName, displayName) => {
- const { field, direction } = this.props.sorting;
- let buttonLabel;
- if (field !== fieldName) {
- if (field === 'name') {
- buttonLabel = this.props.t('SketchList.ButtonLabelAscendingARIA', {
- displayName
- });
- } else {
- buttonLabel = this.props.t('SketchList.ButtonLabelDescendingARIA', {
- displayName
- });
- }
- } else if (direction === SortingActions.DIRECTION.ASC) {
- buttonLabel = this.props.t('SketchList.ButtonLabelDescendingARIA', {
- displayName
- });
- } else {
- buttonLabel = this.props.t('SketchList.ButtonLabelAscendingARIA', {
- displayName
- });
- }
- return buttonLabel;
};
- _renderFieldHeader = (fieldName, displayName) => {
- const { field, direction } = this.props.sorting;
- const headerClass = classNames({
- 'sketches-table__header': true,
- 'sketches-table__header--selected': field === fieldName
- });
- const buttonLabel = this._getButtonLabel(fieldName, displayName);
- return (
-
-
- |
- );
- };
+ const getButtonLabel = useCallback(
+ (fieldName, displayName) => {
+ const { field, direction } = sorting;
+ if (field !== fieldName) {
+ return field === 'name'
+ ? t('SketchList.ButtonLabelAscendingARIA', { displayName })
+ : t('SketchList.ButtonLabelDescendingARIA', { displayName });
+ }
+ return direction === SortingActions.DIRECTION.ASC
+ ? t('SketchList.ButtonLabelDescendingARIA', { displayName })
+ : t('SketchList.ButtonLabelAscendingARIA', { displayName });
+ },
+ [sorting, t]
+ );
- render() {
- const username =
- this.props.username !== undefined
- ? this.props.username
- : this.props.user.username;
- const { mobile } = this.props;
- return (
-
-
- {this.getSketchesTitle()}
-
- {this._renderLoader()}
- {this._renderEmptyTable()}
- {this.hasSketches() && (
- {
+ const { field, direction } = sorting;
+ const headerClass = classNames({
+ 'sketches-table__header': true,
+ 'sketches-table__header--selected': field === fieldName
+ });
+ const buttonLabel = getButtonLabel(fieldName, displayName);
+ return (
+
+ |
- )}
- {this.state.sketchToAddToCollection && (
-
- this.setState({ sketchToAddToCollection: null })
- }
- >
-
-
- )}
-
- );
- }
-}
+
+