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() && (
-
-
-
- {t('AssetList.HeaderName')} |
- {t('AssetList.HeaderSize')} |
- {t('AssetList.HeaderSketch')} |
- |
-
-
-
- {assetList.map((asset) => (
-
- ))}
-
-
- )}
-
- );
- }
-}
+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
})}
- />
-
+
+
+
+ (
+ 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 (
-
-
-
-
-
- );
- };
-
- 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 (
+
+
+
+
+
+ );
+ };
+
+ 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"
>
snapshot testing 1`] = `
class="sketch-list__dropdown-column"
>