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';
import Breadcrumb from 'components/common/Breadcrumb';
import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon';
import Flag from 'components/common/Flag';
import ResourceList from 'components/common/ResourceList';
import TabsComponent from 'components/common/TabsComponent';
import ChartList from './ChartList';
import ImagePreview from './ImagePreview';
......@@ -187,6 +188,13 @@ describe('DashboardPage', () => {
describe('renderTabs', () => {
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', () => {
const result = wrapper.instance().renderTabs();
expect(result.type).toEqual(TabsComponent);
......
......@@ -99,7 +99,6 @@ export class DashboardPage extends React.Component<DashboardPageProps, Dashboard
<ResourceList
allItems={ this.props.dashboard.tables }
itemsPerPage={ TABLES_PER_PAGE }
paginate={ false }
source={ DASHBOARD_SOURCE }
/>
),
......
......@@ -9,7 +9,7 @@ import Breadcrumb from 'components/common/Breadcrumb';
import Flag from 'components/common/Flag';
import ResourceList from 'components/common/ResourceList';
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 { getMockRouterProps } from 'fixtures/mockRouter';
......
......@@ -100,33 +100,30 @@ export class ProfilePage extends React.Component<ProfilePageProps, ProfilePageSt
<>
<ResourceList
allItems={ own }
emptyText={`${EMPTY_TEXT_PREFIX} ${OWNED_LABEL} ${resourceLabel}.`}
footerTextCollapsed={`${FOOTER_TEXT_PREFIX} ${own.length} ${OWNED_LABEL} ${resourceLabel}`}
itemsPerPage={ ITEMS_PER_PAGE }
paginate={ false }
source={ OWNED_SOURCE }
title={`${OWNED_TITLE_PREFIX} (${own.length})`}
customFooterText={`${FOOTER_TEXT_PREFIX} ${own.length} ${OWNED_LABEL} ${resourceLabel}`}
customEmptyText={`${EMPTY_TEXT_PREFIX} ${OWNED_LABEL} ${resourceLabel}.`}
/>
<ResourceList
allItems={ bookmarks }
emptyText={`${EMPTY_TEXT_PREFIX} ${BOOKMARKED_LABEL} ${resourceLabel}.`}
footerTextCollapsed={`${FOOTER_TEXT_PREFIX} ${bookmarks.length} ${BOOKMARKED_LABEL} ${resourceLabel}`}
itemsPerPage={ ITEMS_PER_PAGE }
paginate={ false }
source={ BOOKMARKED_SOURCE }
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 */
resource === ResourceType.table &&
<ResourceList
allItems={ read }
emptyText={`${EMPTY_TEXT_PREFIX} ${READ_LABEL} ${resourceLabel}.`}
footerTextCollapsed={`${FOOTER_TEXT_PREFIX} ${read.length} ${READ_LABEL} ${resourceLabel}`}
itemsPerPage={ ITEMS_PER_PAGE }
paginate={ false }
source={ READ_SOURCE }
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';
import { shallow } from 'enzyme';
import { ResourceType } from 'interfaces';
import { mapDispatchToProps, mapStateToProps, SearchPage, SearchPageProps } from '../';
import { mapDispatchToProps, mapStateToProps, SearchPage, SearchPageProps } from './';
import {
DOCUMENT_TITLE_SUFFIX,
PAGE_INDEX_ERROR_MESSAGE,
......@@ -17,13 +17,13 @@ import {
DASHBOARD_RESOURCE_TITLE,
TABLE_RESOURCE_TITLE,
USER_RESOURCE_TITLE,
} from '../constants';
} from './constants';
import LoadingSpinner from 'components/common/LoadingSpinner';
import ResourceSelector from 'components/SearchPage/ResourceSelector';
import SearchFilter from 'components/SearchPage/SearchFilter';
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 { defaultEmptyFilters, datasetFilterExample } from 'fixtures/search/filters';
......@@ -194,7 +194,7 @@ describe('SearchPage', () => {
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 testResults = {
page_index: 0,
......@@ -202,14 +202,13 @@ describe('SearchPage', () => {
total_results: 11,
};
content = shallow(wrapper.instance().getTabContent(testResults, ResourceType.table));
expect(content.children().find(ResourceList).props()).toMatchObject({
expect(content.children().find(PaginatedApiResourceList).props()).toMatchObject({
activePage: 0,
slicedItems: testResults.results,
slicedItemsCount: testResults.total_results,
itemsPerPage: RESULTS_PER_PAGE,
onPagination: props.setPageIndex,
slicedItems: testResults.results,
source: SEARCH_SOURCE_NAME,
totalItemsCount: testResults.total_results,
});
});
});
......
......@@ -6,7 +6,7 @@ import { RouteComponentProps } from 'react-router';
import { Search as UrlSearch } from 'history';
import LoadingSpinner from 'components/common/LoadingSpinner';
import ResourceList from 'components/common/ResourceList';
import PaginatedApiResourceList from 'components/common/ResourceList/PaginatedApiResourceList';
import ResourceSelector from './ResourceSelector';
import SearchFilter from './SearchFilter';
import SearchPanel from './SearchPanel';
......@@ -140,13 +140,13 @@ export class SearchPage extends React.Component<SearchPageProps> {
return (
<div className="search-list-container">
<ResourceList
slicedItems={ results.results }
slicedItemsCount={ total_results }
source={ SEARCH_SOURCE_NAME }
itemsPerPage={ RESULTS_PER_PAGE }
<PaginatedApiResourceList
activePage={ page_index }
onPagination={ this.props.setPageIndex }
itemsPerPage={ RESULTS_PER_PAGE }
slicedItems={ results.results }
source={ SEARCH_SOURCE_NAME }
totalItemsCount={ total_results }
/>
</div>
);
......
......@@ -5,15 +5,15 @@ import { mocked } from 'ts-jest/utils';
import globalState from 'fixtures/globalState';
import { ResourceType } from 'interfaces';
import { MyBookmarks, MyBookmarksProps, mapStateToProps } from '../';
import ResourceList from 'components/common/ResourceList';
import { MyBookmarks, MyBookmarksProps, mapStateToProps } from './';
import PaginatedResourceList from 'components/common/ResourceList/PaginatedResourceList';
import TabsComponent from 'components/common/TabsComponent';
import {
BOOKMARK_TITLE,
BOOKMARKS_PER_PAGE,
EMPTY_BOOKMARK_MESSAGE,
MY_BOOKMARKS_SOURCE_NAME,
} from '../constants';
} from './constants';
jest.mock('config/config-utils', () => ({
getDisplayNameByResource: jest.fn(() => 'Resource'),
......@@ -105,12 +105,12 @@ describe('MyBookmarks', () => {
content = shallow(<div>{wrapper.instance().generateTabContent(givenResource)}</div>);
});
it('returns a ResourceList with correct props', () => {
const element = content.find(ResourceList);
it('returns a PaginatedResourceList with correct props', () => {
const element = content.find(PaginatedResourceList);
expect(element.props().allItems).toBe(props.myBookmarks[givenResource]);
expect(element.props().itemsPerPage).toBe(BOOKMARKS_PER_PAGE);
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', () => {
......
......@@ -11,7 +11,7 @@ import {
EMPTY_BOOKMARK_MESSAGE,
MY_BOOKMARKS_SOURCE_NAME,
} from './constants';
import ResourceList from 'components/common/ResourceList';
import PaginatedResourceList from 'components/common/ResourceList/PaginatedResourceList';
import TabsComponent from 'components/common/TabsComponent';
interface StateFromProps {
......@@ -32,11 +32,11 @@ export class MyBookmarks extends React.Component<MyBookmarksProps> {
return null;
}
return (
<ResourceList
<PaginatedResourceList
allItems={ bookmarks }
source={ MY_BOOKMARKS_SOURCE_NAME }
emptyText={ EMPTY_BOOKMARK_MESSAGE }
itemsPerPage={ BOOKMARKS_PER_PAGE }
customEmptyText={ EMPTY_BOOKMARK_MESSAGE }
source={ MY_BOOKMARKS_SOURCE_NAME }
/>
)
};
......
......@@ -6,11 +6,11 @@ import {
POPULAR_TABLES_LABEL,
POPULAR_TABLES_PER_PAGE,
POPULAR_TABLES_SOURCE_NAME,
} from '../constants';
} from './constants';
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 { PopularTables, PopularTablesProps, mapStateToProps, mapDispatchToProps } from '..';
import { PopularTables, PopularTablesProps, mapStateToProps, mapDispatchToProps } from './';
describe('PopularTables', () => {
const setup = (propOverrides?: Partial<PopularTablesProps>) => {
......@@ -25,7 +25,7 @@ describe('PopularTables', () => {
};
let wrapper;
let props;
describe('componentDidMount', () => {
let getPopularTablesSpy;
beforeAll(() => {
......@@ -46,7 +46,7 @@ describe('PopularTables', () => {
beforeAll(() => {
result = mapStateToProps(globalState);
});
it('sets popularTables on the props', () => {
expect(result.popularTables).toEqual(globalState.popularTables);
});
......@@ -81,8 +81,8 @@ describe('PopularTables', () => {
});
});
it('renders ResourceList with correct props', () => {
expect(wrapper.children().find(ResourceList).props()).toMatchObject({
it('renders PaginatedResourceList with correct props', () => {
expect(wrapper.children().find(PaginatedResourceList).props()).toMatchObject({
allItems: props.popularTables,
itemsPerPage: POPULAR_TABLES_PER_PAGE,
source: POPULAR_TABLES_SOURCE_NAME,
......
......@@ -12,7 +12,7 @@ import {
import { TableResource } from 'interfaces';
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 { GetPopularTablesRequest } from 'ducks/popularTables/types';
......@@ -46,10 +46,10 @@ export class PopularTables extends React.Component<PopularTablesProps> {
<label className="title-1">{POPULAR_TABLES_LABEL}</label>
<InfoButton infoText={POPULAR_TABLES_INFO_TEXT} />
</div>
<ResourceList
<PaginatedResourceList
allItems={ this.props.popularTables }
source={ POPULAR_TABLES_SOURCE_NAME }
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 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 Pagination from 'react-js-pagination';
import { shallow } from 'enzyme';
import { ResourceType } from 'interfaces';
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', () => {
const setStateSpy = jest.spyOn(ResourceList.prototype, 'setState');
const setupAllItems = (propOverrides?: Partial<ResourceListProps>) => {
const setup = (propOverrides?: Partial<ResourceListProps>) => {
const props: ResourceListProps = {
allItems: [
{ type: ResourceType.table },
......@@ -29,28 +28,9 @@ describe('ResourceList', () => {
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', () => {
it('negates state.Expanded', () => {
const wrapper = setupAllItems().wrapper;
const wrapper = setup().wrapper;
const initialState = wrapper.state().isExpanded;
wrapper.instance().onViewAllToggle();
expect(wrapper.state().isExpanded).toEqual(!initialState);
......@@ -60,64 +40,24 @@ describe('ResourceList', () => {
})
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 wrapper;
beforeAll(() => {
const setupResult = setupAllItems({ paginate: false });
const setupResult = setup();
props = setupResult.props;
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', () => {
wrapper.setState({ isExpanded: true });
const items = wrapper.find(ResourceListItem);
......@@ -127,7 +67,7 @@ describe('ResourceList', () => {
it('should render items.length = itemsPerPage if not expanded', () => {
wrapper.setState({ isExpanded: false });
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', () => {
......@@ -142,77 +82,31 @@ describe('ResourceList', () => {
})
});
});
});
describe('render allItems', () => {
let props;
let wrapper;
beforeAll(() => {
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,
describe('renders footer toggle link if num items > ITEMS_PER_PAGE', () => {
let footerLink;
beforeAll(() => {
footerLink = wrapper.find('.resource-list-footer').find('a');
});
expect(wrapper.find(Pagination).exists()).toBe(false)
});
});
describe('render slicedItems', () => {
let props;
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,
it('renders a link to toggle viewing items', () => {
expect(footerLink.props().onClick).toEqual(wrapper.instance().onViewAllToggle)
});
expect(wrapper.find(Pagination).exists()).toBe(false);
});
});
describe('onPagination', () => {
it('calls the onPagination prop when it exists', () => {
const setupResult = setupSlicedItems();
const wrapper = setupResult.wrapper;
const props = setupResult.props;
const onPaginationSpy = jest.spyOn(props, 'onPagination');
it('renders correct default text if not expanded', () => {
wrapper.setState({ isExpanded: false });
expect(wrapper.find('.resource-list-footer').find('a').text()).toEqual(CONSTANTS.FOOTER_TEXT_COLLAPSED);
});
wrapper.instance().onPagination(3);
expect(onPaginationSpy).toHaveBeenCalledWith(2);
});
it('renders props.footerTextCollapsed if it exists and not expanded', () => {
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', () => {
setStateSpy.mockClear();
const setupResult = setupSlicedItems({ onPagination: undefined });
const wrapper = setupResult.wrapper;
wrapper.instance().onPagination(3);
expect(setStateSpy).toHaveBeenCalledWith({ activePage: 2 });
it('renders correct default text if expanded', () => {
wrapper.setState({ isExpanded: true });
expect(wrapper.find('.resource-list-footer').find('a').text()).toEqual(CONSTANTS.FOOTER_TEXT_EXPANDED);
});
});
});
});
......@@ -2,77 +2,44 @@ import * as React from 'react';
import Pagination from 'react-js-pagination';
import ResourceListItem from 'components/common/ResourceListItem';
import { Resource } from 'interfaces';
import { ITEMS_PER_PAGE, PAGINATION_PAGE_RANGE } from './constants';
import * as Constants from './constants';
import './styles.scss';
export interface ResourceListProps {
source: string;
paginate?: boolean;
itemsPerPage?: number;
// Choose to use either 'allItems' vs 'slicedItems' depending on if you're passing the entire list
// 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;
itemsPerPage: number;
allItems: Resource[];
emptyText?: string;
footerTextCollapsed?: string;
title?: string;
}
interface ResourceListState {
activePage: number;
isExpanded: boolean;
}
class ResourceList extends React.Component<ResourceListProps, ResourceListState> {
public static defaultProps: Partial<ResourceListProps> = {
paginate: true,
itemsPerPage: ITEMS_PER_PAGE,
emptyText: Constants.DEFAULT_EMPTY_TEXT,
footerTextCollapsed: Constants.FOOTER_TEXT_COLLAPSED,
};
constructor(props) {
super(props);
this.state = {
activePage: 0,
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 = () => {
this.setState({ isExpanded: !this.state.isExpanded })
};
render() {
/* TODO ttannis: create render helpers */
const { allItems, customEmptyText, customFooterText, slicedItems, itemsPerPage, paginate, source, title } = this.props;
const activePage = this.props.activePage !== undefined ? this.props.activePage : this.state.activePage;
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);
}
const { allItems, emptyText, footerTextCollapsed, itemsPerPage, source, title } = this.props;
const allItemsCount = allItems.length;
const itemsToRender = this.state.isExpanded ? allItems : allItems.slice(0, itemsPerPage);
return (
<div className="resource-list">
......@@ -81,42 +48,30 @@ class ResourceList extends React.Component<ResourceListProps, ResourceListState>
<div className="resource-list-title title-3">{title}</div>
}
{
itemsCount === 0 && customEmptyText &&
allItemsCount === 0 && emptyText &&
<div className="empty-message body-placeholder">
{ customEmptyText }
{ emptyText }
</div>
}
{
itemsCount > 0 &&
allItemsCount > 0 &&
<>
<ul className="list-group">
{
itemsToRender.map((item, idx) => {
const logging = { source, index: startIndex + idx };
return <ResourceListItem item={ item } logging={ logging } key={ idx } />;
itemsToRender.map((item, index) => {
const logging = { source, index };
return <ResourceListItem item={ item } logging={ logging } key={ index } />;
})
}
</ul>
{
paginate &&
itemsCount > itemsPerPage &&
<Pagination
activePage={ activePage + 1 }
itemsCountPerPage={ itemsPerPage }
totalItemsCount={ itemsCount }
pageRangeDisplayed={ PAGINATION_PAGE_RANGE }
onChange={ this.onPagination }
/>
}
<div className="resource-list-footer">
{
!paginate &&
itemsCount > itemsPerPage &&
allItemsCount > itemsPerPage &&
<a
onClick={this.onViewAllToggle}
target='_blank'
>
{ this.state.isExpanded ? "View less" : (customFooterText ? customFooterText : "View all") }
{ this.state.isExpanded ? Constants.FOOTER_TEXT_EXPANDED : footerTextCollapsed }
</a>
}
</div>
......
@import 'variables';
.paginated-resource-list,
.resource-list {
ul {
margin-bottom: 0;
......@@ -24,3 +25,7 @@
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