diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index 559f60c580..584cd57238 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -1,220 +1,182 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import prettyBytes from 'pretty-bytes'; import { withTranslation } from 'react-i18next'; +import PropTypes from 'prop-types'; // Import PropTypes import Loader from '../../App/components/loader'; import * as AssetActions from '../actions/assets'; import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg'; -class AssetListRowBase extends React.Component { - constructor(props) { - super(props); - this.state = { - isFocused: false, - optionsOpen: false - }; - } - - onFocusComponent = () => { - this.setState({ isFocused: true }); +function AssetListRowBase(props) { + const [isFocused, setIsFocused] = useState(false); + const [optionsOpen, setOptionsOpen] = useState(false); + + const onFocusComponent = () => { + setIsFocused(true); + }; + + const closeOptions = () => { + setOptionsOpen(false); }; - onBlurComponent = () => { - this.setState({ isFocused: false }); + const onBlurComponent = () => { + setIsFocused(false); setTimeout(() => { - if (!this.state.isFocused) { - this.closeOptions(); + if (!isFocused) { + closeOptions(); } }, 200); }; - openOptions = () => { - this.setState({ - optionsOpen: true - }); - }; - - closeOptions = () => { - this.setState({ - optionsOpen: false - }); + const openOptions = () => { + setOptionsOpen(true); }; - toggleOptions = () => { - if (this.state.optionsOpen) { - this.closeOptions(); + const toggleOptions = () => { + if (optionsOpen) { + closeOptions(); } else { - this.openOptions(); + openOptions(); } }; - handleDropdownOpen = () => { - this.closeOptions(); - this.openOptions(); - }; - - handleAssetDelete = () => { - const { key, name } = this.props.asset; - this.closeOptions(); - if (window.confirm(this.props.t('Common.DeleteConfirmation', { name }))) { - this.props.deleteAssetRequest(key); + const handleAssetDelete = () => { + const { key, name } = props.asset; + closeOptions(); + if (window.confirm(props.t('Common.DeleteConfirmation', { name }))) { + props.deleteAssetRequest(key); } }; - render() { - const { asset, username, t } = this.props; - const { optionsOpen } = this.state; - return ( - - - - {asset.name} + const { asset, username, t } = props; + return ( + + + + {asset.name} + + + {prettyBytes(asset.size)} + + {asset.sketchId && ( + + {asset.sketchName} - - {prettyBytes(asset.size)} - - {asset.sketchId && ( - - {asset.sketchName} - - )} - - - - {optionsOpen && ( - - )} - - - ); - } -} - -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, - deleteAssetRequest: PropTypes.func.isRequired, - username: PropTypes.string.isRequired, - t: PropTypes.func.isRequired -}; - -function mapStateToPropsAssetListRow(state) { - return { - username: state.user.username - }; -} - -function mapDispatchToPropsAssetListRow(dispatch) { - return bindActionCreators(AssetActions, dispatch); + )} + + + + {optionsOpen && ( + + )} + + + ); } -const AssetListRow = connect( - mapStateToPropsAssetListRow, - mapDispatchToPropsAssetListRow -)(AssetListRowBase); +function AssetList(props) { + useEffect(() => { + props.getAssets(); + }, []); -class AssetList extends React.Component { - constructor(props) { - super(props); - this.props.getAssets(); - } + const getAssetsTitle = () => props.t('AssetList.Title'); - getAssetsTitle() { - return this.props.t('AssetList.Title'); - } + const hasAssets = () => !props.loading && props.assetList.length > 0; - hasAssets() { - return !this.props.loading && this.props.assetList.length > 0; - } - - renderLoader() { - if (this.props.loading) return ; + const renderLoader = () => { + if (props.loading) return ; return null; - } + }; - renderEmptyTable() { - if (!this.props.loading && this.props.assetList.length === 0) { + const renderEmptyTable = () => { + if (!props.loading && props.assetList.length === 0) { return (

- {this.props.t('AssetList.NoUploadedAssets')} + {props.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')}
- )} -
- ); - } + }; + + const { assetList, t } = props; + return ( +
+ + {getAssetsTitle()} + + {renderLoader()} + {renderEmptyTable()} + {hasAssets() && ( + + + + + + + + + + + {assetList.map((asset) => ( + + ))} + +
{t('AssetList.HeaderName')}{t('AssetList.HeaderSize')}{t('AssetList.HeaderSketch')}
+ )} +
+ ); } +// AssetListRowBase component PropTypes +AssetListRowBase.propTypes = { + asset: PropTypes.string.isRequired, + key: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + sketchName: PropTypes.string.isRequired, + sketchId: PropTypes.string.isRequired, + size: PropTypes.number.isRequired, + username: PropTypes.string.isRequired, + t: PropTypes.string.isRequired, + deleteAssetRequest: PropTypes.string.isRequired +}; + +// AssetList component PropTypes AssetList.propTypes = { user: PropTypes.shape({ username: PropTypes.string @@ -225,7 +187,8 @@ AssetList.propTypes = { name: PropTypes.string.isRequired, url: PropTypes.string.isRequired, sketchName: PropTypes.string, - sketchId: PropTypes.string + sketchId: PropTypes.string, + size: PropTypes.number.isRequired }) ).isRequired, getAssets: PropTypes.func.isRequired, diff --git a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx index 16421f7a6b..646b9824b5 100644 --- a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx @@ -1,299 +1,314 @@ import PropTypes from 'prop-types'; import React 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 { withTranslation } from 'react-i18next'; +import classNames from 'classnames'; +import find from 'lodash/find'; import * as ProjectActions from '../../actions/project'; +import * as ProjectsActions from '../../actions/projects'; import * as CollectionsActions from '../../actions/collections'; -import * as IdeActions from '../../actions/ide'; import * as ToastActions from '../../actions/toast'; -import dates from '../../../../utils/formatDate'; +import * as SortingActions from '../../actions/sorting'; +import getSortedCollections from '../../selectors/collections'; +import Loader from '../../../App/components/loader'; +import Overlay from '../../../App/components/Overlay'; +import AddToCollectionSketchList from '../AddToCollectionSketchList'; +import { SketchSearchbar } from '../Searchbar'; -import DownFilledTriangleIcon from '../../../../images/down-filled-triangle.svg'; -import MoreIconSvg from '../../../../images/more.svg'; +import CollectionListRow from './CollectionListRow'; -const formatDateCell = (date, mobile = false) => - dates.format(date, { showTime: !mobile }); - -class CollectionListRowBase extends React.Component { - static projectInCollection(project, collection) { - return ( - collection.items.find((item) => item.project.id === project.id) != null - ); - } +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); + + if (props.projectId) { + props.getProject(props.projectId); + } + + this.props.getCollections(this.props.username); + this.props.resetSorting(); + this.state = { - optionsOpen: false, - isFocused: false, - renameOpen: false, - renameValue: '' + hasLoadedData: false, + addingSketchesToCollectionId: null }; - this.renameInput = React.createRef(); } - onFocusComponent = () => { - this.setState({ isFocused: true }); - }; - - onBlurComponent = () => { - this.setState({ isFocused: false }); - setTimeout(() => { - if (!this.state.isFocused) { - this.closeAll(); - } - }, 200); - }; + 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 + }); + } + } - openOptions = () => { - this.setState({ - optionsOpen: true + getTitle() { + if (this.props.username === this.props.user.username) { + return this.props.t('CollectionList.Title'); + } + return this.props.t('CollectionList.AnothersTitle', { + anotheruser: this.props.username }); - }; + } - closeOptions = () => { + showAddSketches = (collectionId) => { this.setState({ - optionsOpen: false + addingSketchesToCollectionId: collectionId }); }; - toggleOptions = () => { - if (this.state.optionsOpen) { - this.closeOptions(); - } else { - this.openOptions(); - } - }; - - closeAll = () => { + hideAddSketches = () => { this.setState({ - optionsOpen: false, - renameOpen: false + addingSketchesToCollectionId: null }); }; - handleAddSketches = () => { - this.closeAll(); - this.props.onAddSketches(); - }; - - handleDropdownOpen = () => { - this.closeAll(); - this.openOptions(); - }; - - handleCollectionDelete = () => { - this.closeAll(); - if ( - window.confirm( - this.props.t('Common.DeleteConfirmation', { - name: this.props.collection.name - }) - ) - ) { - this.props.deleteCollection(this.props.collection.id); - } - }; - - handleRenameOpen = () => { - this.closeAll(); - this.setState( - { - renameOpen: true, - renameValue: this.props.collection.name - }, - () => this.renameInput.current.focus() + hasCollections() { + return ( + (!this.props.loading || this.state.hasLoadedData) && + this.props.collections.length > 0 ); - }; + } - handleRenameChange = (e) => { - this.setState({ - renameValue: e.target.value - }); - }; + _renderLoader() { + if (this.props.loading && !this.state.hasLoadedData) return ; + return null; + } - handleRenameEnter = (e) => { - if (e.key === 'Enter') { - this.updateName(); - this.closeAll(); + _renderEmptyTable() { + if (!this.props.loading && this.props.collections.length === 0) { + return ( +

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

+ ); } - }; - - handleRenameBlur = () => { - this.updateName(); - this.closeAll(); - }; + return null; + } - updateName = () => { - const isValid = this.state.renameValue.trim().length !== 0; - if (isValid) { - this.props.editCollection(this.props.collection.id, { - name: this.state.renameValue.trim() + _getButtonLabel = (fieldName, displayName) => { + const { field, direction } = this.props.sorting; + let buttonLabel; + if (field !== fieldName) { + if (field === 'name') { + buttonLabel = this.props.t('CollectionList.ButtonLabelAscendingARIA', { + displayName + }); + } else { + buttonLabel = this.props.t('CollectionList.ButtonLabelDescendingARIA', { + displayName + }); + } + } else if (direction === SortingActions.DIRECTION.ASC) { + buttonLabel = this.props.t('CollectionList.ButtonLabelDescendingARIA', { + displayName + }); + } else { + buttonLabel = this.props.t('CollectionList.ButtonLabelAscendingARIA', { + displayName }); } + return buttonLabel; }; - renderActions = () => { - const { optionsOpen } = this.state; - const userIsOwner = this.props.user.username === this.props.username; - + _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 ( - + - {optionsOpen && ( -
    -
  • - -
  • - {userIsOwner && ( -
  • - -
  • + {displayName} + {field === fieldName && + direction === SortingActions.DIRECTION.ASC && ( + )} - {userIsOwner && ( -
  • - -
  • + {field === fieldName && + direction === SortingActions.DIRECTION.DESC && ( + )} -
- )} -
- ); - }; - - renderCollectionName = () => { - const { collection, username } = this.props; - const { renameOpen, renameValue } = this.state; - - return ( - - - {renameOpen ? '' : collection.name} - - {renameOpen && ( - e.stopPropagation()} - ref={this.renameInput} - /> - )} - + + ); }; render() { - const { collection, mobile } = this.props; + const username = + this.props.username !== undefined + ? this.props.username + : this.props.user.username; + const { mobile } = this.props; return ( - - - - {this.renderCollectionName()} - - - {formatDateCell(collection.createdAt, mobile)} - {formatDateCell(collection.updatedAt, mobile)} - - {mobile && 'sketches: '} - {(collection.items || []).length} - - {this.renderActions()} - +
+ + {this.getTitle()} + + + {this._renderLoader()} + {this._renderEmptyTable()} + {this.hasCollections() && ( + + + + {this._renderFieldHeader( + 'name', + this.props.t('CollectionList.HeaderName') + )} + {this._renderFieldHeader( + 'createdAt', + this.props.t('CollectionList.HeaderCreatedAt', { + context: mobile ? 'mobile' : '' + }) + )} + {this._renderFieldHeader( + 'updatedAt', + this.props.t('CollectionList.HeaderUpdatedAt', { + context: mobile ? 'mobile' : '' + }) + )} + {this._renderFieldHeader( + 'numItems', + this.props.t('CollectionList.HeaderNumItems', { + context: mobile ? 'mobile' : '' + }) + )} + + + + + {this.props.collections.map((collection) => ( + this.showAddSketches(collection.id)} + /> + ))} + +
+ )} + {this.state.addingSketchesToCollectionId && ( + } + closeOverlay={this.hideAddSketches} + isFixedHeight + > + + + )} +
); } } -CollectionListRowBase.propTypes = { - collection: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - owner: PropTypes.shape({ - username: PropTypes.string.isRequired - }).isRequired, - createdAt: PropTypes.string.isRequired, - updatedAt: PropTypes.string.isRequired, - items: PropTypes.arrayOf( - PropTypes.shape({ - project: PropTypes.shape({ - id: PropTypes.string.isRequired - }) - }) - ) - }).isRequired, - username: PropTypes.string.isRequired, +CollectionList.propTypes = { user: PropTypes.shape({ username: PropTypes.string, authenticated: PropTypes.bool.isRequired }).isRequired, - deleteCollection: PropTypes.func.isRequired, - editCollection: PropTypes.func.isRequired, - onAddSketches: PropTypes.func.isRequired, - mobile: PropTypes.bool, - t: PropTypes.func.isRequired + projectId: PropTypes.string, + getCollections: PropTypes.func.isRequired, + getProject: PropTypes.func.isRequired, + collections: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + description: PropTypes.string, + createdAt: PropTypes.string.isRequired, + updatedAt: PropTypes.string.isRequired + }) + ).isRequired, + 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, + project: PropTypes.shape({ + id: PropTypes.string, + owner: PropTypes.shape({ + id: PropTypes.string + }) + }), + t: PropTypes.func.isRequired, + mobile: PropTypes.bool }; -CollectionListRowBase.defaultProps = { +CollectionList.defaultProps = { + projectId: undefined, + project: { + id: undefined, + owner: undefined + }, + username: undefined, mobile: false }; -function mapDispatchToPropsSketchListRow(dispatch) { +function mapStateToProps(state, ownProps) { + return { + user: state.user, + collections: getSortedCollections(state), + sorting: state.sorting, + loading: state.loading, + project: state.project, + projectId: ownProps && ownProps.params ? ownProps.params.project_id : null + }; +} + +function mapDispatchToProps(dispatch) { return bindActionCreators( Object.assign( {}, CollectionsActions, + ProjectsActions, ProjectActions, - IdeActions, - ToastActions + ToastActions, + SortingActions ), dispatch ); } export default withTranslation()( - connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase) + connect(mapStateToProps, mapDispatchToProps)(CollectionList) );