From 25d23db155ac648243d65229539992c9e0ed045a Mon Sep 17 00:00:00 2001 From: nahbee10 Date: Wed, 11 Sep 2024 08:20:01 -0400 Subject: [PATCH 01/10] rest of files for the class to functional conversion --- client/modules/IDE/components/AssetList.jsx | 214 ++------ .../modules/IDE/components/AssetListRow.jsx | 73 +++ .../CollectionList/CollectionList.jsx | 301 +++++----- .../modules/IDE/components/ConsoleInput.jsx | 145 +++-- client/modules/IDE/components/FileNode.jsx | 513 ++++++++---------- client/modules/IDE/components/SketchList.jsx | 511 +++++------------ .../IDE/components/SketchListRowBase.jsx | 167 ++++++ client/modules/User/components/Collection.jsx | 413 ++++---------- .../User/components/CollectionItemRow.jsx | 84 +++ 9 files changed, 1043 insertions(+), 1378 deletions(-) create mode 100644 client/modules/IDE/components/AssetListRow.jsx create mode 100644 client/modules/IDE/components/SketchListRowBase.jsx create mode 100644 client/modules/User/components/CollectionItemRow.jsx diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index 720f21734b..f8a7cb5e8f 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -1,187 +1,67 @@ import PropTypes from 'prop-types'; -import React from 'react'; -import { connect, useDispatch } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { Link } from 'react-router-dom'; +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { Helmet } from 'react-helmet'; -import prettyBytes from 'pretty-bytes'; -import { useTranslation, withTranslation } from 'react-i18next'; -import MenuItem from '../../../components/Dropdown/MenuItem'; -import TableDropdown from '../../../components/Dropdown/TableDropdown'; - +import { withTranslation, useTranslation } from 'react-i18next'; +import AssetListRow from './AssetListRow'; import Loader from '../../App/components/loader'; -import { deleteAssetRequest } from '../actions/assets'; import * as AssetActions from '../actions/assets'; -const AssetMenu = ({ item: asset }) => { - const { t } = useTranslation(); - +const AssetList = ({ t }) => { const dispatch = useDispatch(); + const { username, assetList, loading } = useSelector((state) => ({ + username: state.user.username, + assetList: state.assets.list, + loading: state.loading + })); - const handleAssetDelete = () => { - const { key, name } = asset; - if (window.confirm(t('Common.DeleteConfirmation', { name }))) { - dispatch(deleteAssetRequest(key)); - } - }; - - return ( - - {t('AssetList.Delete')} - - {t('AssetList.OpenNewTab')} - - - ); -}; - -AssetMenu.propTypes = { - item: PropTypes.shape({ - key: PropTypes.string.isRequired, - url: PropTypes.string.isRequired, - name: PropTypes.string.isRequired - }).isRequired -}; - -const AssetListRowBase = ({ asset, username }) => ( - - - - {asset.name} - - - {prettyBytes(asset.size)} - - {asset.sketchId && ( - - {asset.sketchName} - - )} - - - - - -); - -AssetListRowBase.propTypes = { - asset: PropTypes.shape({ - key: PropTypes.string.isRequired, - url: PropTypes.string.isRequired, - sketchId: PropTypes.string, - sketchName: PropTypes.string, - name: PropTypes.string.isRequired, - size: PropTypes.number.isRequired - }).isRequired, - username: PropTypes.string.isRequired -}; - -function mapStateToPropsAssetListRow(state) { - return { - username: state.user.username - }; -} - -function mapDispatchToPropsAssetListRow(dispatch) { - return bindActionCreators(AssetActions, dispatch); -} - -const AssetListRow = connect( - mapStateToPropsAssetListRow, - mapDispatchToPropsAssetListRow -)(AssetListRowBase); - -class AssetList extends React.Component { - constructor(props) { - super(props); - this.props.getAssets(); - } - - getAssetsTitle() { - return this.props.t('AssetList.Title'); - } + useEffect(() => { + dispatch(AssetActions.getAssets()); + }, [dispatch]); - hasAssets() { - return !this.props.loading && this.props.assetList.length > 0; - } + const hasAssets = () => !loading && assetList.length > 0; - renderLoader() { - if (this.props.loading) return ; - return null; - } + const renderLoader = () => (loading ? : null); - renderEmptyTable() { - if (!this.props.loading && this.props.assetList.length === 0) { + const renderEmptyTable = () => { + if (!loading && assetList.length === 0) { return ( -

- {this.props.t('AssetList.NoUploadedAssets')} -

+

{t('AssetList.NoUploadedAssets')}

); } return null; - } + }; - render() { - const { assetList, t } = this.props; - return ( -
- - {this.getAssetsTitle()} - - {this.renderLoader()} - {this.renderEmptyTable()} - {this.hasAssets() && ( - - - - - - - - - - - {assetList.map((asset) => ( - - ))} - -
{t('AssetList.HeaderName')}{t('AssetList.HeaderSize')}{t('AssetList.HeaderSketch')}
- )} -
- ); - } -} + return ( +
+ + {t('AssetList.Title')} + + {renderLoader()} + {renderEmptyTable()} + {hasAssets() && ( + + + + + + + + + + + {assetList.map((asset) => ( + + ))} + +
{t('AssetList.HeaderName')}{t('AssetList.HeaderSize')}{t('AssetList.HeaderSketch')}
+ )} +
+ ); +}; AssetList.propTypes = { - user: PropTypes.shape({ - username: PropTypes.string - }).isRequired, - assetList: PropTypes.arrayOf( - PropTypes.shape({ - key: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - url: PropTypes.string.isRequired, - sketchName: PropTypes.string, - sketchId: PropTypes.string - }) - ).isRequired, - getAssets: PropTypes.func.isRequired, - loading: PropTypes.bool.isRequired, t: PropTypes.func.isRequired }; -function mapStateToProps(state) { - return { - user: state.user, - assetList: state.assets.list, - loading: state.loading - }; -} - -function mapDispatchToProps(dispatch) { - return bindActionCreators(Object.assign({}, AssetActions), dispatch); -} - -export default withTranslation()( - connect(mapStateToProps, mapDispatchToProps)(AssetList) -); +export default withTranslation()(AssetList); diff --git a/client/modules/IDE/components/AssetListRow.jsx b/client/modules/IDE/components/AssetListRow.jsx new file mode 100644 index 0000000000..7e7af8f013 --- /dev/null +++ b/client/modules/IDE/components/AssetListRow.jsx @@ -0,0 +1,73 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import prettyBytes from 'pretty-bytes'; +import MenuItem from '../../../components/Dropdown/MenuItem'; +import TableDropdown from '../../../components/Dropdown/TableDropdown'; +import { deleteAssetRequest } from '../actions/assets'; + +const AssetMenu = ({ item: asset }) => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const handleAssetDelete = () => { + const { key, name } = asset; + if (window.confirm(t('Common.DeleteConfirmation', { name }))) { + dispatch(deleteAssetRequest(key)); + } + }; + + return ( + + {t('AssetList.Delete')} + + {t('AssetList.OpenNewTab')} + + + ); +}; + +AssetMenu.propTypes = { + item: PropTypes.shape({ + key: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired +}; + +const AssetListRow = ({ asset, username }) => ( + + + + {asset.name} + + + {prettyBytes(asset.size)} + + {asset.sketchId && ( + + {asset.sketchName} + + )} + + + + + +); + +AssetListRow.propTypes = { + asset: PropTypes.shape({ + key: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + sketchId: PropTypes.string, + sketchName: PropTypes.string, + name: PropTypes.string.isRequired, + size: PropTypes.number.isRequired + }).isRequired, + username: PropTypes.string.isRequired +}; + +export default AssetListRow; diff --git a/client/modules/IDE/components/CollectionList/CollectionList.jsx b/client/modules/IDE/components/CollectionList/CollectionList.jsx index 9d641f5e66..8f6f24767d 100644 --- a/client/modules/IDE/components/CollectionList/CollectionList.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionList.jsx @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { Helmet } from 'react-helmet'; import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; @@ -22,113 +22,119 @@ import CollectionListRow from './CollectionListRow'; import ArrowUpIcon from '../../../../images/sort-arrow-up.svg'; import ArrowDownIcon from '../../../../images/sort-arrow-down.svg'; -class CollectionList extends React.Component { - constructor(props) { - super(props); +const CollectionList = ({ + user, + projectId, + getCollections, + getProject, + collections, + username: propsUsername, + loading, + toggleDirectionForField, + resetSorting, + sorting, + project, + t, + mobile +}) => { + const [hasLoadedData, setHasLoadedData] = useState(false); + const [ + addingSketchesToCollectionId, + setAddingSketchesToCollectionId + ] = useState(null); - if (props.projectId) { - props.getProject(props.projectId); + useEffect(() => { + if (projectId) { + getProject(projectId); } + getCollections(propsUsername || user.username); + resetSorting(); + }, [ + projectId, + getCollections, + getProject, + propsUsername, + resetSorting, + user.username + ]); - this.props.getCollections(this.props.username); - this.props.resetSorting(); - - this.state = { - hasLoadedData: false, - addingSketchesToCollectionId: null - }; - } - - componentDidUpdate(prevProps, prevState) { - if (prevProps.loading === true && this.props.loading === false) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ - hasLoadedData: true - }); + useEffect(() => { + if (!loading) { + setHasLoadedData(true); } - } + }, [loading]); - getTitle() { - if (this.props.username === this.props.user.username) { - return this.props.t('CollectionList.Title'); + const getTitle = useCallback(() => { + if (propsUsername === user.username) { + return t('CollectionList.Title'); } - return this.props.t('CollectionList.AnothersTitle', { - anotheruser: this.props.username + return t('CollectionList.AnothersTitle', { + anotheruser: propsUsername }); - } + }, [propsUsername, user.username, t]); - showAddSketches = (collectionId) => { - this.setState({ - addingSketchesToCollectionId: collectionId - }); + const showAddSketches = (collectionId) => { + setAddingSketchesToCollectionId(collectionId); }; - hideAddSketches = () => { - this.setState({ - addingSketchesToCollectionId: null - }); + const hideAddSketches = () => { + setAddingSketchesToCollectionId(null); }; - hasCollections() { - return ( - (!this.props.loading || this.state.hasLoadedData) && - this.props.collections.length > 0 - ); - } + const hasCollections = () => + (!loading || hasLoadedData) && collections.length > 0; - _renderLoader() { - if (this.props.loading && !this.state.hasLoadedData) return ; + const renderLoader = () => { + if (loading && !hasLoadedData) return ; return null; - } + }; - _renderEmptyTable() { - if (!this.props.loading && this.props.collections.length === 0) { + const renderEmptyTable = () => { + if (!loading && collections.length === 0) { return (

- {this.props.t('CollectionList.NoCollections')} + {t('CollectionList.NoCollections')}

); } return null; - } + }; - _getButtonLabel = (fieldName, displayName) => { - const { field, direction } = this.props.sorting; - let buttonLabel; - if (field !== fieldName) { - if (field === 'name') { - buttonLabel = this.props.t('CollectionList.ButtonLabelAscendingARIA', { + const getButtonLabel = useCallback( + (fieldName, displayName) => { + const { field, direction } = sorting; + let buttonLabel; + if (field !== fieldName) { + buttonLabel = + field === 'name' + ? t('CollectionList.ButtonLabelAscendingARIA', { displayName }) + : t('CollectionList.ButtonLabelDescendingARIA', { displayName }); + } else if (direction === SortingActions.DIRECTION.ASC) { + buttonLabel = t('CollectionList.ButtonLabelDescendingARIA', { displayName }); } else { - buttonLabel = this.props.t('CollectionList.ButtonLabelDescendingARIA', { + buttonLabel = t('CollectionList.ButtonLabelAscendingARIA', { displayName }); } - } else if (direction === SortingActions.DIRECTION.ASC) { - buttonLabel = this.props.t('CollectionList.ButtonLabelDescendingARIA', { - displayName - }); - } else { - buttonLabel = this.props.t('CollectionList.ButtonLabelAscendingARIA', { - displayName - }); - } - return buttonLabel; - }; + return buttonLabel; + }, + [sorting, t] + ); - _renderFieldHeader = (fieldName, displayName) => { - const { field, direction } = this.props.sorting; + const renderFieldHeader = (fieldName, displayName) => { + const { field, direction } = sorting; const headerClass = classNames({ 'sketches-table__header': true, 'sketches-table__header--selected': field === fieldName }); - const buttonLabel = this._getButtonLabel(fieldName, displayName); + const buttonLabel = getButtonLabel(fieldName, displayName); return ( + + + )} + + + +
+
    + {isFolder && ( + <> +
  • + +
  • +
  • + +
  • + {authenticated && ( +
  • + +
  • + )} + + )} +
  • +
  • +
  • -
- )} - - { - this.fileNameInput = element; - }} - onBlur={this.handleFileNameBlur} - onKeyPress={this.handleKeyPress} - /> - -
-
    - {isFolder && ( - -
  • - -
  • -
  • - -
  • - {this.props.authenticated && ( -
  • - -
  • - )} -
    - )} -
  • - -
  • -
  • - -
  • -
-
+ + - )} - {this.props.children && ( -
    - {this.props.children.map(this.renderChild)} -
- )} - - ); - } -} + + )} + {children && ( +
    + {children.map((childId) => ( +
  • + +
  • + ))} +
+ )} + + ); +}; FileNode.propTypes = { id: PropTypes.string.isRequired, @@ -438,7 +384,6 @@ FileNode.propTypes = { canEdit: PropTypes.bool.isRequired, openUploadFileModal: PropTypes.func.isRequired, authenticated: PropTypes.bool.isRequired, - t: PropTypes.func.isRequired, onClickFile: PropTypes.func }; diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index 6092cd686f..e998137384 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -1,433 +1,174 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import classNames from 'classnames'; +import React, { useEffect, useState, 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 { 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 * as ProjectsActions from '../actions/projects'; // Added Projects actions +import * as CollectionsActions from '../actions/collections'; // Added Collections actions +import * as ToastActions from '../actions/toast'; // Added Toast actions +import * as SortingActions from '../actions/sorting'; // Added Sorting actions 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(); +const SketchList = ({ + user, + getProjects, + sketches, + username, + loading, + sorting, + toggleDirectionForField, + resetSorting, + t, + mobile +}) => { + const [isInitialDataLoad, setIsInitialDataLoad] = useState(true); + const [sketchToAddToCollection, setSketchToAddToCollection] = useState(null); + + useEffect(() => { + getProjects(username); + resetSorting(); + }, [getProjects, username, resetSorting]); + + useEffect(() => { + if (Array.isArray(sketches)) { + setIsInitialDataLoad(false); } - }; - - 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 ( - - - - {this.props.t('SketchList.DropdownRename')} - - - {this.props.t('SketchList.DropdownDownload')} - - - {this.props.t('SketchList.DropdownDuplicate')} - - { - this.props.onAddToCollection(); - }} - > - {this.props.t('SketchList.DropdownAddToCollection')} - - - {/* - - Share - - */} - - {this.props.t('SketchList.DropdownDelete')} - - - - ); - }; - - 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 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 = useCallback( + () => + 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 - }); + const getButtonLabel = useCallback( + (fieldName, displayName) => { + const { field, direction } = sorting; + if (field !== fieldName) { + return field === 'name' + ? t('SketchList.ButtonLabelAscendingARIA', { displayName }) + : 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; - }; + return direction === SortingActions.DIRECTION.ASC + ? t('SketchList.ButtonLabelDescendingARIA', { displayName }) + : t('SketchList.ButtonLabelAscendingARIA', { displayName }); + }, + [sorting, t] + ); - _renderFieldHeader = (fieldName, displayName) => { - const { field, direction } = this.props.sorting; + const renderFieldHeader = (fieldName, displayName) => { + const { field, direction } = sorting; const headerClass = classNames({ 'sketches-table__header': true, 'sketches-table__header--selected': field === fieldName }); - const buttonLabel = this._getButtonLabel(fieldName, displayName); + const buttonLabel = getButtonLabel(fieldName, displayName); return ( ); }; - 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() && ( - - - - {this._renderFieldHeader( - 'name', - this.props.t('SketchList.HeaderName') - )} - {this._renderFieldHeader( - 'createdAt', - this.props.t('SketchList.HeaderCreatedAt', { - context: mobile ? 'mobile' : '' - }) - )} - {this._renderFieldHeader( - 'updatedAt', - this.props.t('SketchList.HeaderUpdatedAt', { - context: mobile ? 'mobile' : '' - }) - )} - - - - - {this.props.sketches.map((sketch) => ( - { - this.setState({ sketchToAddToCollection: sketch }); - }} - t={this.props.t} - /> - ))} - -
- )} - {this.state.sketchToAddToCollection && ( - - this.setState({ sketchToAddToCollection: null }) - } - > - - - )} -
- ); - } -} + return ( +
+ + {getSketchesTitle()} + + {renderLoader()} + {renderEmptyTable()} + {hasSketches() && ( + + + + {renderFieldHeader('name', t('SketchList.HeaderName'))} + {renderFieldHeader( + 'createdAt', + t('SketchList.HeaderCreatedAt', { + context: mobile ? 'mobile' : '' + }) + )} + {renderFieldHeader( + 'updatedAt', + t('SketchList.HeaderUpdatedAt', { + context: mobile ? 'mobile' : '' + }) + )} + + + + + {sketches.map((sketch) => ( + setSketchToAddToCollection(sketch)} + t={t} + /> + ))} + +
+ )} + {sketchToAddToCollection && ( + setSketchToAddToCollection(null)} + > + + + )} +
+ ); +}; SketchList.propTypes = { user: PropTypes.shape({ @@ -483,6 +224,4 @@ function mapDispatchToProps(dispatch) { ); } -export default withTranslation()( - connect(mapStateToProps, mapDispatchToProps)(SketchList) -); +export default connect(mapStateToProps, mapDispatchToProps)(SketchList); diff --git a/client/modules/IDE/components/SketchListRowBase.jsx b/client/modules/IDE/components/SketchListRowBase.jsx new file mode 100644 index 0000000000..2822aa0b03 --- /dev/null +++ b/client/modules/IDE/components/SketchListRowBase.jsx @@ -0,0 +1,167 @@ +import PropTypes from 'prop-types'; +import slugify from 'slugify'; +import React, { useState, useRef, useCallback } from 'react'; +import { Link } from 'react-router-dom'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import * as ProjectActions from '../actions/project'; +import * as IdeActions from '../actions/ide'; // Adding the missing Ide actions +import TableDropdown from '../../../components/Dropdown/TableDropdown'; +import MenuItem from '../../../components/Dropdown/MenuItem'; +import dates from '../../../utils/formatDate'; +import getConfig from '../../../utils/getConfig'; + +const ROOT_URL = getConfig('API_URL'); + +const formatDateCell = (date, mobile = false) => + dates.format(date, { showTime: !mobile }); + +const SketchListRowBase = ({ + sketch, + username, + user, + changeProjectName, + cloneProject, + deleteProject, + t, + mobile, + onAddToCollection +}) => { + const [renameOpen, setRenameOpen] = useState(false); + const [renameValue, setRenameValue] = useState(sketch.name); + const renameInput = useRef(null); + + const openRename = useCallback(() => { + setRenameOpen(true); + setRenameValue(sketch.name); + renameInput.current.focus(); + }, [sketch.name]); + + const closeRename = () => setRenameOpen(false); + + const updateName = useCallback(() => { + if (renameValue.trim().length > 0) { + changeProjectName(sketch.id, renameValue.trim()); + } + }, [renameValue, sketch.id, changeProjectName]); + + const handleRenameChange = (e) => setRenameValue(e.target.value); + + const handleRenameEnter = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + updateName(); + closeRename(); + } + }; + + const handleRenameBlur = () => { + updateName(); + closeRename(); + }; + + const handleSketchDownload = () => { + 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); + }; + + const handleSketchDuplicate = () => cloneProject(sketch); + const handleSketchDelete = () => { + if (window.confirm(t('Common.DeleteConfirmation', { name: sketch.name }))) { + deleteProject(sketch.id); + } + }; + + const userIsOwner = user.username === username; + + let url = `/${username}/sketches/${sketch.id}`; + if (username === 'p5') { + url = `/${username}/sketches/${slugify(sketch.name, '_')}`; + } + + const name = ( + <> + {renameOpen ? '' : sketch.name} + {renameOpen && ( + + )} + + ); + + return ( + + {name} + {formatDateCell(sketch.createdAt, mobile)} + {formatDateCell(sketch.updatedAt, mobile)} + + + + {t('SketchList.DropdownRename')} + + + {t('SketchList.DropdownDownload')} + + + {t('SketchList.DropdownDuplicate')} + + + {t('SketchList.DropdownAddToCollection')} + + + {t('SketchList.DropdownDelete')} + + + + + ); +}; + +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, + 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), // Binding both ProjectActions and IdeActions + dispatch + ); +} + +export default connect( + null, + mapDispatchToPropsSketchListRow +)(SketchListRowBase); diff --git a/client/modules/User/components/Collection.jsx b/client/modules/User/components/Collection.jsx index 939b2b851f..34acf688ef 100644 --- a/client/modules/User/components/Collection.jsx +++ b/client/modules/User/components/Collection.jsx @@ -1,12 +1,9 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { Helmet } from 'react-helmet'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; -import { bindActionCreators } from 'redux'; -import { useTranslation, withTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; - import * as ProjectActions from '../../IDE/actions/project'; import * as ProjectsActions from '../../IDE/actions/projects'; import * as CollectionsActions from '../../IDE/actions/collections'; @@ -16,351 +13,159 @@ import * as IdeActions from '../../IDE/actions/ide'; import { getCollection } from '../../IDE/selectors/collections'; import Loader from '../../App/components/loader'; import dates from '../../../utils/formatDate'; - import ArrowUpIcon from '../../../images/sort-arrow-up.svg'; import ArrowDownIcon from '../../../images/sort-arrow-down.svg'; -import RemoveIcon from '../../../images/close.svg'; import CollectionMetadata from './CollectionMetadata'; +import CollectionItemRow from './CollectionItemRow'; -const CollectionItemRowBase = ({ - collection, - item, - isOwner, - removeFromCollection -}) => { +const Collection = ({ collectionId }) => { const { t } = useTranslation(); - - const projectIsDeleted = item.isDeleted; - - const handleSketchRemove = () => { - const name = projectIsDeleted ? 'deleted sketch' : item.project.name; - - if ( - window.confirm( - t('Collection.DeleteFromCollection', { name_sketch: name }) - ) - ) { - removeFromCollection(collection.id, item.projectId); - } - }; - - const name = projectIsDeleted ? ( - {t('Collection.SketchDeleted')} - ) : ( - - {item.project.name} - - ); - - const sketchOwnerUsername = projectIsDeleted - ? null - : item.project.user.username; - - return ( - - {name} - {dates.format(item.createdAt)} - {sketchOwnerUsername} - - {isOwner && ( - - )} - - + const dispatch = useDispatch(); + + const { user, collection, sorting, loading, username } = useSelector( + (state) => ({ + user: state.user, + collection: getCollection(state, collectionId), + sorting: state.sorting, + loading: state.loading, + username: state.user.username + }) ); -}; - -CollectionItemRowBase.propTypes = { - collection: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired - }).isRequired, - item: PropTypes.shape({ - createdAt: PropTypes.string.isRequired, - projectId: PropTypes.string.isRequired, - isDeleted: PropTypes.bool.isRequired, - project: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - user: PropTypes.shape({ - username: PropTypes.string.isRequired - }) - }).isRequired - }).isRequired, - isOwner: PropTypes.bool.isRequired, - user: PropTypes.shape({ - username: PropTypes.string, - authenticated: PropTypes.bool.isRequired - }).isRequired, - removeFromCollection: PropTypes.func.isRequired -}; -function mapDispatchToPropsSketchListRow(dispatch) { - return bindActionCreators( - Object.assign({}, CollectionsActions, ProjectActions, IdeActions), - dispatch - ); -} + useEffect(() => { + dispatch(CollectionsActions.getCollections(username)); + dispatch(SortingActions.resetSorting()); + }, [dispatch, username]); -const CollectionItemRow = connect( - null, - mapDispatchToPropsSketchListRow -)(CollectionItemRowBase); + const isOwner = () => + user != null && + user.username && + collection?.owner?.username === user.username; -class Collection extends React.Component { - constructor(props) { - super(props); - this.props.getCollections(this.props.username); - this.props.resetSorting(); - this._renderFieldHeader = this._renderFieldHeader.bind(this); - } + const hasCollection = () => !!collection; + const hasCollectionItems = () => + hasCollection() && collection.items.length > 0; - getTitle() { - if (this.hasCollection()) { - return `${this.props.t('Common.SiteName')} | ${this.getCollectionName()}`; - } - if (this.props.username === this.props.user.username) { - return this.props.t('Collection.Title'); + const getTitle = () => { + if (hasCollection()) { + return `${t('Common.SiteName')} | ${collection.name}`; } - return this.props.t('Collection.AnothersTitle', { - anotheruser: this.props.username - }); - } - - getUsername() { - return this.props.username !== undefined - ? this.props.username - : this.props.user.username; - } - - getCollectionName() { - return this.props.collection.name; - } - - isOwner() { - let isOwner = false; - - if ( - this.props.user != null && - this.props.user.username && - this.props.collection?.owner?.username === this.props.user.username - ) { - isOwner = true; + if (username === user.username) { + return t('Collection.Title'); } + return t('Collection.AnothersTitle', { anotheruser: username }); + }; - return isOwner; - } - - hasCollection() { - return !!this.props.collection; - } - - hasCollectionItems() { - return this.hasCollection() && this.props.collection.items.length > 0; - } - - _renderLoader() { - if (this.props.loading && !this.hasCollection()) return ; - return null; - } + const renderLoader = () => (loading && !hasCollection() ? : null); - _renderEmptyTable() { - if (this.hasCollection() && !this.hasCollectionItems()) { + const renderEmptyTable = () => { + if (hasCollection() && !hasCollectionItems()) { return ( -

- {this.props.t('Collection.NoSketches')} -

+

{t('Collection.NoSketches')}

); } return null; - } + }; - _getButtonLabel = (fieldName, displayName) => { - const { field, direction } = this.props.sorting; - let buttonLabel; + const getButtonLabel = (fieldName, displayName) => { + const { field, direction } = sorting; if (field !== fieldName) { - if (field === 'name') { - buttonLabel = this.props.t('Collection.ButtonLabelAscendingARIA', { - displayName - }); - } else { - buttonLabel = this.props.t('Collection.ButtonLabelDescendingARIA', { - displayName - }); - } - } else if (direction === SortingActions.DIRECTION.ASC) { - buttonLabel = this.props.t('Collection.ButtonLabelDescendingARIA', { - displayName - }); - } else { - buttonLabel = this.props.t('Collection.ButtonLabelAscendingARIA', { - displayName - }); + return field === 'name' + ? t('Collection.ButtonLabelAscendingARIA', { displayName }) + : t('Collection.ButtonLabelDescendingARIA', { displayName }); } - return buttonLabel; + return direction === SortingActions.DIRECTION.ASC + ? t('Collection.ButtonLabelDescendingARIA', { displayName }) + : t('Collection.ButtonLabelAscendingARIA', { displayName }); }; - _renderFieldHeader(fieldName, displayName) { - const { field, direction } = this.props.sorting; + const renderFieldHeader = (fieldName, displayName) => { + const { field, direction } = sorting; const headerClass = classNames({ arrowDown: true, 'sketches-table__header--selected': field === fieldName }); - const buttonLabel = this._getButtonLabel(fieldName, displayName); + const buttonLabel = getButtonLabel(fieldName, displayName); return ( ); - } - - render() { - const isOwner = this.isOwner(); + }; - return ( -
-
- - {this.getTitle()} - - {this._renderLoader()} - -
-
- {this._renderEmptyTable()} - {this.hasCollectionItems() && ( - - - - {this._renderFieldHeader( - 'name', - this.props.t('Collection.HeaderName') - )} - {this._renderFieldHeader( - 'createdAt', - this.props.t('Collection.HeaderCreatedAt') - )} - {this._renderFieldHeader( - 'user', - this.props.t('Collection.HeaderUser') - )} - - - - - {this.props.collection.items.map((item) => ( - - ))} - -
- )} -
-
+ return ( +
+
+ + {getTitle()} + + {renderLoader()} + +
+
+ {renderEmptyTable()} + {hasCollectionItems() && ( + + + + {renderFieldHeader('name', t('Collection.HeaderName'))} + {renderFieldHeader( + 'createdAt', + t('Collection.HeaderCreatedAt') + )} + {renderFieldHeader('user', t('Collection.HeaderUser'))} + + + + + {collection.items.map((item) => ( + + ))} + +
+ )} +
-
- ); - } -} - -Collection.propTypes = { - collectionId: PropTypes.string.isRequired, - user: PropTypes.shape({ - username: PropTypes.string, - authenticated: PropTypes.bool.isRequired - }).isRequired, - getCollections: PropTypes.func.isRequired, - collection: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - slug: PropTypes.string, - description: PropTypes.string, - owner: PropTypes.shape({ - username: PropTypes.string - }).isRequired, - items: PropTypes.arrayOf(PropTypes.shape({})) - }), - username: PropTypes.string, - loading: PropTypes.bool.isRequired, - toggleDirectionForField: PropTypes.func.isRequired, - resetSorting: PropTypes.func.isRequired, - sorting: PropTypes.shape({ - field: PropTypes.string.isRequired, - direction: PropTypes.string.isRequired - }).isRequired, - t: PropTypes.func.isRequired +
+
+ ); }; -Collection.defaultProps = { - username: undefined, - collection: null +Collection.propTypes = { + collectionId: PropTypes.string.isRequired }; -function mapStateToProps(state, ownProps) { - return { - user: state.user, - collection: getCollection(state, ownProps.collectionId), - sorting: state.sorting, - loading: state.loading, - project: state.project - }; -} - -function mapDispatchToProps(dispatch) { - return bindActionCreators( - Object.assign( - {}, - CollectionsActions, - ProjectsActions, - ToastActions, - SortingActions - ), - dispatch - ); -} - -export default withTranslation()( - connect(mapStateToProps, mapDispatchToProps)(Collection) -); +export default Collection; diff --git a/client/modules/User/components/CollectionItemRow.jsx b/client/modules/User/components/CollectionItemRow.jsx new file mode 100644 index 0000000000..f34479d468 --- /dev/null +++ b/client/modules/User/components/CollectionItemRow.jsx @@ -0,0 +1,84 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import dates from '../../../utils/formatDate'; +import RemoveIcon from '../../../images/close.svg'; + +const CollectionItemRow = ({ + collection, + item, + isOwner, + removeFromCollection +}) => { + const { t } = useTranslation(); + const projectIsDeleted = item.isDeleted; + + const handleSketchRemove = () => { + const name = projectIsDeleted ? 'deleted sketch' : item.project.name; + + if ( + window.confirm( + t('Collection.DeleteFromCollection', { name_sketch: name }) + ) + ) { + removeFromCollection(collection.id, item.projectId); + } + }; + + const name = projectIsDeleted ? ( + {t('Collection.SketchDeleted')} + ) : ( + + {item.project.name} + + ); + + const sketchOwnerUsername = projectIsDeleted + ? null + : item.project.user.username; + + return ( + + {name} + {dates.format(item.createdAt)} + {sketchOwnerUsername} + + {isOwner && ( + + )} + + + ); +}; + +CollectionItemRow.propTypes = { + collection: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }).isRequired, + item: PropTypes.shape({ + createdAt: PropTypes.string.isRequired, + projectId: PropTypes.string.isRequired, + isDeleted: PropTypes.bool.isRequired, + project: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + user: PropTypes.shape({ + username: PropTypes.string.isRequired + }) + }).isRequired + }).isRequired, + isOwner: PropTypes.bool.isRequired, + removeFromCollection: PropTypes.func.isRequired +}; + +export default CollectionItemRow; From 5c424eddb492112e7d24ba9194c116d46452f9ee Mon Sep 17 00:00:00 2001 From: nahbee10 Date: Fri, 13 Sep 2024 14:40:39 -0400 Subject: [PATCH 02/10] fix connect import for redux --- client/modules/IDE/components/FileNode.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index f8df755a0f..f0bb7637d3 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -1,8 +1,8 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import React, { useState, useRef, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { bindActionCreators, connect } from 'redux'; +import { useDispatch, useSelector, connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; import { withTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'; import * as IDEActions from '../actions/ide'; From 4cd3e0e045942373c16cb910878080750d46bedd Mon Sep 17 00:00:00 2001 From: nahbee10 Date: Fri, 13 Sep 2024 14:59:51 -0400 Subject: [PATCH 03/10] fix test error --- client/modules/IDE/components/FileNode.jsx | 1 - client/modules/IDE/components/SketchList.unit.test.jsx | 4 +++- client/modules/User/pages/DashboardView.jsx | 7 ++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index f0bb7637d3..f49fcd0ed6 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -93,7 +93,6 @@ const FileNode = ({ const { t } = useTranslation(); const fileNameInput = useRef(null); const fileOptionsRef = useRef(null); - const dispatch = useDispatch(); const handleFileClick = (event) => { event.stopPropagation(); diff --git a/client/modules/IDE/components/SketchList.unit.test.jsx b/client/modules/IDE/components/SketchList.unit.test.jsx index 162d12bcc1..75d92591e4 100644 --- a/client/modules/IDE/components/SketchList.unit.test.jsx +++ b/client/modules/IDE/components/SketchList.unit.test.jsx @@ -1,6 +1,7 @@ import React from 'react'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { useTranslation } from 'react-i18next'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { act } from 'react-dom/test-utils'; @@ -26,8 +27,9 @@ afterAll(() => server.close()); describe('', () => { const mockStore = configureStore([thunk]); const store = mockStore(initialTestState); + const { t } = useTranslation(); - let subjectProps = { username: initialTestState.user.username }; + let subjectProps = { username: initialTestState.user.username, t }; const subject = () => reduxRender(, { store }); diff --git a/client/modules/User/pages/DashboardView.jsx b/client/modules/User/pages/DashboardView.jsx index fcde949cd7..d729a18e88 100644 --- a/client/modules/User/pages/DashboardView.jsx +++ b/client/modules/User/pages/DashboardView.jsx @@ -107,7 +107,12 @@ const DashboardView = () => { case TabKey.sketches: default: return ( - + ); } }; From a46664009fff0d78a635d20f02615d0181fd8479 Mon Sep 17 00:00:00 2001 From: nahbee10 Date: Sun, 15 Sep 2024 20:58:16 -0400 Subject: [PATCH 04/10] console height error ifx --- client/modules/IDE/components/Console.jsx | 4 ++ .../modules/IDE/components/ConsoleInput.jsx | 64 +++++++++---------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/client/modules/IDE/components/Console.jsx b/client/modules/IDE/components/Console.jsx index f4f8f32396..f8668ce62d 100644 --- a/client/modules/IDE/components/Console.jsx +++ b/client/modules/IDE/components/Console.jsx @@ -227,6 +227,10 @@ const Console = () => { }; }); + useEffect(() => { + console.log(isExpanded); + }, [isExpanded]); + const consoleClass = classNames({ 'preview-console': true, 'preview-console--collapsed': !isExpanded diff --git a/client/modules/IDE/components/ConsoleInput.jsx b/client/modules/IDE/components/ConsoleInput.jsx index 3c6470f518..660f8ec2b2 100644 --- a/client/modules/IDE/components/ConsoleInput.jsx +++ b/client/modules/IDE/components/ConsoleInput.jsx @@ -1,11 +1,15 @@ import PropTypes from 'prop-types'; -import React, { useState, useEffect, useRef } from 'react'; +import React, { useRef, useEffect, useState } from 'react'; import CodeMirror from 'codemirror'; import { Encode } from 'console-feed'; + import RightArrowIcon from '../../../images/right-arrow.svg'; import { dispatchMessage, MessageTypes } from '../../../utils/dispatcher'; -const ConsoleInput = ({ theme, dispatchConsoleEvent, fontSize }) => { +// heavily inspired by +// https://github.com/codesandbox/codesandbox-client/blob/92a1131f4ded6f7d9c16945dc7c18aa97c8ada27/packages/app/src/app/components/Preview/DevTools/Console/Input/index.tsx + +function ConsoleInput({ theme, dispatchConsoleEvent, fontSize }) { const [commandHistory, setCommandHistory] = useState([]); const [commandCursor, setCommandCursor] = useState(-1); const codemirrorContainer = useRef(null); @@ -25,10 +29,9 @@ const ConsoleInput = ({ theme, dispatchConsoleEvent, fontSize }) => { e.preventDefault(); e.stopPropagation(); const value = cm.getValue(); - if (value.trim() === '') { + if (value.trim(' ') === '') { return false; } - const messages = [ { log: Encode({ method: 'command', data: [value] }) } ]; @@ -42,7 +45,7 @@ const ConsoleInput = ({ theme, dispatchConsoleEvent, fontSize }) => { }); dispatchConsoleEvent(consoleEvent); cm.setValue(''); - setCommandHistory((prevHistory) => [value, ...prevHistory]); + setCommandHistory([value, ...commandHistory]); setCommandCursor(-1); } else if (e.key === 'ArrowUp') { const lineNumber = cmInstance.current.getDoc().getCursor().line; @@ -50,13 +53,14 @@ const ConsoleInput = ({ theme, dispatchConsoleEvent, fontSize }) => { return false; } - setCommandCursor((prevCursor) => { - const newCursor = Math.min(prevCursor + 1, commandHistory.length - 1); - cmInstance.current.getDoc().setValue(commandHistory[newCursor] || ''); - const cursorPos = cmInstance.current.getDoc().getLine(0).length - 1; - cmInstance.current.getDoc().setCursor({ line: 0, ch: cursorPos }); - return newCursor; - }); + const newCursor = Math.min( + commandCursor + 1, + commandHistory.length - 1 + ); + cmInstance.current.getDoc().setValue(commandHistory[newCursor] || ''); + const cursorPos = cmInstance.current.getDoc().getLine(0).length - 1; + cmInstance.current.getDoc().setCursor({ line: 0, ch: cursorPos }); + setCommandCursor(newCursor); } else if (e.key === 'ArrowDown') { const lineNumber = cmInstance.current.getDoc().getCursor().line; const lineCount = cmInstance.current.getValue().split('\n').length; @@ -64,17 +68,15 @@ const ConsoleInput = ({ theme, dispatchConsoleEvent, fontSize }) => { return false; } - setCommandCursor((prevCursor) => { - const newCursor = Math.max(prevCursor - 1, -1); - cmInstance.current.getDoc().setValue(commandHistory[newCursor] || ''); - const newLineCount = cmInstance.current.getValue().split('\n').length; - const newLine = cmInstance.current.getDoc().getLine(newLineCount); - const cursorPos = newLine ? newLine.length - 1 : 1; - cmInstance.current - .getDoc() - .setCursor({ line: lineCount, ch: cursorPos }); - return newCursor; - }); + const newCursor = Math.max(commandCursor - 1, -1); + cmInstance.current.getDoc().setValue(commandHistory[newCursor] || ''); + const newLineCount = cmInstance.current.getValue().split('\n').length; + const newLine = cmInstance.current.getDoc().getLine(newLineCount); + const cursorPos = newLine ? newLine.length - 1 : 1; + cmInstance.current + .getDoc() + .setCursor({ line: lineCount, ch: cursorPos }); + setCommandCursor(newCursor); } return true; }); @@ -82,18 +84,14 @@ const ConsoleInput = ({ theme, dispatchConsoleEvent, fontSize }) => { cmInstance.current.getWrapperElement().style['font-size'] = `${fontSize}px`; return () => { - cmInstance.current = null; // Cleanup when the component unmounts + cmInstance.current = null; }; - }, [theme, dispatchConsoleEvent, fontSize, commandHistory]); + }, []); useEffect(() => { - if (cmInstance.current) { - cmInstance.current.setOption('theme', `p5-${theme}`); - cmInstance.current.getWrapperElement().style[ - 'font-size' - ] = `${fontSize}px`; - cmInstance.current.refresh(); - } + cmInstance.current.setOption('theme', `p5-${theme}`); + cmInstance.current.getWrapperElement().style['font-size'] = `${fontSize}px`; + cmInstance.current.refresh(); }, [theme, fontSize]); return ( @@ -115,7 +113,7 @@ const ConsoleInput = ({ theme, dispatchConsoleEvent, fontSize }) => {
); -}; +} ConsoleInput.propTypes = { theme: PropTypes.string.isRequired, From 9ed5799d4b1c8e35643d6cf9f44915f2badcd621 Mon Sep 17 00:00:00 2001 From: nahbee10 Date: Sun, 15 Sep 2024 22:23:42 -0400 Subject: [PATCH 05/10] fix test error --- .../IDE/components/SketchList.unit.test.jsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/client/modules/IDE/components/SketchList.unit.test.jsx b/client/modules/IDE/components/SketchList.unit.test.jsx index 75d92591e4..2600cb85f6 100644 --- a/client/modules/IDE/components/SketchList.unit.test.jsx +++ b/client/modules/IDE/components/SketchList.unit.test.jsx @@ -1,7 +1,6 @@ import React from 'react'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { useTranslation } from 'react-i18next'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { act } from 'react-dom/test-utils'; @@ -9,6 +8,20 @@ import SketchList from './SketchList'; import { reduxRender, fireEvent, screen, within } from '../../../test-utils'; import { initialTestState } from '../../../testData/testReduxStore'; +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key) => key, + i18n: { + changeLanguage: jest.fn(), + language: 'en-US' + } + }), + initReactI18next: { + type: '3rdParty', + init: jest.fn() + } +})); + jest.mock('../../../i18n'); const server = setupServer( @@ -27,9 +40,8 @@ afterAll(() => server.close()); describe('', () => { const mockStore = configureStore([thunk]); const store = mockStore(initialTestState); - const { t } = useTranslation(); - let subjectProps = { username: initialTestState.user.username, t }; + let subjectProps = { username: initialTestState.user.username }; const subject = () => reduxRender(, { store }); From 3ce1cb4bdc7463dec2e045348d8e7ffb01e030b4 Mon Sep 17 00:00:00 2001 From: nahbee10 Date: Sun, 15 Sep 2024 23:08:35 -0400 Subject: [PATCH 06/10] add stoppropagation --- client/modules/IDE/components/SketchListRowBase.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/modules/IDE/components/SketchListRowBase.jsx b/client/modules/IDE/components/SketchListRowBase.jsx index 2822aa0b03..a158b7153c 100644 --- a/client/modules/IDE/components/SketchListRowBase.jsx +++ b/client/modules/IDE/components/SketchListRowBase.jsx @@ -92,6 +92,7 @@ const SketchListRowBase = ({ onChange={handleRenameChange} onKeyDown={handleRenameEnter} onBlur={handleRenameBlur} + onClick={(e) => e.stopPropagation()} ref={renameInput} maxLength={128} /> From 4ffc471e204e739798d1f4745d5605caa424b754 Mon Sep 17 00:00:00 2001 From: Connie Ye Date: Thu, 19 Sep 2024 23:44:57 -0700 Subject: [PATCH 07/10] fix test errors in SketchList --- client/modules/IDE/components/SketchList.jsx | 8 +++++--- .../IDE/components/SketchList.unit.test.jsx | 16 ---------------- client/modules/User/pages/DashboardView.jsx | 7 +------ 3 files changed, 6 insertions(+), 25 deletions(-) diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index e998137384..c792fd767d 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import React, { useEffect, useState, useCallback } from 'react'; import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; +import { useTranslation } from 'react-i18next'; import { bindActionCreators } from 'redux'; import * as ProjectsActions from '../actions/projects'; // Added Projects actions import * as CollectionsActions from '../actions/collections'; // Added Collections actions @@ -25,11 +26,11 @@ const SketchList = ({ sorting, toggleDirectionForField, resetSorting, - t, mobile }) => { const [isInitialDataLoad, setIsInitialDataLoad] = useState(true); const [sketchToAddToCollection, setSketchToAddToCollection] = useState(null); + const { t } = useTranslation(); useEffect(() => { getProjects(username); @@ -98,11 +99,13 @@ const SketchList = ({ {field === fieldName && (direction === SortingActions.DIRECTION.ASC ? ( ) : ( @@ -192,8 +195,7 @@ SketchList.propTypes = { field: PropTypes.string.isRequired, direction: PropTypes.string.isRequired }).isRequired, - mobile: PropTypes.bool, - t: PropTypes.func.isRequired + mobile: PropTypes.bool }; SketchList.defaultProps = { diff --git a/client/modules/IDE/components/SketchList.unit.test.jsx b/client/modules/IDE/components/SketchList.unit.test.jsx index 2600cb85f6..9110c43951 100644 --- a/client/modules/IDE/components/SketchList.unit.test.jsx +++ b/client/modules/IDE/components/SketchList.unit.test.jsx @@ -8,22 +8,6 @@ import SketchList from './SketchList'; import { reduxRender, fireEvent, screen, within } from '../../../test-utils'; import { initialTestState } from '../../../testData/testReduxStore'; -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key) => key, - i18n: { - changeLanguage: jest.fn(), - language: 'en-US' - } - }), - initReactI18next: { - type: '3rdParty', - init: jest.fn() - } -})); - -jest.mock('../../../i18n'); - const server = setupServer( rest.get(`/${initialTestState.user.username}/projects`, (req, res, ctx) => // it just needs to return something so it doesn't throw an error diff --git a/client/modules/User/pages/DashboardView.jsx b/client/modules/User/pages/DashboardView.jsx index d729a18e88..fcde949cd7 100644 --- a/client/modules/User/pages/DashboardView.jsx +++ b/client/modules/User/pages/DashboardView.jsx @@ -107,12 +107,7 @@ const DashboardView = () => { case TabKey.sketches: default: return ( - + ); } }; From cf00dc440c7ae3ab7856ef20fb4862dd45833a92 Mon Sep 17 00:00:00 2001 From: nahbee10 Date: Wed, 25 Sep 2024 09:39:34 -0400 Subject: [PATCH 08/10] remove comments and use better react functions to memoize --- client/modules/IDE/components/AssetList.jsx | 12 +-- .../CollectionList/CollectionList.jsx | 17 ++-- client/modules/IDE/components/FileNode.jsx | 16 ++-- client/modules/IDE/components/SketchList.jsx | 83 ++++++++++--------- .../IDE/components/SketchListRowBase.jsx | 2 +- client/modules/User/components/Collection.jsx | 5 -- 6 files changed, 61 insertions(+), 74 deletions(-) diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index f8a7cb5e8f..b435a52c85 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -1,13 +1,13 @@ -import PropTypes from 'prop-types'; import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Helmet } from 'react-helmet'; -import { withTranslation, useTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import AssetListRow from './AssetListRow'; import Loader from '../../App/components/loader'; import * as AssetActions from '../actions/assets'; -const AssetList = ({ t }) => { +const AssetList = () => { + const { t } = useTranslation(); const dispatch = useDispatch(); const { username, assetList, loading } = useSelector((state) => ({ username: state.user.username, @@ -60,8 +60,4 @@ const AssetList = ({ t }) => { ); }; -AssetList.propTypes = { - t: PropTypes.func.isRequired -}; - -export default withTranslation()(AssetList); +export default AssetList; diff --git a/client/modules/IDE/components/CollectionList/CollectionList.jsx b/client/modules/IDE/components/CollectionList/CollectionList.jsx index 8f6f24767d..8fcd621ea6 100644 --- a/client/modules/IDE/components/CollectionList/CollectionList.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionList.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { Helmet } from 'react-helmet'; -import { withTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import classNames from 'classnames'; @@ -34,9 +34,9 @@ const CollectionList = ({ resetSorting, sorting, project, - t, mobile }) => { + const { t } = useTranslation(); const [hasLoadedData, setHasLoadedData] = useState(false); const [ addingSketchesToCollectionId, @@ -64,7 +64,7 @@ const CollectionList = ({ } }, [loading]); - const getTitle = useCallback(() => { + const getTitle = useMemo(() => { if (propsUsername === user.username) { return t('CollectionList.Title'); } @@ -100,7 +100,7 @@ const CollectionList = ({ return null; }; - const getButtonLabel = useCallback( + const getButtonLabel = useMemo( (fieldName, displayName) => { const { field, direction } = sorting; let buttonLabel; @@ -162,7 +162,7 @@ const CollectionList = ({ return (
- {getTitle()} + {getTitle} {renderLoader()} @@ -258,7 +258,6 @@ CollectionList.propTypes = { id: PropTypes.string }) }), - t: PropTypes.func.isRequired, mobile: PropTypes.bool }; @@ -297,6 +296,4 @@ function mapDispatchToProps(dispatch) { ); } -export default withTranslation()( - connect(mapStateToProps, mapDispatchToProps)(CollectionList) -); +export default connect(mapStateToProps, mapDispatchToProps)(CollectionList); diff --git a/client/modules/IDE/components/FileNode.jsx b/client/modules/IDE/components/FileNode.jsx index f49fcd0ed6..f893d246af 100644 --- a/client/modules/IDE/components/FileNode.jsx +++ b/client/modules/IDE/components/FileNode.jsx @@ -1,9 +1,8 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; -import React, { useState, useRef, useEffect } from 'react'; -import { useDispatch, useSelector, connect } from 'react-redux'; +import React, { useState, useRef } from 'react'; +import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { withTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'; import * as IDEActions from '../actions/ide'; import * as FileActions from '../actions/files'; @@ -86,7 +85,6 @@ const FileNode = ({ }) => { const [isOptionsOpen, setIsOptionsOpen] = useState(false); const [isEditingName, setIsEditingName] = useState(false); - const [isFocused, setIsFocused] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [updatedName, setUpdatedName] = useState(name); @@ -245,7 +243,7 @@ const FileNode = ({
- - ); - }; + const renderFieldHeader = useCallback( + (fieldName, displayName) => { + const { field, direction } = sorting; + const headerClass = classNames({ + 'sketches-table__header': true, + 'sketches-table__header--selected': field === fieldName + }); + const buttonLabel = getButtonLabel(fieldName, displayName); + return ( + + + + ); + }, + [sorting, getButtonLabel, toggleDirectionForField, t] + ); return (
- {getSketchesTitle()} + {getSketchesTitle} {renderLoader()} {renderEmptyTable()} diff --git a/client/modules/IDE/components/SketchListRowBase.jsx b/client/modules/IDE/components/SketchListRowBase.jsx index a158b7153c..5c00015fdc 100644 --- a/client/modules/IDE/components/SketchListRowBase.jsx +++ b/client/modules/IDE/components/SketchListRowBase.jsx @@ -5,7 +5,7 @@ import { Link } from 'react-router-dom'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import * as ProjectActions from '../actions/project'; -import * as IdeActions from '../actions/ide'; // Adding the missing Ide actions +import * as IdeActions from '../actions/ide'; import TableDropdown from '../../../components/Dropdown/TableDropdown'; import MenuItem from '../../../components/Dropdown/MenuItem'; import dates from '../../../utils/formatDate'; diff --git a/client/modules/User/components/Collection.jsx b/client/modules/User/components/Collection.jsx index 34acf688ef..59f89e4a34 100644 --- a/client/modules/User/components/Collection.jsx +++ b/client/modules/User/components/Collection.jsx @@ -4,15 +4,10 @@ import { useDispatch, useSelector } from 'react-redux'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import * as ProjectActions from '../../IDE/actions/project'; -import * as ProjectsActions from '../../IDE/actions/projects'; import * as CollectionsActions from '../../IDE/actions/collections'; -import * as ToastActions from '../../IDE/actions/toast'; import * as SortingActions from '../../IDE/actions/sorting'; -import * as IdeActions from '../../IDE/actions/ide'; import { getCollection } from '../../IDE/selectors/collections'; import Loader from '../../App/components/loader'; -import dates from '../../../utils/formatDate'; import ArrowUpIcon from '../../../images/sort-arrow-up.svg'; import ArrowDownIcon from '../../../images/sort-arrow-down.svg'; import CollectionMetadata from './CollectionMetadata'; From d7fe122e60c233233e4975ab5f82449df46362ec Mon Sep 17 00:00:00 2001 From: nahbee10 Date: Wed, 2 Oct 2024 13:03:07 -0400 Subject: [PATCH 09/10] final edits --- client/modules/IDE/components/AssetList.jsx | 2 +- .../IDE/components/CollectionList/CollectionList.jsx | 9 +-------- client/modules/IDE/components/Console.jsx | 4 ---- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index b435a52c85..cf827b0354 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -17,7 +17,7 @@ const AssetList = () => { useEffect(() => { dispatch(AssetActions.getAssets()); - }, [dispatch]); + }, []); const hasAssets = () => !loading && assetList.length > 0; diff --git a/client/modules/IDE/components/CollectionList/CollectionList.jsx b/client/modules/IDE/components/CollectionList/CollectionList.jsx index 8fcd621ea6..18fd69246f 100644 --- a/client/modules/IDE/components/CollectionList/CollectionList.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionList.jsx @@ -49,14 +49,7 @@ const CollectionList = ({ } getCollections(propsUsername || user.username); resetSorting(); - }, [ - projectId, - getCollections, - getProject, - propsUsername, - resetSorting, - user.username - ]); + }, []); useEffect(() => { if (!loading) { diff --git a/client/modules/IDE/components/Console.jsx b/client/modules/IDE/components/Console.jsx index f8668ce62d..f4f8f32396 100644 --- a/client/modules/IDE/components/Console.jsx +++ b/client/modules/IDE/components/Console.jsx @@ -227,10 +227,6 @@ const Console = () => { }; }); - useEffect(() => { - console.log(isExpanded); - }, [isExpanded]); - const consoleClass = classNames({ 'preview-console': true, 'preview-console--collapsed': !isExpanded From c7a619c43c43bec41cd24df7b99b6ca3b02beb27 Mon Sep 17 00:00:00 2001 From: nahbee10 Date: Wed, 2 Oct 2024 13:20:21 -0400 Subject: [PATCH 10/10] fix collection list error --- .../CollectionList/CollectionList.jsx | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/client/modules/IDE/components/CollectionList/CollectionList.jsx b/client/modules/IDE/components/CollectionList/CollectionList.jsx index 18fd69246f..cf25be5018 100644 --- a/client/modules/IDE/components/CollectionList/CollectionList.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionList.jsx @@ -93,28 +93,25 @@ const CollectionList = ({ return null; }; - const getButtonLabel = useMemo( - (fieldName, displayName) => { - const { field, direction } = sorting; - let buttonLabel; - if (field !== fieldName) { - buttonLabel = - field === 'name' - ? t('CollectionList.ButtonLabelAscendingARIA', { displayName }) - : t('CollectionList.ButtonLabelDescendingARIA', { displayName }); - } else if (direction === SortingActions.DIRECTION.ASC) { - buttonLabel = t('CollectionList.ButtonLabelDescendingARIA', { - displayName - }); - } else { - buttonLabel = t('CollectionList.ButtonLabelAscendingARIA', { - displayName - }); - } - return buttonLabel; - }, - [sorting, t] - ); + const getButtonLabel = (fieldName, displayName) => { + const { field, direction } = sorting; + let buttonLabel; + if (field !== fieldName) { + buttonLabel = + field === 'name' + ? t('CollectionList.ButtonLabelAscendingARIA', { displayName }) + : t('CollectionList.ButtonLabelDescendingARIA', { displayName }); + } else if (direction === SortingActions.DIRECTION.ASC) { + buttonLabel = t('CollectionList.ButtonLabelDescendingARIA', { + displayName + }); + } else { + buttonLabel = t('CollectionList.ButtonLabelAscendingARIA', { + displayName + }); + } + return buttonLabel; + }; const renderFieldHeader = (fieldName, displayName) => { const { field, direction } = sorting;