diff --git a/client/common/Table/TableBase.jsx b/client/common/Table/TableBase.jsx new file mode 100644 index 0000000000..99c012cf2b --- /dev/null +++ b/client/common/Table/TableBase.jsx @@ -0,0 +1,107 @@ +import classNames from 'classnames'; +import { orderBy } from 'lodash'; +import PropTypes from 'prop-types'; +import React, { useMemo } from 'react'; +import Loader from '../../modules/App/components/loader'; +import { DIRECTION } from '../../modules/IDE/actions/sorting'; +import { TableEmpty } from './TableElements'; +import TableHeaderCell, { StyledHeaderCell } from './TableHeaderCell'; + +const toAscDesc = (direction) => (direction === DIRECTION.ASC ? 'asc' : 'desc'); + +/** + * Renders the headers, loading spinner, empty message. + * Sorts the array of items based on the `sortBy` prop. + * Expects a `renderRow` prop to render each row. + */ +function TableBase({ + sortBy, + onChangeSort, + columns, + items = [], + isLoading, + emptyMessage, + caption, + addDropdownColumn, + renderRow, + className +}) { + const sortedItems = useMemo( + () => orderBy(items, sortBy.field, toAscDesc(sortBy.direction)), + [sortBy.field, sortBy.direction, items] + ); + + if (isLoading) { + return ; + } + + if (items.length === 0) { + return {emptyMessage}; + } + + return ( + . + summary={caption} + > + + + {columns.map((column) => ( + + ))} + {addDropdownColumn && } + + + {sortedItems.map((item) => renderRow(item))} + + ); +} + +TableBase.propTypes = { + sortBy: PropTypes.shape({ + field: PropTypes.string.isRequired, + direction: PropTypes.string.isRequired + }).isRequired, + /** + * Function that gets called with the new sort order ({ field, direction }) + */ + onChangeSort: PropTypes.func.isRequired, + columns: PropTypes.arrayOf( + PropTypes.shape({ + field: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + defaultOrder: PropTypes.oneOf([DIRECTION.ASC, DIRECTION.DESC]), + formatValue: PropTypes.func + }) + ).isRequired, + items: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired + // Will have other properties, depending on the type. + }) + ), + renderRow: PropTypes.func.isRequired, + addDropdownColumn: PropTypes.bool, + isLoading: PropTypes.bool, + emptyMessage: PropTypes.string.isRequired, + caption: PropTypes.string, + className: PropTypes.string +}; + +TableBase.defaultProps = { + items: [], + isLoading: false, + caption: '', + addDropdownColumn: false, + className: '' +}; + +export default TableBase; diff --git a/client/common/Table/TableBase.test.jsx b/client/common/Table/TableBase.test.jsx new file mode 100644 index 0000000000..4a8bbe9d42 --- /dev/null +++ b/client/common/Table/TableBase.test.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { DIRECTION } from '../../modules/IDE/actions/sorting'; +import { render, screen } from '../../test-utils'; +import TableBase from './TableBase'; + +describe('', () => { + const items = [ + { id: '1', name: 'abc', count: 3 }, + { id: '2', name: 'def', count: 10 } + ]; + + const props = { + items, + sortBy: { field: 'count', direction: DIRECTION.DESC }, + emptyMessage: 'No items found', + renderRow: (item) => , + columns: [], + onChangeSort: jest.fn() + }; + + const subject = (overrideProps) => + render(); + + jest.spyOn(props, 'renderRow'); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('shows a spinner when loading', () => { + subject({ isLoading: true }); + + expect(document.querySelector('.loader')).toBeInTheDocument(); + }); + + it('show the `emptyMessage` when there are no items', () => { + subject({ items: [] }); + + expect(screen.getByText(props.emptyMessage)).toBeVisible(); + }); + + it('calls `renderRow` function for each row', () => { + subject(); + + expect(props.renderRow).toHaveBeenCalledTimes(2); + }); + + it('sorts the items', () => { + subject(); + + expect(props.renderRow).toHaveBeenNthCalledWith(1, items[1]); + expect(props.renderRow).toHaveBeenNthCalledWith(2, items[0]); + }); + + it('does not add an extra header if `addDropdownColumn` is false', () => { + subject({ addDropdownColumn: false }); + expect(screen.queryByRole('columnheader')).not.toBeInTheDocument(); + }); + + it('adds an extra header if `addDropdownColumn` is true', () => { + subject({ addDropdownColumn: true }); + expect(screen.getByRole('columnheader')).toBeInTheDocument(); + }); +}); diff --git a/client/common/Table/TableElements.jsx b/client/common/Table/TableElements.jsx new file mode 100644 index 0000000000..59a8b88a0c --- /dev/null +++ b/client/common/Table/TableElements.jsx @@ -0,0 +1,9 @@ +import styled from 'styled-components'; +import { remSize } from '../../theme'; + +// eslint-disable-next-line import/prefer-default-export +export const TableEmpty = styled.p` + text-align: center; + font-size: ${remSize(16)}; + padding: ${remSize(42)} 0; +`; diff --git a/client/common/Table/TableHeaderCell.jsx b/client/common/Table/TableHeaderCell.jsx new file mode 100644 index 0000000000..99cec86eac --- /dev/null +++ b/client/common/Table/TableHeaderCell.jsx @@ -0,0 +1,100 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { DIRECTION } from '../../modules/IDE/actions/sorting'; +import { prop, remSize } from '../../theme'; +import { SortArrowDownIcon, SortArrowUpIcon } from '../icons'; + +const opposite = (direction) => + direction === DIRECTION.ASC ? DIRECTION.DESC : DIRECTION.ASC; + +const ariaSort = (direction) => + direction === DIRECTION.ASC ? 'ascending' : 'descending'; + +const TableHeaderTitle = styled.span` + border-bottom: 2px dashed transparent; + padding: ${remSize(3)} 0; + color: ${prop('inactiveTextColor')}; + ${(props) => props.selected && `border-color: ${prop('accentColor')(props)}`} +`; + +export const StyledHeaderCell = styled.th` + height: ${remSize(32)}; + position: sticky; + top: 0; + z-index: 1; + background-color: ${prop('backgroundColor')}; + font-weight: normal; + &:nth-child(1) { + padding-left: ${remSize(12)}; + } + button { + display: flex; + align-items: center; + height: ${remSize(35)}; + svg { + margin-left: ${remSize(8)}; + fill: ${prop('inactiveTextColor')}; + } + } +`; + +const TableHeaderCell = ({ sorting, field, title, defaultOrder, onSort }) => { + const isSelected = sorting.field === field; + const { direction } = sorting; + const { t } = useTranslation(); + const directionWhenClicked = isSelected ? opposite(direction) : defaultOrder; + // TODO: more generic translation properties + const translationKey = + directionWhenClicked === DIRECTION.ASC + ? 'SketchList.ButtonLabelAscendingARIA' + : 'SketchList.ButtonLabelDescendingARIA'; + const buttonLabel = t(translationKey, { + displayName: title + }); + + return ( + + + + ); +}; + +TableHeaderCell.propTypes = { + sorting: PropTypes.shape({ + field: PropTypes.string.isRequired, + direction: PropTypes.string.isRequired + }).isRequired, + field: PropTypes.string.isRequired, + title: PropTypes.string, + defaultOrder: PropTypes.oneOf([DIRECTION.ASC, DIRECTION.DESC]), + onSort: PropTypes.func.isRequired +}; + +TableHeaderCell.defaultProps = { + title: '', + defaultOrder: DIRECTION.ASC +}; + +export default TableHeaderCell; diff --git a/client/common/Table/TableHeaderCell.test.jsx b/client/common/Table/TableHeaderCell.test.jsx new file mode 100644 index 0000000000..b6bf0bda33 --- /dev/null +++ b/client/common/Table/TableHeaderCell.test.jsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { DIRECTION } from '../../modules/IDE/actions/sorting'; +import { render, fireEvent, screen } from '../../test-utils'; +import TableHeaderCell from './TableHeaderCell'; + +describe('', () => { + const onSort = jest.fn(); + + const table = document.createElement('table'); + const tr = document.createElement('tr'); + table.appendChild(tr); + document.body.appendChild(table); + + const subject = (sorting, defaultOrder = DIRECTION.ASC) => + render( + , + { container: tr } + ); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('indicates the active sort order', () => { + it('shows an up arrow when active ascending', () => { + subject({ field: 'name', direction: DIRECTION.ASC }); + expect(screen.getByRole('columnheader')).toHaveAttribute( + 'aria-sort', + 'ascending' + ); + expect(screen.getByLabelText('Ascending')).toBeVisible(); + expect(screen.queryByLabelText('Descending')).not.toBeInTheDocument(); + }); + + it('shows a down arrow when active descending', () => { + subject({ field: 'name', direction: DIRECTION.DESC }); + expect(screen.getByRole('columnheader')).toHaveAttribute( + 'aria-sort', + 'descending' + ); + expect(screen.queryByLabelText('Ascending')).not.toBeInTheDocument(); + expect(screen.getByLabelText('Descending')).toBeVisible(); + }); + + it('has no icon when inactive', () => { + subject({ field: 'other', direction: DIRECTION.ASC }); + expect(screen.getByRole('columnheader')).not.toHaveAttribute('aria-sort'); + expect(screen.queryByLabelText('Ascending')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Descending')).not.toBeInTheDocument(); + }); + }); + + describe('calls onSort when clicked', () => { + const checkSort = (expectedDirection) => { + fireEvent.click(screen.getByText('Name')); + + expect(onSort).toHaveBeenCalledWith({ + field: 'name', + direction: expectedDirection + }); + }; + + it('uses defaultOrder when inactive, ascending', () => { + subject({ field: 'other', direction: DIRECTION.ASC }, DIRECTION.ASC); + checkSort(DIRECTION.ASC); + }); + + it('uses defaultOrder when inactive, descending', () => { + subject({ field: 'other', direction: DIRECTION.ASC }, DIRECTION.DESC); + checkSort(DIRECTION.DESC); + }); + + it('calls with DESC if currently sorted ASC', () => { + subject({ field: 'name', direction: DIRECTION.ASC }); + checkSort(DIRECTION.DESC); + }); + + it('calls with ASC if currently sorted DESC', () => { + subject({ field: 'name', direction: DIRECTION.DESC }); + checkSort(DIRECTION.ASC); + }); + }); +}); diff --git a/client/components/Dropdown/DropdownMenu.jsx b/client/components/Dropdown/DropdownMenu.jsx index da41b30101..8761cd606b 100644 --- a/client/components/Dropdown/DropdownMenu.jsx +++ b/client/components/Dropdown/DropdownMenu.jsx @@ -1,11 +1,17 @@ import PropTypes from 'prop-types'; import React, { forwardRef, useCallback, useRef, useState } from 'react'; +import styled from 'styled-components'; import useModalClose from '../../common/useModalClose'; import DownArrowIcon from '../../images/down-filled-triangle.svg'; import { DropdownWrapper } from '../Dropdown'; // TODO: enable arrow keys to navigate options from list +const Container = styled.div` + position: relative; + width: fit-content; +`; + const DropdownMenu = forwardRef( ( { children, anchor, 'aria-label': ariaLabel, align, className, classes }, @@ -38,7 +44,7 @@ const DropdownMenu = forwardRef( }; return ( -
+
+ ); } ); diff --git a/client/components/Dropdown/TableDropdown.jsx b/client/components/Dropdown/TableDropdown.jsx index d4db78f963..cbc8e3ccff 100644 --- a/client/components/Dropdown/TableDropdown.jsx +++ b/client/components/Dropdown/TableDropdown.jsx @@ -41,7 +41,6 @@ const TableDropdown = styled(DropdownMenu).attrs({ } & ul { top: 63%; - right: calc(100% - 26px); } `; diff --git a/client/modules/IDE/components/AddToCollectionList.jsx b/client/modules/IDE/components/AddToCollectionList.jsx index ecb020ce6f..b05cadaefd 100644 --- a/client/modules/IDE/components/AddToCollectionList.jsx +++ b/client/modules/IDE/components/AddToCollectionList.jsx @@ -10,7 +10,7 @@ import { getCollections, removeFromCollection } from '../actions/collections'; -import getSortedCollections from '../selectors/collections'; +import getFilteredCollections from '../selectors/collections'; import QuickAddList from './QuickAddList'; import { remSize } from '../../../theme'; @@ -34,7 +34,7 @@ const AddToCollectionList = ({ projectId }) => { const username = useSelector((state) => state.user.username); - const collections = useSelector(getSortedCollections); + const collections = useSelector(getFilteredCollections); // TODO: improve loading state const loading = useSelector((state) => state.loading); diff --git a/client/modules/IDE/components/AddToCollectionSketchList.jsx b/client/modules/IDE/components/AddToCollectionSketchList.jsx index eb70c2ed71..3b575dfe72 100644 --- a/client/modules/IDE/components/AddToCollectionSketchList.jsx +++ b/client/modules/IDE/components/AddToCollectionSketchList.jsx @@ -8,8 +8,7 @@ import { withTranslation } from 'react-i18next'; 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 getSortedSketches from '../selectors/projects'; +import getFilteredSketches from '../selectors/projects'; import Loader from '../../App/components/loader'; import QuickAddList from './QuickAddList'; import { @@ -124,10 +123,6 @@ SketchList.propTypes = { }).isRequired, username: PropTypes.string, loading: PropTypes.bool.isRequired, - sorting: PropTypes.shape({ - field: PropTypes.string.isRequired, - direction: PropTypes.string.isRequired - }).isRequired, addToCollection: PropTypes.func.isRequired, removeFromCollection: PropTypes.func.isRequired, t: PropTypes.func.isRequired @@ -140,8 +135,7 @@ SketchList.defaultProps = { function mapStateToProps(state) { return { user: state.user, - sketches: getSortedSketches(state), - sorting: state.sorting, + sketches: getFilteredSketches(state), loading: state.loading, project: state.project }; @@ -149,13 +143,7 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return bindActionCreators( - Object.assign( - {}, - ProjectsActions, - CollectionsActions, - ToastActions, - SortingActions - ), + Object.assign({}, ProjectsActions, CollectionsActions, ToastActions), dispatch ); } diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index 720f21734b..ddfe117c74 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -1,17 +1,15 @@ +import prettyBytes from 'pretty-bytes'; 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, useMemo } from 'react'; import { Helmet } from 'react-helmet'; -import prettyBytes from 'pretty-bytes'; -import { useTranslation, withTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; +import { connect, useDispatch, useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; import MenuItem from '../../../components/Dropdown/MenuItem'; import TableDropdown from '../../../components/Dropdown/TableDropdown'; - -import Loader from '../../App/components/loader'; -import { deleteAssetRequest } from '../actions/assets'; -import * as AssetActions from '../actions/assets'; +import { deleteAssetRequest, getAssets } from '../actions/assets'; +import { DIRECTION } from '../actions/sorting'; +import ConnectedTableBase from './ConnectedTableBase'; const AssetMenu = ({ item: asset }) => { const { t } = useTranslation(); @@ -82,106 +80,63 @@ function mapStateToPropsAssetListRow(state) { }; } -function mapDispatchToPropsAssetListRow(dispatch) { - return bindActionCreators(AssetActions, dispatch); -} +const AssetListRow = connect(mapStateToPropsAssetListRow)(AssetListRowBase); -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'); - } - - hasAssets() { - return !this.props.loading && this.props.assetList.length > 0; - } - - renderLoader() { - if (this.props.loading) return ; - return null; - } - - renderEmptyTable() { - if (!this.props.loading && this.props.assetList.length === 0) { - return ( -

- {this.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 = () => { + const { t } = useTranslation(); -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 -}; + const dispatch = useDispatch(); -function mapStateToProps(state) { - return { - user: state.user, - assetList: state.assets.list, - loading: state.loading - }; -} + useEffect(() => { + dispatch(getAssets()); + }, []); -function mapDispatchToProps(dispatch) { - return bindActionCreators(Object.assign({}, AssetActions), dispatch); -} + const isLoading = useSelector((state) => state.loading); -export default withTranslation()( - connect(mapStateToProps, mapDispatchToProps)(AssetList) -); + const assetList = useSelector((state) => state.assets.list); + + const items = useMemo( + // This is a hack to use the order from the API as the initial sort + () => assetList?.map((asset, i) => ({ ...asset, index: i, id: asset.key })), + [assetList] + ); + + return ( +
+ + {t('AssetList.Title')} + + } + /> +
+ ); +}; + +export default AssetList; diff --git a/client/modules/IDE/components/CollectionList/CollectionList.jsx b/client/modules/IDE/components/CollectionList/CollectionList.jsx index e3c910881e..e9584171a3 100644 --- a/client/modules/IDE/components/CollectionList/CollectionList.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionList.jsx @@ -1,314 +1,130 @@ +import find from 'lodash'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet'; -import { withTranslation } from 'react-i18next'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import classNames from 'classnames'; -import { find } from 'lodash'; -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 getSortedCollections from '../../selectors/collections'; -import Loader from '../../../App/components/loader'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; import Overlay from '../../../App/components/Overlay'; +import { getCollections } from '../../actions/collections'; +import { DIRECTION } from '../../actions/sorting'; +import getFilteredCollections from '../../selectors/collections'; +import { selectCurrentUsername } from '../../selectors/users'; import AddToCollectionSketchList from '../AddToCollectionSketchList'; +import ConnectedTableBase from '../ConnectedTableBase'; import { SketchSearchbar } from '../Searchbar'; 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); - - if (props.projectId) { - props.getProject(props.projectId); - } - - 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 - }); - } - } - - 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 - }); - } - - showAddSketches = (collectionId) => { - this.setState({ - addingSketchesToCollectionId: collectionId - }); - }; - - hideAddSketches = () => { - this.setState({ - addingSketchesToCollectionId: null - }); - }; - - hasCollections() { - return ( - (!this.props.loading || this.state.hasLoadedData) && - this.props.collections.length > 0 - ); - } - - _renderLoader() { - if (this.props.loading && !this.state.hasLoadedData) return ; - return null; - } - - _renderEmptyTable() { - if (!this.props.loading && this.props.collections.length === 0) { - return ( -

- {this.props.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', { - 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; - }; - - _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 ( - - - - ); - }; - - render() { - const username = - this.props.username !== undefined - ? this.props.username - : this.props.user.username; - const { mobile } = this.props; - - return ( -
- - {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 - > - { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const collections = useSelector(getFilteredCollections); + + // TODO: combine with AddToCollectionList + const loading = useSelector((state) => state.loading); + const [hasLoadedData, setHasLoadedData] = useState(false); + const showLoader = loading && !hasLoadedData; + + useEffect(() => { + dispatch(getCollections(username)).then(() => setHasLoadedData(true)); + }, [dispatch, username]); + + const currentUser = useSelector(selectCurrentUsername); + const userIsOwner = username === currentUser; + + const [ + addingSketchesToCollectionId, + setAddingSketchesToCollectionId + ] = useState(null); + + return ( +
+ + + {userIsOwner + ? t('CollectionList.Title') + : t('CollectionList.AnothersTitle', { + anotheruser: username })} - /> - </Overlay> + + + + ( + setAddingSketchesToCollectionId(collection.id)} + /> )} -
- ); - } -} + /> + {addingSketchesToCollectionId && ( + } + closeOverlay={() => setAddingSketchesToCollectionId(null)} + isFixedHeight + > + + + )} +
+ ); +}; CollectionList.propTypes = { - user: PropTypes.shape({ - username: PropTypes.string, - authenticated: PropTypes.bool.isRequired - }).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, + username: PropTypes.string.isRequired, mobile: PropTypes.bool }; CollectionList.defaultProps = { - projectId: undefined, - project: { - id: undefined, - owner: undefined - }, - username: undefined, mobile: false }; -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, - ToastActions, - SortingActions - ), - dispatch - ); -} - -export default withTranslation()( - connect(mapStateToProps, mapDispatchToProps)(CollectionList) -); +export default CollectionList; diff --git a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx index dafbe21517..afeecaf2aa 100644 --- a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx @@ -1,192 +1,189 @@ -import PropTypes from 'prop-types'; -import React, { useState, useRef } from 'react'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; -import { bindActionCreators } from 'redux'; -import { withTranslation } from 'react-i18next'; -import MenuItem from '../../../../components/Dropdown/MenuItem'; -import TableDropdown from '../../../../components/Dropdown/TableDropdown'; -import * as ProjectActions from '../../actions/project'; -import * as CollectionsActions from '../../actions/collections'; -import * as IdeActions from '../../actions/ide'; -import * as ToastActions from '../../actions/toast'; -import dates from '../../../../utils/formatDate'; - -const formatDateCell = (date, mobile = false) => - dates.format(date, { showTime: !mobile }); - -const CollectionListRowBase = (props) => { - const [renameOpen, setRenameOpen] = useState(false); - const [renameValue, setRenameValue] = useState(''); - const renameInput = useRef(null); - - const closeAll = () => { - setRenameOpen(false); - }; - - const updateName = () => { - const isValid = renameValue.trim().length !== 0; - if (isValid) { - props.editCollection(props.collection.id, { - name: renameValue.trim() - }); - } - }; - - const handleAddSketches = () => { - closeAll(); - props.onAddSketches(); - }; - - const handleCollectionDelete = () => { - closeAll(); - if ( - window.confirm( - props.t('Common.DeleteConfirmation', { - name: props.collection.name - }) - ) - ) { - props.deleteCollection(props.collection.id); - } - }; - - const handleRenameOpen = () => { - closeAll(); - setRenameOpen(true); - setRenameValue(props.collection.name); - if (renameInput.current) { - renameInput.current.focus(); - } - }; - - const handleRenameChange = (e) => { - setRenameValue(e.target.value); - }; - - const handleRenameEnter = (e) => { - if (e.key === 'Enter') { - updateName(); - closeAll(); - } - }; - - const handleRenameBlur = () => { - updateName(); - closeAll(); - }; - - const renderActions = () => { - const userIsOwner = props.user.username === props.username; - - return ( - - - {props.t('CollectionListRow.AddSketch')} - - - {props.t('CollectionListRow.Delete')} - - - {props.t('CollectionListRow.Rename')} - - - ); - }; - - const renderCollectionName = () => { - const { collection, username } = props; - - return ( - <> - - {renameOpen ? '' : collection.name} - - {renameOpen && ( - e.stopPropagation()} - ref={renameInput} - /> - )} - - ); - }; - - const { collection, mobile } = props; - - return ( - - - {renderCollectionName()} - - {formatDateCell(collection.createdAt, mobile)} - {formatDateCell(collection.updatedAt, mobile)} - - {mobile && 'sketches: '} - {(collection.items || []).length} - - {renderActions()} - - ); -}; - -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, - 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 -}; - -CollectionListRowBase.defaultProps = { - mobile: false -}; - -function mapDispatchToPropsSketchListRow(dispatch) { - return bindActionCreators( - Object.assign( - {}, - CollectionsActions, - ProjectActions, - IdeActions, - ToastActions - ), - dispatch - ); -} - -export default withTranslation()( - connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase) -); +import PropTypes from 'prop-types'; +import React, { useState, useRef } from 'react'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { bindActionCreators } from 'redux'; +import { withTranslation } from 'react-i18next'; +import MenuItem from '../../../../components/Dropdown/MenuItem'; +import TableDropdown from '../../../../components/Dropdown/TableDropdown'; +import * as ProjectActions from '../../actions/project'; +import * as CollectionsActions from '../../actions/collections'; +import * as IdeActions from '../../actions/ide'; +import * as ToastActions from '../../actions/toast'; +import dates from '../../../../utils/formatDate'; + +const formatDateCell = (date, mobile = false) => + dates.format(date, { showTime: !mobile }); + +const CollectionListRowBase = (props) => { + const [renameOpen, setRenameOpen] = useState(false); + const [renameValue, setRenameValue] = useState(''); + const renameInput = useRef(null); + + const closeAll = () => { + setRenameOpen(false); + }; + + const updateName = () => { + const isValid = renameValue.trim().length !== 0; + if (isValid) { + props.editCollection(props.collection.id, { + name: renameValue.trim() + }); + } + }; + + const handleAddSketches = () => { + closeAll(); + props.onAddSketches(); + }; + + const handleCollectionDelete = () => { + closeAll(); + if ( + window.confirm( + props.t('Common.DeleteConfirmation', { + name: props.collection.name + }) + ) + ) { + props.deleteCollection(props.collection.id); + } + }; + + const handleRenameOpen = () => { + closeAll(); + setRenameOpen(true); + setRenameValue(props.collection.name); + if (renameInput.current) { + renameInput.current.focus(); + } + }; + + const handleRenameChange = (e) => { + setRenameValue(e.target.value); + }; + + const handleRenameEnter = (e) => { + if (e.key === 'Enter') { + updateName(); + closeAll(); + } + }; + + const handleRenameBlur = () => { + updateName(); + closeAll(); + }; + + const renderActions = () => { + const { userIsOwner } = props; + + return ( + + + {props.t('CollectionListRow.AddSketch')} + + + {props.t('CollectionListRow.Delete')} + + + {props.t('CollectionListRow.Rename')} + + + ); + }; + + const renderCollectionName = () => { + const { collection, username } = props; + + return ( + <> + + {renameOpen ? '' : collection.name} + + {renameOpen && ( + e.stopPropagation()} + ref={renameInput} + /> + )} + + ); + }; + + const { collection, mobile } = props; + + return ( + + + {renderCollectionName()} + + {formatDateCell(collection.createdAt, mobile)} + {formatDateCell(collection.updatedAt, mobile)} + + {mobile && 'sketches: '} + {(collection.items || []).length} + + {renderActions()} + + ); +}; + +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, + userIsOwner: PropTypes.bool.isRequired, + deleteCollection: PropTypes.func.isRequired, + editCollection: PropTypes.func.isRequired, + onAddSketches: PropTypes.func.isRequired, + mobile: PropTypes.bool, + t: PropTypes.func.isRequired +}; + +CollectionListRowBase.defaultProps = { + mobile: false +}; + +function mapDispatchToPropsSketchListRow(dispatch) { + return bindActionCreators( + Object.assign( + {}, + CollectionsActions, + ProjectActions, + IdeActions, + ToastActions + ), + dispatch + ); +} + +export default withTranslation()( + connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase) +); diff --git a/client/modules/IDE/components/ConnectedTableBase.jsx b/client/modules/IDE/components/ConnectedTableBase.jsx new file mode 100644 index 0000000000..c24182da40 --- /dev/null +++ b/client/modules/IDE/components/ConnectedTableBase.jsx @@ -0,0 +1,43 @@ +import { omit } from 'lodash'; +import React, { useCallback, useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import TableBase from '../../../common/Table/TableBase'; +import { DIRECTION, setSorting } from '../actions/sorting'; + +/** + * Connects the `TableBase` UI component with Redux. + * Resets the sorting state on mount based on the `initialSort` prop. + * Changes the sorting state when clicking on headers. + */ +const ConnectedTableBase = ({ initialSort, ...props }) => { + const sorting = useSelector((state) => state.sorting); + + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(setSorting(initialSort.field, initialSort.direction)); + }, [initialSort.field, initialSort.direction, dispatch]); + + const handleSort = useCallback( + (sort) => { + dispatch(setSorting(sort.field, sort.direction)); + }, + [dispatch] + ); + + return ; +}; + +ConnectedTableBase.propTypes = { + ...omit(TableBase.propTypes, 'sortBy', 'onChangeSort'), + initialSort: TableBase.propTypes.sortBy +}; + +ConnectedTableBase.defaultProps = { + initialSort: { + field: 'createdAt', + direction: DIRECTION.DESC + } +}; + +export default ConnectedTableBase; diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index 2237227766..9c01789422 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -1,29 +1,25 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet'; -import { withTranslation } from 'react-i18next'; -import { connect } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { connect, useDispatch, useSelector } 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 getSortedSketches from '../selectors/projects'; -import Loader from '../../App/components/loader'; import Overlay from '../../App/components/Overlay'; +import * as IdeActions from '../actions/ide'; +import * as ProjectActions from '../actions/project'; +import { getProjects } from '../actions/projects'; +import { DIRECTION } from '../actions/sorting'; +import getFilteredSketches from '../selectors/projects'; +import { selectCurrentUsername } from '../selectors/users'; import AddToCollectionList from './AddToCollectionList'; import getConfig from '../../../utils/getConfig'; - -import ArrowUpIcon from '../../../images/sort-arrow-up.svg'; -import ArrowDownIcon from '../../../images/sort-arrow-down.svg'; +import ConnectedTableBase from './ConnectedTableBase'; const ROOT_URL = getConfig('API_URL'); @@ -224,6 +220,12 @@ SketchListRowBase.defaultProps = { mobile: false }; +function mapStateToPropsSketchListRow(state) { + return { + user: state.user + }; +} + function mapDispatchToPropsSketchListRow(dispatch) { return bindActionCreators( Object.assign({}, ProjectActions, IdeActions), @@ -232,255 +234,108 @@ function mapDispatchToPropsSketchListRow(dispatch) { } const SketchListRow = connect( - null, + mapStateToPropsSketchListRow, mapDispatchToPropsSketchListRow )(SketchListRowBase); -class SketchList extends React.Component { - constructor(props) { - super(props); - this.props.getProjects(this.props.username); - this.props.resetSorting(); - - this.state = { - isInitialDataLoad: true - }; - } - - 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; - } - - _renderLoader() { - if (this.isLoading()) return ; - return null; - } - - _renderEmptyTable() { - if (!this.isLoading() && this.props.sketches.length === 0) { - return ( -

- {this.props.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 ( - - - - ); - }; - - 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 }) - } - > - - +const SketchList = ({ username, mobile }) => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const currentUser = useSelector(selectCurrentUsername); + + const sketches = useSelector(getFilteredSketches); + + // TODO: combine with AddToCollectionSketchList + const loading = useSelector((state) => state.loading); + const [hasLoadedData, setHasLoadedData] = useState(false); + const showLoader = loading && !hasLoadedData; + + useEffect(() => { + dispatch(getProjects(username)).then(() => setHasLoadedData(true)); + }, [dispatch, username]); + + const [sketchToAddToCollection, setSketchToAddToCollection] = useState(null); + + return ( +
+ + + {username === currentUser + ? t('SketchList.Title') + : t('SketchList.AnothersTitle', { + anotheruser: username + })} + + + formatDateCell(value, mobile) + }, + { + field: 'updatedAt', + defaultOrder: DIRECTION.DESC, + title: t('SketchList.HeaderUpdatedAt', { + context: mobile ? 'mobile' : '' + }), + formatValue: (value) => formatDateCell(value, mobile) + } + ]} + addDropdownColumn + initialSort={{ + field: 'createdAt', + direction: DIRECTION.DESC + }} + emptyMessage={t('SketchList.NoSketches')} + caption={t('SketchList.TableSummary')} + renderRow={(sketch) => ( + { + setSketchToAddToCollection(sketch); + }} + t={t} + /> )} -
- ); - } -} + /> + {sketchToAddToCollection && ( + { + setSketchToAddToCollection(null); + }} + > + + + )} +
+ ); +}; SketchList.propTypes = { - user: PropTypes.shape({ - username: PropTypes.string, - authenticated: PropTypes.bool.isRequired - }).isRequired, - getProjects: PropTypes.func.isRequired, - sketches: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - 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, - mobile: PropTypes.bool, - t: PropTypes.func.isRequired + username: PropTypes.string.isRequired, + mobile: PropTypes.bool }; SketchList.defaultProps = { - username: undefined, mobile: false }; -function mapStateToProps(state) { - return { - user: state.user, - sketches: getSortedSketches(state), - sorting: state.sorting, - loading: state.loading, - project: state.project - }; -} - -function mapDispatchToProps(dispatch) { - return bindActionCreators( - Object.assign( - {}, - ProjectsActions, - CollectionsActions, - ToastActions, - SortingActions - ), - dispatch - ); -} - -export default withTranslation()( - connect(mapStateToProps, mapDispatchToProps)(SketchList) -); +export default SketchList; diff --git a/client/modules/IDE/components/SketchList.unit.test.jsx b/client/modules/IDE/components/SketchList.unit.test.jsx index 162d12bcc1..b72dcaeb3f 100644 --- a/client/modules/IDE/components/SketchList.unit.test.jsx +++ b/client/modules/IDE/components/SketchList.unit.test.jsx @@ -44,17 +44,32 @@ describe('', () => { expect(screen.getByText('testsketch2')).toBeInTheDocument(); }); - it('clicking on date created row header dispatches a reordering action', () => { + it('clicking on date created row header sorts the table', () => { act(() => { subject(); }); + expect.assertions(6); + + const rowsBefore = screen.getAllByRole('row'); + expect(within(rowsBefore[1]).getByText('testsketch1')).toBeInTheDocument(); + expect(within(rowsBefore[2]).getByText('testsketch2')).toBeInTheDocument(); + + expect( + screen.getByLabelText(/Sort by Date Created ascending/i) + ).toBeInTheDocument(); + act(() => { fireEvent.click(screen.getByText(/date created/i)); }); - const expectedAction = [{ type: 'TOGGLE_DIRECTION', field: 'createdAt' }]; - expect(store.getActions()).toEqual(expect.arrayContaining(expectedAction)); + expect( + screen.getByLabelText(/Sort by Date Created descending/i) + ).toBeInTheDocument(); + + const rowsAfter = screen.getAllByRole('row'); + expect(within(rowsAfter[1]).getByText('testsketch2')).toBeInTheDocument(); + expect(within(rowsAfter[2]).getByText('testsketch1')).toBeInTheDocument(); }); it('clicking on dropdown arrow opens sketch options - sketches belong to user', () => { diff --git a/client/modules/IDE/components/__snapshots__/SketchList.unit.test.jsx.snap b/client/modules/IDE/components/__snapshots__/SketchList.unit.test.jsx.snap index 051c95be19..50321dbc04 100644 --- a/client/modules/IDE/components/__snapshots__/SketchList.unit.test.jsx.snap +++ b/client/modules/IDE/components/__snapshots__/SketchList.unit.test.jsx.snap @@ -2,27 +2,71 @@ exports[` snapshot testing 1`] = ` - .c0 > button { + .c3 > button { width: 2.0833333333333335rem; height: 2.0833333333333335rem; padding: 0; } -.c0 > button svg { +.c3 > button svg { max-width: 100%; max-height: 100%; } -.c0 > button polygon, -.c0 > button path { +.c3 > button polygon, +.c3 > button path { fill: #666; } -.c0 ul { +.c3 ul { top: 63%; right: calc(100% - 26px); } +.c1 { + border-bottom: 2px dashed transparent; + padding: 0.25rem 0; + color: #666; +} + +.c2 { + border-bottom: 2px dashed transparent; + padding: 0.25rem 0; + color: #666; + border-color: #ed225d; +} + +.c0 { + height: 2.6666666666666665rem; + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1; + background-color: #FBFBFB; + font-weight: normal; +} + +.c0:nth-child(1) { + padding-left: 1rem; +} + +.c0 button { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + height: 2.9166666666666665rem; +} + +.c0 button svg { + margin-left: 0.6666666666666666rem; + fill: #666; +} +
@@ -33,53 +77,59 @@ exports[` snapshot testing 1`] = ` @@ -107,7 +157,7 @@ exports[` snapshot testing 1`] = ` class="sketch-list__dropdown-column" >
- - ); - } - render() { const isOwner = this.isOwner(); + // Need top-level string fields in order to sort. + const items = this.props.collection?.items?.map((item) => ({ + ...item, + // 'zz' is a dumb hack to put deleted items last in the sort order + name: item.isDeleted ? 'zz' : item.project?.name, + owner: item.isDeleted ? 'zz' : item.project?.user?.username + })); + return (
- {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) => ( - - ))} - -
- )} + ( + + )} + />
@@ -324,12 +254,6 @@ Collection.propTypes = { }), 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 }; @@ -342,25 +266,10 @@ function mapStateToProps(state, ownProps) { return { user: state.user, collection: getCollection(state, ownProps.collectionId), - sorting: state.sorting, - loading: state.loading, - project: state.project + loading: state.loading }; } -function mapDispatchToProps(dispatch) { - return bindActionCreators( - Object.assign( - {}, - CollectionsActions, - ProjectsActions, - ToastActions, - SortingActions - ), - dispatch - ); -} - export default withTranslation()( - connect(mapStateToProps, mapDispatchToProps)(Collection) + connect(mapStateToProps, CollectionsActions)(Collection) ); diff --git a/client/styles/components/_asset-list.scss b/client/styles/components/_asset-list.scss index 7d8f6065e8..531e3944fe 100644 --- a/client/styles/components/_asset-list.scss +++ b/client/styles/components/_asset-list.scss @@ -5,28 +5,7 @@ } .asset-table { - width: 100%; - - max-height: 100%; - border-spacing: 0; position: relative; - & .asset-table__dropdown-column { - width: #{60 / $base-font-size}rem; - position: relative; - } -} - -.asset-table thead th { - height: #{32 / $base-font-size}rem; - position: sticky; - top: 0; - @include themify() { - background-color: getThemifyVariable('background-color'); - } -} - -.asset-table thead th:nth-child(1){ - padding-left: #{12 / $base-font-size}rem; } .asset-table__row { @@ -51,23 +30,6 @@ } } -.asset-table thead { - font-size: #{12 / $base-font-size}rem; - @include themify() { - color: getThemifyVariable('inactive-text-color') - } -} - -.asset-table th { - font-weight: normal; -} - -.asset-table__empty { - text-align: center; - font-size: #{16 / $base-font-size}rem; - padding: #{42 / $base-font-size}rem 0; -} - .asset-table__total { padding: 0 #{20 / $base-font-size}rem; position: sticky; diff --git a/client/styles/components/_collection.scss b/client/styles/components/_collection.scss index a616154ba5..5f7bf10536 100644 --- a/client/styles/components/_collection.scss +++ b/client/styles/components/_collection.scss @@ -134,11 +134,6 @@ align-items: center; } -.collection-empty-message { - text-align: center; - font-size: #{16 / $base-font-size}rem; -} - .collection-row__action-column { width: #{60 / $base-font-size}rem; position: relative; diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index c577552aea..1d64fd5acf 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -79,50 +79,6 @@ } } -.sketches-table thead th { - height: #{32 / $base-font-size}rem; - position: sticky; - top: 0; - z-index: 1; - @include themify() { - background-color: getThemifyVariable("background-color"); - } -} - -.sketch-list__sort-button { - display: flex; - align-items: center; - height: #{35 / $base-font-size}rem; - - & .isvg { - margin-left: #{8 / $base-font-size}rem; - } - - & svg { - @include themify() { - fill: getThemifyVariable("inactive-text-color"); - } - } -} - -.sketches-table__header { - border-bottom: 2px dashed transparent; - padding: #{3 / $base-font-size}rem 0; - @include themify() { - color: getThemifyVariable("inactive-text-color"); - } -} - -.sketches-table__header--selected { - @include themify() { - border-color: getThemifyVariable("logo-color"); - } -} - -.sketches-table thead th:nth-child(1) { - padding-left: #{12 / $base-font-size}rem; -} - .sketches-table__row { margin: #{10 / $base-font-size}rem; height: #{72 / $base-font-size}rem; @@ -172,9 +128,3 @@ .sketches-table__icon-cell { width: #{35 / $base-font-size}rem; } - -.sketches-table__empty { - text-align: center; - font-size: #{16 / $base-font-size}rem; - padding: #{42 / $base-font-size}rem 0; -} diff --git a/client/theme.js b/client/theme.js index f60ef0735a..c0cc457e25 100644 --- a/client/theme.js +++ b/client/theme.js @@ -248,6 +248,7 @@ export default { ...baseThemes, [Theme.contrast]: extend(baseThemes[Theme.dark], { inactiveTextColor: grays.light, + accentColor: colors.yellow, Button: { primary: {