Unverified Commit 821f393c authored by Tamika Tannis's avatar Tamika Tannis Committed by GitHub

Split ResourceList Components (#445)

* Fix initial bug

* Separate ResourceList behaviors into dedicated components

* Update tests for parent usages

* Update ResourceList tests

* Finish updating tests

* Update bug fix logic

* Some code cleanup
parent fcf7ed0f
...@@ -10,6 +10,7 @@ import LoadingSpinner from 'components/common/LoadingSpinner'; ...@@ -10,6 +10,7 @@ import LoadingSpinner from 'components/common/LoadingSpinner';
import Breadcrumb from 'components/common/Breadcrumb'; import Breadcrumb from 'components/common/Breadcrumb';
import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon'; import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon';
import Flag from 'components/common/Flag'; import Flag from 'components/common/Flag';
import ResourceList from 'components/common/ResourceList';
import TabsComponent from 'components/common/TabsComponent'; import TabsComponent from 'components/common/TabsComponent';
import ChartList from './ChartList'; import ChartList from './ChartList';
import ImagePreview from './ImagePreview'; import ImagePreview from './ImagePreview';
...@@ -187,6 +188,13 @@ describe('DashboardPage', () => { ...@@ -187,6 +188,13 @@ describe('DashboardPage', () => {
describe('renderTabs', () => { describe('renderTabs', () => {
const { props, wrapper } = setup(); const { props, wrapper } = setup();
it('returns a ResourceList', () => {
const result = shallow(wrapper.instance().renderTabs());
const element = result.find(ResourceList);
expect(element.exists()).toBe(true);
expect(element.props().allItems).toEqual(props.dashboard.tables);
});
it('returns a Tabs component', () => { it('returns a Tabs component', () => {
const result = wrapper.instance().renderTabs(); const result = wrapper.instance().renderTabs();
expect(result.type).toEqual(TabsComponent); expect(result.type).toEqual(TabsComponent);
......
...@@ -99,7 +99,6 @@ export class DashboardPage extends React.Component<DashboardPageProps, Dashboard ...@@ -99,7 +99,6 @@ export class DashboardPage extends React.Component<DashboardPageProps, Dashboard
<ResourceList <ResourceList
allItems={ this.props.dashboard.tables } allItems={ this.props.dashboard.tables }
itemsPerPage={ TABLES_PER_PAGE } itemsPerPage={ TABLES_PER_PAGE }
paginate={ false }
source={ DASHBOARD_SOURCE } source={ DASHBOARD_SOURCE }
/> />
), ),
......
...@@ -9,7 +9,7 @@ import Breadcrumb from 'components/common/Breadcrumb'; ...@@ -9,7 +9,7 @@ import Breadcrumb from 'components/common/Breadcrumb';
import Flag from 'components/common/Flag'; import Flag from 'components/common/Flag';
import ResourceList from 'components/common/ResourceList'; import ResourceList from 'components/common/ResourceList';
import TabsComponent from 'components/common/TabsComponent'; import TabsComponent from 'components/common/TabsComponent';
import { mapDispatchToProps, mapStateToProps, ProfilePage, ProfilePageProps, RouteProps } from '.'; import { mapDispatchToProps, mapStateToProps, ProfilePage, ProfilePageProps, RouteProps } from './';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import { getMockRouterProps } from 'fixtures/mockRouter'; import { getMockRouterProps } from 'fixtures/mockRouter';
......
...@@ -100,33 +100,30 @@ export class ProfilePage extends React.Component<ProfilePageProps, ProfilePageSt ...@@ -100,33 +100,30 @@ export class ProfilePage extends React.Component<ProfilePageProps, ProfilePageSt
<> <>
<ResourceList <ResourceList
allItems={ own } allItems={ own }
emptyText={`${EMPTY_TEXT_PREFIX} ${OWNED_LABEL} ${resourceLabel}.`}
footerTextCollapsed={`${FOOTER_TEXT_PREFIX} ${own.length} ${OWNED_LABEL} ${resourceLabel}`}
itemsPerPage={ ITEMS_PER_PAGE } itemsPerPage={ ITEMS_PER_PAGE }
paginate={ false }
source={ OWNED_SOURCE } source={ OWNED_SOURCE }
title={`${OWNED_TITLE_PREFIX} (${own.length})`} title={`${OWNED_TITLE_PREFIX} (${own.length})`}
customFooterText={`${FOOTER_TEXT_PREFIX} ${own.length} ${OWNED_LABEL} ${resourceLabel}`}
customEmptyText={`${EMPTY_TEXT_PREFIX} ${OWNED_LABEL} ${resourceLabel}.`}
/> />
<ResourceList <ResourceList
allItems={ bookmarks } allItems={ bookmarks }
emptyText={`${EMPTY_TEXT_PREFIX} ${BOOKMARKED_LABEL} ${resourceLabel}.`}
footerTextCollapsed={`${FOOTER_TEXT_PREFIX} ${bookmarks.length} ${BOOKMARKED_LABEL} ${resourceLabel}`}
itemsPerPage={ ITEMS_PER_PAGE } itemsPerPage={ ITEMS_PER_PAGE }
paginate={ false }
source={ BOOKMARKED_SOURCE } source={ BOOKMARKED_SOURCE }
title={`${BOOKMARKED_TITLE_PREFIX} (${bookmarks.length})`} title={`${BOOKMARKED_TITLE_PREFIX} (${bookmarks.length})`}
customFooterText={`${FOOTER_TEXT_PREFIX} ${bookmarks.length} ${BOOKMARKED_LABEL} ${resourceLabel}`}
customEmptyText={`${EMPTY_TEXT_PREFIX} ${BOOKMARKED_LABEL} ${resourceLabel}.`}
/> />
{ {
/* Frequently Used currently not supported for dashboards */ /* Frequently Used currently not supported for dashboards */
resource === ResourceType.table && resource === ResourceType.table &&
<ResourceList <ResourceList
allItems={ read } allItems={ read }
emptyText={`${EMPTY_TEXT_PREFIX} ${READ_LABEL} ${resourceLabel}.`}
footerTextCollapsed={`${FOOTER_TEXT_PREFIX} ${read.length} ${READ_LABEL} ${resourceLabel}`}
itemsPerPage={ ITEMS_PER_PAGE } itemsPerPage={ ITEMS_PER_PAGE }
paginate={ false }
source={ READ_SOURCE } source={ READ_SOURCE }
title={`${READ_TITLE_PREFIX} (${read.length})`} title={`${READ_TITLE_PREFIX} (${read.length})`}
customFooterText={`${FOOTER_TEXT_PREFIX} ${read.length} ${READ_LABEL} ${resourceLabel}`}
customEmptyText={`${EMPTY_TEXT_PREFIX} ${READ_LABEL} ${resourceLabel}.`}
/> />
} }
</> </>
......
...@@ -5,7 +5,7 @@ import * as History from 'history'; ...@@ -5,7 +5,7 @@ import * as History from 'history';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
import { mapDispatchToProps, mapStateToProps, SearchPage, SearchPageProps } from '../'; import { mapDispatchToProps, mapStateToProps, SearchPage, SearchPageProps } from './';
import { import {
DOCUMENT_TITLE_SUFFIX, DOCUMENT_TITLE_SUFFIX,
PAGE_INDEX_ERROR_MESSAGE, PAGE_INDEX_ERROR_MESSAGE,
...@@ -17,13 +17,13 @@ import { ...@@ -17,13 +17,13 @@ import {
DASHBOARD_RESOURCE_TITLE, DASHBOARD_RESOURCE_TITLE,
TABLE_RESOURCE_TITLE, TABLE_RESOURCE_TITLE,
USER_RESOURCE_TITLE, USER_RESOURCE_TITLE,
} from '../constants'; } from './constants';
import LoadingSpinner from 'components/common/LoadingSpinner'; import LoadingSpinner from 'components/common/LoadingSpinner';
import ResourceSelector from 'components/SearchPage/ResourceSelector'; import ResourceSelector from 'components/SearchPage/ResourceSelector';
import SearchFilter from 'components/SearchPage/SearchFilter'; import SearchFilter from 'components/SearchPage/SearchFilter';
import SearchPanel from 'components/SearchPage/SearchPanel'; import SearchPanel from 'components/SearchPage/SearchPanel';
import ResourceList from 'components/common/ResourceList'; import PaginatedApiResourceList from 'components/common/ResourceList/PaginatedApiResourceList';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import { defaultEmptyFilters, datasetFilterExample } from 'fixtures/search/filters'; import { defaultEmptyFilters, datasetFilterExample } from 'fixtures/search/filters';
...@@ -194,7 +194,7 @@ describe('SearchPage', () => { ...@@ -194,7 +194,7 @@ describe('SearchPage', () => {
content = shallow(wrapper.instance().getTabContent(props.tables, ResourceType.table)); content = shallow(wrapper.instance().getTabContent(props.tables, ResourceType.table));
}); });
it('renders ResourceList with correct props', () => { it('renders PaginatedApiResourceList with correct props', () => {
const { props, wrapper } = setup(); const { props, wrapper } = setup();
const testResults = { const testResults = {
page_index: 0, page_index: 0,
...@@ -202,14 +202,13 @@ describe('SearchPage', () => { ...@@ -202,14 +202,13 @@ describe('SearchPage', () => {
total_results: 11, total_results: 11,
}; };
content = shallow(wrapper.instance().getTabContent(testResults, ResourceType.table)); content = shallow(wrapper.instance().getTabContent(testResults, ResourceType.table));
expect(content.children().find(PaginatedApiResourceList).props()).toMatchObject({
expect(content.children().find(ResourceList).props()).toMatchObject({
activePage: 0, activePage: 0,
slicedItems: testResults.results,
slicedItemsCount: testResults.total_results,
itemsPerPage: RESULTS_PER_PAGE, itemsPerPage: RESULTS_PER_PAGE,
onPagination: props.setPageIndex, onPagination: props.setPageIndex,
slicedItems: testResults.results,
source: SEARCH_SOURCE_NAME, source: SEARCH_SOURCE_NAME,
totalItemsCount: testResults.total_results,
}); });
}); });
}); });
......
...@@ -6,7 +6,7 @@ import { RouteComponentProps } from 'react-router'; ...@@ -6,7 +6,7 @@ import { RouteComponentProps } from 'react-router';
import { Search as UrlSearch } from 'history'; import { Search as UrlSearch } from 'history';
import LoadingSpinner from 'components/common/LoadingSpinner'; import LoadingSpinner from 'components/common/LoadingSpinner';
import ResourceList from 'components/common/ResourceList'; import PaginatedApiResourceList from 'components/common/ResourceList/PaginatedApiResourceList';
import ResourceSelector from './ResourceSelector'; import ResourceSelector from './ResourceSelector';
import SearchFilter from './SearchFilter'; import SearchFilter from './SearchFilter';
import SearchPanel from './SearchPanel'; import SearchPanel from './SearchPanel';
...@@ -140,13 +140,13 @@ export class SearchPage extends React.Component<SearchPageProps> { ...@@ -140,13 +140,13 @@ export class SearchPage extends React.Component<SearchPageProps> {
return ( return (
<div className="search-list-container"> <div className="search-list-container">
<ResourceList <PaginatedApiResourceList
slicedItems={ results.results }
slicedItemsCount={ total_results }
source={ SEARCH_SOURCE_NAME }
itemsPerPage={ RESULTS_PER_PAGE }
activePage={ page_index } activePage={ page_index }
onPagination={ this.props.setPageIndex } onPagination={ this.props.setPageIndex }
itemsPerPage={ RESULTS_PER_PAGE }
slicedItems={ results.results }
source={ SEARCH_SOURCE_NAME }
totalItemsCount={ total_results }
/> />
</div> </div>
); );
......
...@@ -5,15 +5,15 @@ import { mocked } from 'ts-jest/utils'; ...@@ -5,15 +5,15 @@ import { mocked } from 'ts-jest/utils';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
import { MyBookmarks, MyBookmarksProps, mapStateToProps } from '../'; import { MyBookmarks, MyBookmarksProps, mapStateToProps } from './';
import ResourceList from 'components/common/ResourceList'; import PaginatedResourceList from 'components/common/ResourceList/PaginatedResourceList';
import TabsComponent from 'components/common/TabsComponent'; import TabsComponent from 'components/common/TabsComponent';
import { import {
BOOKMARK_TITLE, BOOKMARK_TITLE,
BOOKMARKS_PER_PAGE, BOOKMARKS_PER_PAGE,
EMPTY_BOOKMARK_MESSAGE, EMPTY_BOOKMARK_MESSAGE,
MY_BOOKMARKS_SOURCE_NAME, MY_BOOKMARKS_SOURCE_NAME,
} from '../constants'; } from './constants';
jest.mock('config/config-utils', () => ({ jest.mock('config/config-utils', () => ({
getDisplayNameByResource: jest.fn(() => 'Resource'), getDisplayNameByResource: jest.fn(() => 'Resource'),
...@@ -105,12 +105,12 @@ describe('MyBookmarks', () => { ...@@ -105,12 +105,12 @@ describe('MyBookmarks', () => {
content = shallow(<div>{wrapper.instance().generateTabContent(givenResource)}</div>); content = shallow(<div>{wrapper.instance().generateTabContent(givenResource)}</div>);
}); });
it('returns a ResourceList with correct props', () => { it('returns a PaginatedResourceList with correct props', () => {
const element = content.find(ResourceList); const element = content.find(PaginatedResourceList);
expect(element.props().allItems).toBe(props.myBookmarks[givenResource]); expect(element.props().allItems).toBe(props.myBookmarks[givenResource]);
expect(element.props().itemsPerPage).toBe(BOOKMARKS_PER_PAGE); expect(element.props().itemsPerPage).toBe(BOOKMARKS_PER_PAGE);
expect(element.props().source).toBe(MY_BOOKMARKS_SOURCE_NAME); expect(element.props().source).toBe(MY_BOOKMARKS_SOURCE_NAME);
expect(element.props().customEmptyText).toBe(EMPTY_BOOKMARK_MESSAGE); expect(element.props().emptyText).toBe(EMPTY_BOOKMARK_MESSAGE);
}); });
it('returns null if there are no bookmarks to render', () => { it('returns null if there are no bookmarks to render', () => {
......
...@@ -11,7 +11,7 @@ import { ...@@ -11,7 +11,7 @@ import {
EMPTY_BOOKMARK_MESSAGE, EMPTY_BOOKMARK_MESSAGE,
MY_BOOKMARKS_SOURCE_NAME, MY_BOOKMARKS_SOURCE_NAME,
} from './constants'; } from './constants';
import ResourceList from 'components/common/ResourceList'; import PaginatedResourceList from 'components/common/ResourceList/PaginatedResourceList';
import TabsComponent from 'components/common/TabsComponent'; import TabsComponent from 'components/common/TabsComponent';
interface StateFromProps { interface StateFromProps {
...@@ -32,11 +32,11 @@ export class MyBookmarks extends React.Component<MyBookmarksProps> { ...@@ -32,11 +32,11 @@ export class MyBookmarks extends React.Component<MyBookmarksProps> {
return null; return null;
} }
return ( return (
<ResourceList <PaginatedResourceList
allItems={ bookmarks } allItems={ bookmarks }
source={ MY_BOOKMARKS_SOURCE_NAME } emptyText={ EMPTY_BOOKMARK_MESSAGE }
itemsPerPage={ BOOKMARKS_PER_PAGE } itemsPerPage={ BOOKMARKS_PER_PAGE }
customEmptyText={ EMPTY_BOOKMARK_MESSAGE } source={ MY_BOOKMARKS_SOURCE_NAME }
/> />
) )
}; };
......
...@@ -6,11 +6,11 @@ import { ...@@ -6,11 +6,11 @@ import {
POPULAR_TABLES_LABEL, POPULAR_TABLES_LABEL,
POPULAR_TABLES_PER_PAGE, POPULAR_TABLES_PER_PAGE,
POPULAR_TABLES_SOURCE_NAME, POPULAR_TABLES_SOURCE_NAME,
} from '../constants'; } from './constants';
import InfoButton from 'components/common/InfoButton'; import InfoButton from 'components/common/InfoButton';
import ResourceList from 'components/common/ResourceList'; import PaginatedResourceList from 'components/common/ResourceList/PaginatedResourceList';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import { PopularTables, PopularTablesProps, mapStateToProps, mapDispatchToProps } from '..'; import { PopularTables, PopularTablesProps, mapStateToProps, mapDispatchToProps } from './';
describe('PopularTables', () => { describe('PopularTables', () => {
const setup = (propOverrides?: Partial<PopularTablesProps>) => { const setup = (propOverrides?: Partial<PopularTablesProps>) => {
...@@ -25,7 +25,7 @@ describe('PopularTables', () => { ...@@ -25,7 +25,7 @@ describe('PopularTables', () => {
}; };
let wrapper; let wrapper;
let props; let props;
describe('componentDidMount', () => { describe('componentDidMount', () => {
let getPopularTablesSpy; let getPopularTablesSpy;
beforeAll(() => { beforeAll(() => {
...@@ -46,7 +46,7 @@ describe('PopularTables', () => { ...@@ -46,7 +46,7 @@ describe('PopularTables', () => {
beforeAll(() => { beforeAll(() => {
result = mapStateToProps(globalState); result = mapStateToProps(globalState);
}); });
it('sets popularTables on the props', () => { it('sets popularTables on the props', () => {
expect(result.popularTables).toEqual(globalState.popularTables); expect(result.popularTables).toEqual(globalState.popularTables);
}); });
...@@ -81,8 +81,8 @@ describe('PopularTables', () => { ...@@ -81,8 +81,8 @@ describe('PopularTables', () => {
}); });
}); });
it('renders ResourceList with correct props', () => { it('renders PaginatedResourceList with correct props', () => {
expect(wrapper.children().find(ResourceList).props()).toMatchObject({ expect(wrapper.children().find(PaginatedResourceList).props()).toMatchObject({
allItems: props.popularTables, allItems: props.popularTables,
itemsPerPage: POPULAR_TABLES_PER_PAGE, itemsPerPage: POPULAR_TABLES_PER_PAGE,
source: POPULAR_TABLES_SOURCE_NAME, source: POPULAR_TABLES_SOURCE_NAME,
......
...@@ -12,7 +12,7 @@ import { ...@@ -12,7 +12,7 @@ import {
import { TableResource } from 'interfaces'; import { TableResource } from 'interfaces';
import InfoButton from 'components/common/InfoButton'; import InfoButton from 'components/common/InfoButton';
import ResourceList from 'components/common/ResourceList'; import PaginatedResourceList from 'components/common/ResourceList/PaginatedResourceList';
import { getPopularTables } from 'ducks/popularTables/reducer'; import { getPopularTables } from 'ducks/popularTables/reducer';
import { GetPopularTablesRequest } from 'ducks/popularTables/types'; import { GetPopularTablesRequest } from 'ducks/popularTables/types';
...@@ -46,10 +46,10 @@ export class PopularTables extends React.Component<PopularTablesProps> { ...@@ -46,10 +46,10 @@ export class PopularTables extends React.Component<PopularTablesProps> {
<label className="title-1">{POPULAR_TABLES_LABEL}</label> <label className="title-1">{POPULAR_TABLES_LABEL}</label>
<InfoButton infoText={POPULAR_TABLES_INFO_TEXT} /> <InfoButton infoText={POPULAR_TABLES_INFO_TEXT} />
</div> </div>
<ResourceList <PaginatedResourceList
allItems={ this.props.popularTables } allItems={ this.props.popularTables }
source={ POPULAR_TABLES_SOURCE_NAME }
itemsPerPage={ POPULAR_TABLES_PER_PAGE } itemsPerPage={ POPULAR_TABLES_PER_PAGE }
source={ POPULAR_TABLES_SOURCE_NAME }
/> />
</> </>
); );
......
import * as React from 'react';
import Pagination from 'react-js-pagination';
import { shallow } from 'enzyme';
import { ResourceType } from 'interfaces';
import ResourceListItem from 'components/common/ResourceListItem/index';
import PaginatedApiResourceList, { PaginatedApiResourceListProps } from './';
import * as CONSTANTS from '../constants';
describe('PaginatedApiResourceList', () => {
const setStateSpy = jest.spyOn(PaginatedApiResourceList.prototype, 'setState');
const setup = (propOverrides?: Partial<PaginatedApiResourceListProps>) => {
const props: PaginatedApiResourceListProps = {
activePage: 3,
itemsPerPage: 4,
onPagination: jest.fn(),
slicedItems: [
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
],
totalItemsCount: 40,
source: 'testSource',
...propOverrides
};
const wrapper = shallow<PaginatedApiResourceList>(<PaginatedApiResourceList {...props} />);
return { props, wrapper };
};
describe('onPagination', () => {
it('calls the onPagination prop', () => {
const setupResult = setup();
const wrapper = setupResult.wrapper;
const props = setupResult.props;
const onPaginationSpy = jest.spyOn(props, 'onPagination');
wrapper.instance().onPagination(3);
expect(onPaginationSpy).toHaveBeenCalledWith(2);
});
});
describe('render', () => {
let props;
let wrapper;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('renders empty messages if it exists and there are no items', () => {
const { props, wrapper } = setup({ totalItemsCount: 0, emptyText: 'Nothing Here'});
expect(wrapper.find('.empty-message').text()).toBe(props.emptyText);
});
it('renders all slicedItems', () => {
const items = wrapper.find(ResourceListItem);
expect(items.length).toEqual(props.slicedItems.length);
});
it('renders pagination widget when totalItemsCount is greater than itemsPerPage', () => {
expect(wrapper.find(Pagination).exists()).toBe(true);
});
it('hides pagination widget when totalItemsCount is less than itemsPerPage', () => {
const { props, wrapper } = setup({ totalItemsCount: 2, itemsPerPage: 3 });
expect(wrapper.find(Pagination).exists()).toBe(false);
});
});
});
import * as React from 'react';
import Pagination from 'react-js-pagination';
import ResourceListItem from 'components/common/ResourceListItem';
import { Resource } from 'interfaces';
import * as Constants from '../constants';
import '../styles.scss';
export interface PaginatedApiResourceListProps {
activePage: number;
emptyText?: string;
itemsPerPage: number;
onPagination: (pageNumber: number) => void;
slicedItems: Resource[];
totalItemsCount: number;
source: string;
}
class PaginatedApiResourceList extends React.Component<PaginatedApiResourceListProps, {}> {
public static defaultProps: Partial<PaginatedApiResourceListProps> = {
emptyText: Constants.DEFAULT_EMPTY_TEXT,
};
onPagination = (rawPageNum: number) => {
const activePage = rawPageNum - 1;
this.props.onPagination(activePage);
};
render() {
const { activePage, emptyText, totalItemsCount, itemsPerPage, slicedItems, source } = this.props;
const startIndex = itemsPerPage * activePage;
return (
<div className="paginated-resource-list">
{
totalItemsCount === 0 && emptyText &&
<div className="empty-message body-placeholder">
{ emptyText }
</div>
}
{
totalItemsCount > 0 &&
<>
<ul className="list-group">
{
slicedItems.map((item, idx) => {
const logging = { source, index: startIndex + idx };
return <ResourceListItem item={ item } logging={ logging } key={ idx } />;
})
}
</ul>
{
totalItemsCount > itemsPerPage &&
<Pagination
activePage={ activePage + 1 }
itemsCountPerPage={ itemsPerPage }
totalItemsCount={ totalItemsCount }
pageRangeDisplayed={ Constants.PAGINATION_PAGE_RANGE }
onChange={ this.onPagination }
/>
}
</>
}
</div>
);
}
}
export default PaginatedApiResourceList;
import * as React from 'react';
import Pagination from 'react-js-pagination';
import { shallow } from 'enzyme';
import { ResourceType } from 'interfaces';
import ResourceListItem from 'components/common/ResourceListItem/index';
import PaginatedResourceList, { PaginatedResourceListProps } from './';
import * as CONSTANTS from '../constants';
describe('PaginatedResourceList', () => {
const setStateSpy = jest.spyOn(PaginatedResourceList.prototype, 'setState');
const setup = (propOverrides?: Partial<PaginatedResourceListProps>) => {
const props: PaginatedResourceListProps = {
allItems: [
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
],
itemsPerPage: 4,
source: 'testSource',
...propOverrides
};
const wrapper = shallow<PaginatedResourceList>(<PaginatedResourceList {...props} />);
return { props, wrapper };
};
describe('componentDidUpdate', () => {
it('updates activePage state if the activePage is out of bounds by multiple pages', () => {
const wrapper = setup({ itemsPerPage: 2 }).wrapper;
wrapper.setState({ activePage: 2 });
wrapper.setProps({
allItems: [
{ type: ResourceType.table },
{ type: ResourceType.table },
]
});
expect(wrapper.state().activePage).toEqual(0);
})
it('updates activePage state if the activePage is out of bounds by one page', () => {
const wrapper = setup({ itemsPerPage: 2 }).wrapper;
wrapper.setState({ activePage: 2 });
wrapper.setProps({
allItems: [
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
]
})
expect(wrapper.state().activePage).toEqual(1);
})
it('does not update activePage if new values are not out of bounds', () => {
const wrapper = setup({ itemsPerPage: 2 }).wrapper;
wrapper.setState({ activePage: 2 });
wrapper.setProps({
allItems: [
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
]
})
expect(wrapper.state().activePage).toEqual(2);
})
});
describe('onPagination', () => {
it('calls setState to update the activePage', () => {
setStateSpy.mockClear();
const wrapper = setup().wrapper;
wrapper.instance().onPagination(3);
expect(setStateSpy).toHaveBeenCalledWith({ activePage: 2 });
});
});
describe('render', () => {
let props;
let wrapper;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('renders empty messages if it exists and there are no items', () => {
const { props, wrapper } = setup({ allItems: [], emptyText: 'Nothing Here'});
expect(wrapper.find('.empty-message').text()).toBe(props.emptyText);
});
it('renders at most itemsPerPage ResourceListItems', () => {
const items = wrapper.find(ResourceListItem);
expect(items.length).toEqual(props.itemsPerPage);
});
it('renders a pagination widget when there are more than itemsPerPage items', () => {
expect(wrapper.find(Pagination).exists()).toBe(true)
});
it('hides a pagination widget when there are fewer than itemsPerPage items', () => {
const { props, wrapper } = setup({ itemsPerPage: 20 });
expect(wrapper.find(Pagination).exists()).toBe(false)
});
});
});
import * as React from 'react';
import Pagination from 'react-js-pagination';
import ResourceListItem from 'components/common/ResourceListItem';
import { Resource } from 'interfaces';
import * as Constants from '../constants';
import '../styles.scss';
export interface PaginatedResourceListProps {
allItems: Resource[];
emptyText?: string;
itemsPerPage: number;
source: string;
}
interface PaginatedResourceListState {
activePage: number;
}
class PaginatedResourceList extends React.Component<PaginatedResourceListProps, PaginatedResourceListState> {
public static defaultProps: Partial<PaginatedResourceListProps> = {
emptyText: Constants.DEFAULT_EMPTY_TEXT,
};
constructor(props) {
super(props);
this.state = {
activePage: 0,
};
}
componentDidUpdate(prevProps, prevState) {
// Resets the activePage to the maximum possible value if page is out of bounds for the new length of allItems
const effectivePageNum = this.state.activePage + 1;
const { itemsPerPage, allItems } = this.props;
const newPage = Math.ceil(allItems.length / itemsPerPage) - 1;
if (itemsPerPage * effectivePageNum > allItems.length && newPage !== this.state.activePage) {
this.setState({ activePage: newPage });
}
}
onPagination = (rawPageNum: number) => {
const activePage = rawPageNum - 1;
this.setState({ activePage });
};
render() {
const { allItems, emptyText, itemsPerPage, source } = this.props;
const activePage = this.state.activePage;
const allItemsCount = allItems.length;
const startIndex = itemsPerPage * activePage;
const itemsToRender = this.props.allItems.slice(startIndex, startIndex + itemsPerPage);
return (
<div className="paginated-resource-list">
{
allItemsCount === 0 && emptyText &&
<div className="empty-message body-placeholder">
{ emptyText }
</div>
}
{
allItemsCount > 0 &&
<>
<ul className="list-group">
{
itemsToRender.map((item, idx) => {
const logging = { source, index: startIndex + idx };
return <ResourceListItem item={ item } logging={ logging } key={ idx } />;
})
}
</ul>
{
allItemsCount > itemsPerPage &&
<Pagination
activePage={ activePage + 1 }
itemsCountPerPage={ itemsPerPage }
totalItemsCount={ allItemsCount }
pageRangeDisplayed={ Constants.PAGINATION_PAGE_RANGE }
onChange={ this.onPagination }
/>
}
</>
}
</div>
);
}
}
export default PaginatedResourceList;
export const ITEMS_PER_PAGE = 4;
export const PAGINATION_PAGE_RANGE = 10; export const PAGINATION_PAGE_RANGE = 10;
export const DEFAULT_EMPTY_TEXT = 'No resources to display';
export const FOOTER_TEXT_COLLAPSED = 'View all';
export const FOOTER_TEXT_EXPANDED = 'View less';
import * as React from 'react'; import * as React from 'react';
import Pagination from 'react-js-pagination';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
import ResourceListItem from 'components/common/ResourceListItem/index'; import ResourceListItem from 'components/common/ResourceListItem/index';
import ResourceList, { ResourceListProps } from '../'; import ResourceList, { ResourceListProps } from './';
import * as CONSTANTS from '../constants'; import * as CONSTANTS from './constants';
describe('ResourceList', () => { describe('ResourceList', () => {
const setStateSpy = jest.spyOn(ResourceList.prototype, 'setState'); const setStateSpy = jest.spyOn(ResourceList.prototype, 'setState');
const setupAllItems = (propOverrides?: Partial<ResourceListProps>) => { const setup = (propOverrides?: Partial<ResourceListProps>) => {
const props: ResourceListProps = { const props: ResourceListProps = {
allItems: [ allItems: [
{ type: ResourceType.table }, { type: ResourceType.table },
...@@ -29,28 +28,9 @@ describe('ResourceList', () => { ...@@ -29,28 +28,9 @@ describe('ResourceList', () => {
return { props, wrapper }; return { props, wrapper };
}; };
const setupSlicedItems = (propOverrides?: Partial<ResourceListProps>) => {
const props: ResourceListProps = {
source: 'testSource',
activePage: 3,
slicedItems: [
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
],
slicedItemsCount: 40,
itemsPerPage: 4,
onPagination: jest.fn(),
...propOverrides
};
const wrapper = shallow<ResourceList>(<ResourceList {...props} />);
return { props, wrapper };
};
describe('onViewAllToggle', () => { describe('onViewAllToggle', () => {
it('negates state.Expanded', () => { it('negates state.Expanded', () => {
const wrapper = setupAllItems().wrapper; const wrapper = setup().wrapper;
const initialState = wrapper.state().isExpanded; const initialState = wrapper.state().isExpanded;
wrapper.instance().onViewAllToggle(); wrapper.instance().onViewAllToggle();
expect(wrapper.state().isExpanded).toEqual(!initialState); expect(wrapper.state().isExpanded).toEqual(!initialState);
...@@ -60,64 +40,24 @@ describe('ResourceList', () => { ...@@ -60,64 +40,24 @@ describe('ResourceList', () => {
}) })
describe('render', () => { describe('render', () => {
describe('renders title if it exists', () => {
const { props, wrapper } = setupAllItems({ title: 'I am a title'});
expect(wrapper.find('.resource-list-title').text()).toBe(props.title);
});
describe('renders empty messages if it exists and there are no items', () => {
const { props, wrapper } = setupAllItems({ allItems: [], customEmptyText: 'Nothing Here'});
expect(wrapper.find('.empty-message').text()).toBe(props.customEmptyText);
});
describe('renders footer', () => {
it('renders nothing if pagination is turned on', () => {
const { props, wrapper } = setupAllItems({ paginate: true });
expect(wrapper.find('.resource-list-footer').children().length).toBe(0);
});
describe('renders toggle link if not paginating with itemsToRender.length > ITEMS_PER_PAGE', () => {
let props;
let wrapper;
let footerLink;
beforeAll(() => {
const setupResult = setupAllItems({ paginate: false });
props = setupResult.props;
wrapper = setupResult.wrapper;
footerLink = wrapper.find('.resource-list-footer').find('a');
});
it('renders a link to toggle viewing items', () => {
expect(footerLink.props().onClick).toEqual(wrapper.instance().onViewAllToggle)
});
it('renders correct default text if not expanded', () => {
wrapper.setState({ isExpanded: false });
expect(wrapper.find('.resource-list-footer').find('a').text()).toEqual('View all');
});
it('renders customFooterText text if it exists and not expanded', () => {
wrapper = setupAllItems({ paginate: false, customFooterText: 'Hello' }).wrapper;
expect(wrapper.find('.resource-list-footer').find('a').text()).toEqual('Hello');
})
it('renders correct default text if expanded', () => {
wrapper.setState({ isExpanded: true });
expect(wrapper.find('.resource-list-footer').find('a').text()).toEqual('View less');
});
});
});
});
describe('render with no pagination', () => {
let props; let props;
let wrapper; let wrapper;
beforeAll(() => { beforeAll(() => {
const setupResult = setupAllItems({ paginate: false }); const setupResult = setup();
props = setupResult.props; props = setupResult.props;
wrapper = setupResult.wrapper; wrapper = setupResult.wrapper;
}); });
it('renders title if it exists', () => {
const { props, wrapper } = setup({ title: 'I am a title'});
expect(wrapper.find('.resource-list-title').text()).toBe(props.title);
});
it('renders empty messages if it exists and there are no items', () => {
const { props, wrapper } = setup({ allItems: [], emptyText: 'Nothing Here'});
expect(wrapper.find('.empty-message').text()).toBe(props.emptyText);
});
it('should render all items if expanded', () => { it('should render all items if expanded', () => {
wrapper.setState({ isExpanded: true }); wrapper.setState({ isExpanded: true });
const items = wrapper.find(ResourceListItem); const items = wrapper.find(ResourceListItem);
...@@ -127,7 +67,7 @@ describe('ResourceList', () => { ...@@ -127,7 +67,7 @@ describe('ResourceList', () => {
it('should render items.length = itemsPerPage if not expanded', () => { it('should render items.length = itemsPerPage if not expanded', () => {
wrapper.setState({ isExpanded: false }); wrapper.setState({ isExpanded: false });
const items = wrapper.find(ResourceListItem); const items = wrapper.find(ResourceListItem);
expect(items.length).toEqual(CONSTANTS.ITEMS_PER_PAGE); expect(items.length).toEqual(props.itemsPerPage);
}); });
it('passes correct props to each ResourceListItem', () => { it('passes correct props to each ResourceListItem', () => {
...@@ -142,77 +82,31 @@ describe('ResourceList', () => { ...@@ -142,77 +82,31 @@ describe('ResourceList', () => {
}) })
}); });
}); });
});
describe('render allItems', () => { describe('renders footer toggle link if num items > ITEMS_PER_PAGE', () => {
let props; let footerLink;
let wrapper; beforeAll(() => {
beforeAll(() => { footerLink = wrapper.find('.resource-list-footer').find('a');
const setupResult = setupAllItems();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('renders at most itemsPerPage ResourceListItems', () => {
const items = wrapper.find(ResourceListItem);
expect(items.length).toEqual(props.itemsPerPage);
});
it('renders a pagination widget when there are more than itemsPerPage items', () => {
expect(wrapper.find(Pagination).exists()).toBe(true)
});
it('hides a pagination widget when there are fewer than itemsPerPage items', () => {
const { props, wrapper } = setupAllItems({
itemsPerPage: 20,
}); });
expect(wrapper.find(Pagination).exists()).toBe(false)
});
});
describe('render slicedItems', () => { it('renders a link to toggle viewing items', () => {
let props; expect(footerLink.props().onClick).toEqual(wrapper.instance().onViewAllToggle)
let wrapper;
beforeAll(() => {
const setupResult = setupSlicedItems();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('renders all slicedItems', () => {
const items = wrapper.find(ResourceListItem);
expect(items.length).toEqual(props.slicedItems.length);
});
it('renders pagination widget when slicedItemsCount is greater than itemsPerPage', () => {
expect(wrapper.find(Pagination).exists()).toBe(true);
});
it('hides pagination widget when slicedItemsCount is less than itemsPerPage', () => {
const { props, wrapper } = setupAllItems({
itemsPerPage: 20,
}); });
expect(wrapper.find(Pagination).exists()).toBe(false);
});
});
describe('onPagination', () => { it('renders correct default text if not expanded', () => {
it('calls the onPagination prop when it exists', () => { wrapper.setState({ isExpanded: false });
const setupResult = setupSlicedItems(); expect(wrapper.find('.resource-list-footer').find('a').text()).toEqual(CONSTANTS.FOOTER_TEXT_COLLAPSED);
const wrapper = setupResult.wrapper; });
const props = setupResult.props;
const onPaginationSpy = jest.spyOn(props, 'onPagination');
wrapper.instance().onPagination(3); it('renders props.footerTextCollapsed if it exists and not expanded', () => {
expect(onPaginationSpy).toHaveBeenCalledWith(2); const wrapper = setup({ footerTextCollapsed: 'Hello' }).wrapper;
}); expect(wrapper.find('.resource-list-footer').find('a').text()).toEqual('Hello');
})
it('calls setState when onPagination prop does not exist', () => { it('renders correct default text if expanded', () => {
setStateSpy.mockClear(); wrapper.setState({ isExpanded: true });
const setupResult = setupSlicedItems({ onPagination: undefined }); expect(wrapper.find('.resource-list-footer').find('a').text()).toEqual(CONSTANTS.FOOTER_TEXT_EXPANDED);
const wrapper = setupResult.wrapper; });
wrapper.instance().onPagination(3);
expect(setStateSpy).toHaveBeenCalledWith({ activePage: 2 });
}); });
}); });
}); });
...@@ -2,77 +2,44 @@ import * as React from 'react'; ...@@ -2,77 +2,44 @@ import * as React from 'react';
import Pagination from 'react-js-pagination'; import Pagination from 'react-js-pagination';
import ResourceListItem from 'components/common/ResourceListItem'; import ResourceListItem from 'components/common/ResourceListItem';
import { Resource } from 'interfaces'; import { Resource } from 'interfaces';
import { ITEMS_PER_PAGE, PAGINATION_PAGE_RANGE } from './constants'; import * as Constants from './constants';
import './styles.scss'; import './styles.scss';
export interface ResourceListProps { export interface ResourceListProps {
source: string; source: string;
paginate?: boolean; itemsPerPage: number;
itemsPerPage?: number; allItems: Resource[];
emptyText?: string;
// Choose to use either 'allItems' vs 'slicedItems' depending on if you're passing the entire list footerTextCollapsed?: string;
// of items vs a pre-sliced section of all items.
allItems?: Resource[];
// 'slicedItems' and 'slicedItemsCount' should be used together
slicedItems?: Resource[];
slicedItemsCount?: number;
// 'onPagination' and 'activePage' should be used together
onPagination?: (pageNumber: number) => void;
activePage?: number;
customEmptyText?: string;
customFooterText?: string;
title?: string; title?: string;
} }
interface ResourceListState { interface ResourceListState {
activePage: number;
isExpanded: boolean; isExpanded: boolean;
} }
class ResourceList extends React.Component<ResourceListProps, ResourceListState> { class ResourceList extends React.Component<ResourceListProps, ResourceListState> {
public static defaultProps: Partial<ResourceListProps> = { public static defaultProps: Partial<ResourceListProps> = {
paginate: true, emptyText: Constants.DEFAULT_EMPTY_TEXT,
itemsPerPage: ITEMS_PER_PAGE, footerTextCollapsed: Constants.FOOTER_TEXT_COLLAPSED,
}; };
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
activePage: 0,
isExpanded: false, isExpanded: false,
}; };
} }
onPagination = (rawPageNum: number) => {
const activePage = rawPageNum - 1;
if (this.props.onPagination !== undefined) {
// activePage is managed externally via 'props'
this.props.onPagination(activePage);
} else {
// activePage is managed internally via 'state'.
this.setState({ activePage });
}
};
onViewAllToggle = () => { onViewAllToggle = () => {
this.setState({ isExpanded: !this.state.isExpanded }) this.setState({ isExpanded: !this.state.isExpanded })
}; };
render() { render() {
/* TODO ttannis: create render helpers */ const { allItems, emptyText, footerTextCollapsed, itemsPerPage, source, title } = this.props;
const { allItems, customEmptyText, customFooterText, slicedItems, itemsPerPage, paginate, source, title } = this.props; const allItemsCount = allItems.length;
const activePage = this.props.activePage !== undefined ? this.props.activePage : this.state.activePage; const itemsToRender = this.state.isExpanded ? allItems : allItems.slice(0, itemsPerPage);
const itemsCount = this.props.slicedItemsCount || allItems.length;
const startIndex = itemsPerPage * activePage;
let itemsToRender = slicedItems || allItems;
if ((paginate && allItems) || (!this.state.isExpanded && allItems)) {
itemsToRender = allItems.slice(startIndex, startIndex + itemsPerPage);
}
return ( return (
<div className="resource-list"> <div className="resource-list">
...@@ -81,42 +48,30 @@ class ResourceList extends React.Component<ResourceListProps, ResourceListState> ...@@ -81,42 +48,30 @@ class ResourceList extends React.Component<ResourceListProps, ResourceListState>
<div className="resource-list-title title-3">{title}</div> <div className="resource-list-title title-3">{title}</div>
} }
{ {
itemsCount === 0 && customEmptyText && allItemsCount === 0 && emptyText &&
<div className="empty-message body-placeholder"> <div className="empty-message body-placeholder">
{ customEmptyText } { emptyText }
</div> </div>
} }
{ {
itemsCount > 0 && allItemsCount > 0 &&
<> <>
<ul className="list-group"> <ul className="list-group">
{ {
itemsToRender.map((item, idx) => { itemsToRender.map((item, index) => {
const logging = { source, index: startIndex + idx }; const logging = { source, index };
return <ResourceListItem item={ item } logging={ logging } key={ idx } />; return <ResourceListItem item={ item } logging={ logging } key={ index } />;
}) })
} }
</ul> </ul>
{
paginate &&
itemsCount > itemsPerPage &&
<Pagination
activePage={ activePage + 1 }
itemsCountPerPage={ itemsPerPage }
totalItemsCount={ itemsCount }
pageRangeDisplayed={ PAGINATION_PAGE_RANGE }
onChange={ this.onPagination }
/>
}
<div className="resource-list-footer"> <div className="resource-list-footer">
{ {
!paginate && allItemsCount > itemsPerPage &&
itemsCount > itemsPerPage &&
<a <a
onClick={this.onViewAllToggle} onClick={this.onViewAllToggle}
target='_blank' target='_blank'
> >
{ this.state.isExpanded ? "View less" : (customFooterText ? customFooterText : "View all") } { this.state.isExpanded ? Constants.FOOTER_TEXT_EXPANDED : footerTextCollapsed }
</a> </a>
} }
</div> </div>
......
@import 'variables'; @import 'variables';
.paginated-resource-list,
.resource-list { .resource-list {
ul { ul {
margin-bottom: 0; margin-bottom: 0;
...@@ -24,3 +25,7 @@ ...@@ -24,3 +25,7 @@
justify-content: center; justify-content: center;
} }
} }
.paginated-resource-list {
margin-bottom: $spacer-4
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment