diff --git a/packages/react-vapor/docs/Reducers.ts b/packages/react-vapor/docs/Reducers.ts index 2398f0b2d2..57f32f6aeb 100644 --- a/packages/react-vapor/docs/Reducers.ts +++ b/packages/react-vapor/docs/Reducers.ts @@ -4,20 +4,14 @@ import { IListBoxExampleCompositeState, listBoxExampleReducer, } from '../src/components/listBox/examples/ListBoxExampleReducer'; -import { - IExampleServerTableState, - TableHOCServerExampleReducer, -} from '../src/components/table-hoc/examples/TableHOCServerExamples'; import {IReactVaporState} from '../src/ReactVapor'; import {ReactVaporReducers} from '../src/ReactVaporReducers'; export interface IReactVaporExampleState extends IReactVaporState { listBoxExampleState?: IListBoxExampleCompositeState; - tableHOCExample?: IExampleServerTableState; } export const ExamplesReducers: Redux.Reducer = Redux.combineReducers({ ...ReactVaporReducers, listBoxExampleState: listBoxExampleReducer, - tableHOCExample: TableHOCServerExampleReducer, }); diff --git a/packages/react-vapor/src/components/actions/ActionBarReducers.ts b/packages/react-vapor/src/components/actions/ActionBarReducers.ts index 501d32499d..0048546509 100644 --- a/packages/react-vapor/src/components/actions/ActionBarReducers.ts +++ b/packages/react-vapor/src/components/actions/ActionBarReducers.ts @@ -7,7 +7,7 @@ import {LoadingActions} from '../loading/LoadingActions'; import {PaginationActions} from '../navigation/pagination/NavigationPaginationActions'; import {PerPageActions} from '../navigation/perPage/NavigationPerPageActions'; import {TableHOCRowActionsType} from '../table-hoc/actions/TableHOCRowActions'; -import {TableHOCUtils} from '../table-hoc/TableHOCUtils'; +import {TableHOCUtils} from '../table-hoc/utils/TableHOCUtils'; import {IActionOptions} from './Action'; import {ActionBarActions} from './ActionBarActions'; diff --git a/packages/react-vapor/src/components/actions/tests/actionBarsReducers.spec.ts b/packages/react-vapor/src/components/actions/tests/actionBarsReducers.spec.ts index 6a09f42e41..67f45aa59e 100644 --- a/packages/react-vapor/src/components/actions/tests/actionBarsReducers.spec.ts +++ b/packages/react-vapor/src/components/actions/tests/actionBarsReducers.spec.ts @@ -4,7 +4,7 @@ import {selectListBoxOption} from '../../listBox/ListBoxActions'; import {turnOffLoading, turnOnLoading} from '../../loading/LoadingActions'; import {changePage} from '../../navigation/pagination/NavigationPaginationActions'; import {changePerPage} from '../../navigation/perPage/NavigationPerPageActions'; -import {TableHOCUtils} from '../../table-hoc/TableHOCUtils'; +import {TableHOCUtils} from '../../table-hoc/utils/TableHOCUtils'; import {ActionBarActions, IActionBarPayload, IChangeActionBarActionsPayload} from '../ActionBarActions'; import { actionBarInitialState, diff --git a/packages/react-vapor/src/components/flatSelect/FlatSelect.tsx b/packages/react-vapor/src/components/flatSelect/FlatSelect.tsx index 5eb5510f64..d41f2e8f36 100644 --- a/packages/react-vapor/src/components/flatSelect/FlatSelect.tsx +++ b/packages/react-vapor/src/components/flatSelect/FlatSelect.tsx @@ -6,11 +6,13 @@ import {FlatSelectOption, IFlatSelectOptionProps} from './FlatSelectOption'; export interface IFlatSelectOwnProps { id: string; options: IFlatSelectOptionProps[]; - classes?: string[]; + className?: string; group?: boolean; optionPicker?: boolean; defaultSelectedOptionId?: string; onClick?: (option: IFlatSelectOptionProps) => void; + disabled?: boolean; + classes?: string[] /* @deprecated use className instead */; } export interface IFlatSelectStateProps { @@ -54,7 +56,7 @@ export class FlatSelect extends React.Component { this.props.selectedOptionId && this.props.selectedOptionId === flatSelectOption.id; flatSelectOption.onClick = (option: IFlatSelectOptionProps) => this.handleOnOptionClick(option); - return ; + return ; }); } @@ -65,7 +67,8 @@ export class FlatSelect extends React.Component { 'mod-btn-group': this.props.group, 'mod-option-picker': this.props.optionPicker, }, - this.props.classes + this.props.classes, + this.props.className ); return
{this.getOptions()}
; diff --git a/packages/react-vapor/src/components/flatSelect/FlatSelectConnected.tsx b/packages/react-vapor/src/components/flatSelect/FlatSelectConnected.tsx index 824943da06..eb2e0f2fef 100644 --- a/packages/react-vapor/src/components/flatSelect/FlatSelectConnected.tsx +++ b/packages/react-vapor/src/components/flatSelect/FlatSelectConnected.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import {connect} from 'react-redux'; -import * as _ from 'underscore'; import {IReactVaporState} from '../../ReactVapor'; import {IDispatch, ReduxUtils} from '../../utils/ReduxUtils'; import { diff --git a/packages/react-vapor/src/components/flatSelect/FlatSelectOption.tsx b/packages/react-vapor/src/components/flatSelect/FlatSelectOption.tsx index 92303d0f26..3a65a9af67 100644 --- a/packages/react-vapor/src/components/flatSelect/FlatSelectOption.tsx +++ b/packages/react-vapor/src/components/flatSelect/FlatSelectOption.tsx @@ -12,6 +12,7 @@ export interface IFlatSelectOptionProps { tooltip?: ITooltipProps; selected?: boolean; onClick?: (option: IFlatSelectOptionProps) => void; + disabled?: boolean; } export class FlatSelectOption extends React.Component { @@ -30,6 +31,7 @@ export class FlatSelectOption extends React.Component = ({ + prepend, + prependClassName = '', + ...flatSelectProps +}) => ( +
+
{prepend}
+ +
+); diff --git a/packages/react-vapor/src/components/index.ts b/packages/react-vapor/src/components/index.ts index 86f36fa52e..78b16f33fa 100644 --- a/packages/react-vapor/src/components/index.ts +++ b/packages/react-vapor/src/components/index.ts @@ -76,3 +76,4 @@ export * from './title'; export * from './toast'; export * from './tooltip'; export * from './userFeedback'; +export * from './pagination'; diff --git a/packages/react-vapor/src/components/loading/components/ContentLoadingPlaceholder.tsx b/packages/react-vapor/src/components/loading/components/ContentLoadingPlaceholder.tsx new file mode 100644 index 0000000000..0e9a55aa1e --- /dev/null +++ b/packages/react-vapor/src/components/loading/components/ContentLoadingPlaceholder.tsx @@ -0,0 +1,11 @@ +import * as classNames from 'classnames'; +import * as React from 'react'; + +export interface IContentLoadingPlaceholder { + className?: string; +} + +export const ContentLoadingPlaceholder: React.FunctionComponent = ({ + className = '', + children, +}) =>
{children}
; diff --git a/packages/react-vapor/src/components/loading/components/PaginationLoading.tsx b/packages/react-vapor/src/components/loading/components/PaginationLoading.tsx index 76e8d101c8..43705cc5e9 100644 --- a/packages/react-vapor/src/components/loading/components/PaginationLoading.tsx +++ b/packages/react-vapor/src/components/loading/components/PaginationLoading.tsx @@ -1,41 +1,46 @@ import * as React from 'react'; +import {ContentLoadingPlaceholder} from './ContentLoadingPlaceholder'; +import {TextLoadingPlaceholder} from './TextLoadingPlaceholder'; export const PaginationLoading = () => (
-
-
+ + -
-
+ + -
-
+ + -
+ + + +
-
-
+ + -
-
+ + -
-
+ + -
-
+ + -
-
+ + -
-
+ +
diff --git a/packages/react-vapor/src/components/loading/components/TableLoading.tsx b/packages/react-vapor/src/components/loading/components/TableLoading.tsx index 6593b196b2..dd13346aa0 100644 --- a/packages/react-vapor/src/components/loading/components/TableLoading.tsx +++ b/packages/react-vapor/src/components/loading/components/TableLoading.tsx @@ -1,32 +1,37 @@ +import * as classNames from 'classnames'; import * as React from 'react'; import * as _ from 'underscore'; -export const TableLoading = ({ - numberOfColumn = 4, - numberOfRow = 10, -}: { - numberOfColumn?: number; - numberOfRow?: number; -}) => { +const Table = ({numberOfColumns = 4, numberOfRow = 10}: {numberOfColumns?: number; numberOfRow?: number}) => { return ( <> - - {_.times(numberOfRow, () => ( - - {_.times(numberOfColumn, () => ( - - ))} - - ))} - +
); }; -const TableRowLoading = () => ( +const Body = ({numberOfColumns = 4, numberOfRow = 10}: {numberOfColumns?: number; numberOfRow?: number}) => ( + + {_.times(numberOfRow, (nColumn: number) => ( + + {_.times(numberOfColumns, (nRow: number) => ( + + ))} + + ))} + +); + +const Row = ({num}: {num: number}) => ( -
+
); + +export const TableLoading = { + Table, + Body, + Row, +}; diff --git a/packages/react-vapor/src/components/loading/components/TextLoadingPlaceholder.tsx b/packages/react-vapor/src/components/loading/components/TextLoadingPlaceholder.tsx new file mode 100644 index 0000000000..4178b26df5 --- /dev/null +++ b/packages/react-vapor/src/components/loading/components/TextLoadingPlaceholder.tsx @@ -0,0 +1,23 @@ +import * as classNames from 'classnames'; +import * as React from 'react'; + +export interface ITextLoadingPlaceholder { + small?: boolean; + word?: boolean; + large?: boolean; + className?: string; +} + +export const TextLoadingPlaceholder = ({small, word, large, className = ''}: ITextLoadingPlaceholder) => ( +
+); diff --git a/packages/react-vapor/src/components/loading/components/tests/ActionBarLoading.spec.tsx b/packages/react-vapor/src/components/loading/components/tests/ActionBarLoading.spec.tsx new file mode 100644 index 0000000000..e099cfe6a5 --- /dev/null +++ b/packages/react-vapor/src/components/loading/components/tests/ActionBarLoading.spec.tsx @@ -0,0 +1,14 @@ +import {shallow} from 'enzyme'; +import * as React from 'react'; +import {ActionBarLoading} from '../ActionBarLoading'; + +describe('ActionBarLoading tests', () => { + describe('', () => { + it('should mount and unmount without errors', () => { + expect(() => { + const wrapper = shallow(, {}); + wrapper.unmount(); + }); + }); + }); +}); diff --git a/packages/react-vapor/src/components/loading/components/tests/BasicHeaderLoading.spec.tsx b/packages/react-vapor/src/components/loading/components/tests/BasicHeaderLoading.spec.tsx new file mode 100644 index 0000000000..97a1026054 --- /dev/null +++ b/packages/react-vapor/src/components/loading/components/tests/BasicHeaderLoading.spec.tsx @@ -0,0 +1,14 @@ +import {shallow} from 'enzyme'; +import * as React from 'react'; +import {BasicHeaderLoading} from '../BasicHeaderLoading'; + +describe('BasicHeaderLoading tests', () => { + describe('', () => { + it('should mount and unmount without errors', () => { + expect(() => { + const wrapper = shallow(, {}); + wrapper.unmount(); + }); + }); + }); +}); diff --git a/packages/react-vapor/src/components/loading/components/tests/ContentLoadingPlaceholder.spec.tsx b/packages/react-vapor/src/components/loading/components/tests/ContentLoadingPlaceholder.spec.tsx new file mode 100644 index 0000000000..f61f4de59e --- /dev/null +++ b/packages/react-vapor/src/components/loading/components/tests/ContentLoadingPlaceholder.spec.tsx @@ -0,0 +1,19 @@ +import {shallow} from 'enzyme'; +import * as React from 'react'; +import {ContentLoadingPlaceholder} from '../ContentLoadingPlaceholder'; + +describe('ContentLoadingPlaceholder tests', () => { + describe('', () => { + it('should mount and unmount without errors', () => { + expect(() => { + const wrapper = shallow( + +
Test
+
, + {} + ); + wrapper.unmount(); + }); + }); + }); +}); diff --git a/packages/react-vapor/src/components/loading/components/tests/PaginationLoading.spec.tsx b/packages/react-vapor/src/components/loading/components/tests/PaginationLoading.spec.tsx new file mode 100644 index 0000000000..166d9ba243 --- /dev/null +++ b/packages/react-vapor/src/components/loading/components/tests/PaginationLoading.spec.tsx @@ -0,0 +1,14 @@ +import {shallow} from 'enzyme'; +import * as React from 'react'; +import {PaginationLoading} from '../PaginationLoading'; + +describe('PaginationLoading tests', () => { + describe('', () => { + it('should mount and unmount without errors', () => { + expect(() => { + const wrapper = shallow(, {}); + wrapper.unmount(); + }); + }); + }); +}); diff --git a/packages/react-vapor/src/components/loading/components/tests/TableLoading.spec.tsx b/packages/react-vapor/src/components/loading/components/tests/TableLoading.spec.tsx new file mode 100644 index 0000000000..cc039e8dad --- /dev/null +++ b/packages/react-vapor/src/components/loading/components/tests/TableLoading.spec.tsx @@ -0,0 +1,52 @@ +import {shallow} from 'enzyme'; +import * as React from 'react'; +import {TableLoading} from '../TableLoading'; + +describe('TableLoading tests', () => { + describe('', () => { + it('should mount and unmount without errors', () => { + expect(() => { + const wrapper = shallow(, {}); + wrapper.unmount(); + }); + }); + }); + + describe('', () => { + it('should mount and unmount without errors', () => { + expect(() => { + const wrapper = shallow(, {}); + wrapper.unmount(); + }); + }); + + it('should render equal of the the number of columns sent as parameter', () => { + const wrapper = shallow(, {}); + expect(wrapper.find('tr').length).toBe(10); + }); + + it('should render equal of the the number of columns sent as parameter', () => { + const wrapper = shallow(, {}); + expect(wrapper.find(TableLoading.Row).length).toBe(8); + }); + }); + + describe('', () => { + it('should mount and unmount without errors', () => { + expect(() => { + const wrapper = shallow(, {}); + wrapper.unmount(); + }); + }); + + it('should add the class mod-haft if the number is odd', () => { + const wrapper = shallow(, {}); + expect(wrapper.find('div').hasClass('mod-half')).toBe(true); + }); + + it('should not add the class mod-haft if the number is even', () => { + const wrapper = shallow(, {}); + expect(wrapper.find('div').hasClass('mod-half')).toBe(false); + }); + }); +}); diff --git a/packages/react-vapor/src/components/loading/components/tests/TextLoadingPlaceholder.spec.tsx b/packages/react-vapor/src/components/loading/components/tests/TextLoadingPlaceholder.spec.tsx new file mode 100644 index 0000000000..a3d227f4b4 --- /dev/null +++ b/packages/react-vapor/src/components/loading/components/tests/TextLoadingPlaceholder.spec.tsx @@ -0,0 +1,36 @@ +import {shallow} from 'enzyme'; +import * as React from 'react'; +import {TextLoadingPlaceholder} from '../TextLoadingPlaceholder'; + +describe('TextLoadingPlaceholder tests', () => { + describe('', () => { + it('should mount and unmount without errors', () => { + expect(() => { + const wrapper = shallow(, {}); + wrapper.unmount(); + }); + }); + + it('should not have class by default', () => { + const wrapper = shallow(, {}); + expect(wrapper.hasClass('mod-small')).toBe(false); + expect(wrapper.hasClass('mod-word')).toBe(false); + expect(wrapper.hasClass('mod-large')).toBe(false); + }); + + it('should add mod-small with small as prop', () => { + const wrapper = shallow(, {}); + expect(wrapper.hasClass('mod-small')).toBe(true); + }); + + it('should add mod-word with word as prop', () => { + const wrapper = shallow(, {}); + expect(wrapper.hasClass('mod-word')).toBe(true); + }); + + it('should add mod-large with large as prop', () => { + const wrapper = shallow(, {}); + expect(wrapper.hasClass('mod-large')).toBe(true); + }); + }); +}); diff --git a/packages/react-vapor/src/components/loading/examples/LoadingExamples.tsx b/packages/react-vapor/src/components/loading/examples/LoadingExamples.tsx index e8c159d4c3..c240ff51d3 100644 --- a/packages/react-vapor/src/components/loading/examples/LoadingExamples.tsx +++ b/packages/react-vapor/src/components/loading/examples/LoadingExamples.tsx @@ -29,10 +29,10 @@ export const LoadingExamples = () => (
- +
- +
diff --git a/packages/react-vapor/src/components/loading/index.ts b/packages/react-vapor/src/components/loading/index.ts index 30b01d3a4f..9506250970 100644 --- a/packages/react-vapor/src/components/loading/index.ts +++ b/packages/react-vapor/src/components/loading/index.ts @@ -2,3 +2,9 @@ export * from './Loading'; export * from './LoadingActions'; export * from './LoadingConnected'; export * from './LoadingReducers'; +export * from './components/ActionBarLoading'; +export * from './components/BasicHeaderLoading'; +export * from './components/ContentLoadingPlaceholder'; +export * from './components/PaginationLoading'; +export * from './components/TableLoading'; +export * from './components/TextLoadingPlaceholder'; diff --git a/packages/react-vapor/src/components/navigation/pagination/NavigationPaginationActions.ts b/packages/react-vapor/src/components/navigation/pagination/NavigationPaginationActions.ts index 0a4b9a27c1..168d374900 100644 --- a/packages/react-vapor/src/components/navigation/pagination/NavigationPaginationActions.ts +++ b/packages/react-vapor/src/components/navigation/pagination/NavigationPaginationActions.ts @@ -44,3 +44,10 @@ export const resetPaging = (id: string): IReduxAction { + disabled?: boolean; selected: boolean; pageNb: number; onPageClick: (pageNb: number) => void; @@ -8,7 +10,10 @@ export interface INavigationPaginationSelectProps extends React.ClassAttributes< export class NavigationPaginationSelect extends React.Component { render() { - const linkClasses: string = 'flat-select-option' + (this.props.selected ? '' : ' selectable'); + const linkClasses: string = classNames('flat-select-option', { + selectable: !this.props.selected, + disabled: this.props.disabled, + }); return ( ({ + currentPage: PaginationSelectors.getPaginationPageNumber(state, {id: ownProps.id}), +}); + +const mapDispatchToProps = ( + dispatch: (action: IReduxAction) => void, + ownProps: IPaginationPagesNumberOwnProps +) => ({ + onRender: () => dispatch(PaginationReduxActions.addPagination(ownProps.id)), + onDestroy: () => dispatch(PaginationReduxActions.removePagination(ownProps.id)), + onPageClick: (pageNb: number) => dispatch(PaginationReduxActions.changePage(ownProps.id, pageNb)), +}); + +export interface IPaginationPagesNumberProps + extends IPaginationPagesNumberOwnProps, + ReturnType, + ReturnType {} + +const NUMBER_OF_PAGES_SHOWING: number = 7; +const PREVIOUS_LABEL: string = 'Previous'; +const NEXT_LABEL: string = 'Next'; + +class PaginationPagesNumberDisconnected extends React.Component { + private handlePageClick = (pageNb: number) => { + if (pageNb >= 0 && this.props.currentPage !== pageNb) { + this.props.onPageClick?.(pageNb); + } + }; + + componentDidUpdate() { + if (this.props.currentPage > this.props.totalPages - 1) { + this.handlePageClick(this.props.totalPages - 1); + } + } + + componentDidMount() { + this.props.onRender?.(); + } + + componentWillUnmount() { + this.props.onDestroy?.(); + } + + render() { + const currentPage: number = this.props.currentPage || 0; + + const showXPages: number = Math.abs((this.props.numberOfPagesToShow || NUMBER_OF_PAGES_SHOWING) - 1); + const previousLabel: string = this.props.previousLabel || PREVIOUS_LABEL; + const nextLabel: string = this.props.nextLabel || NEXT_LABEL; + let start: number = 0; + let end: number = showXPages; + const lastPage: number = this.props.totalPages - 1; + const previousClasses: string = classNames('flat-select-option mod-link', { + disabled: currentPage === 0 || this.props.disabled, + selectable: currentPage !== 0, + hidden: this.props.hidden, + }); + const nextClasses: string = classNames('flat-select-option mod-link', { + disabled: currentPage === lastPage || this.props.disabled, + selectable: currentPage !== lastPage, + hidden: this.props.hidden, + }); + + const pageSelects: JSX.Element[] = []; + + if (!this.props.hidden) { + if (currentPage + showXPages / 2 > lastPage) { + end = lastPage; + start = Math.max(lastPage - showXPages, 0); + } else { + start = Math.max(Math.floor(currentPage - showXPages / 2), 0); + end = Math.min(start + showXPages, lastPage); + } + + _.each(_.range(start, end + 1), (nbr: number): void => { + pageSelects.push( + + ); + }); + } + + return ( + + ); + } +} + +export const PaginationPagesNumber = connect(mapStateToProps, mapDispatchToProps)(PaginationPagesNumberDisconnected); diff --git a/packages/react-vapor/src/components/pagination/PaginationPerPage.tsx b/packages/react-vapor/src/components/pagination/PaginationPerPage.tsx new file mode 100644 index 0000000000..092c636109 --- /dev/null +++ b/packages/react-vapor/src/components/pagination/PaginationPerPage.tsx @@ -0,0 +1,38 @@ +import * as classNames from 'classnames'; +import * as React from 'react'; +import * as _ from 'underscore'; +import {UrlUtils} from '../../utils/UrlUtils'; +import {FlatSelectWithPrepend} from '../flatSelect/FlatSelectWithPrepend'; +import {Params} from '../table-hoc/TableWithUrlState'; +import {PaginationUtils} from './PaginationUtils'; +import {TablePaginationDefaultValue} from './TablePagination'; + +export interface IPaginationPerPageProps { + id: string; + perPage?: number[]; + defaultPerPageSelected?: number; + label?: React.ReactNode; + disabled?: boolean; + hidden?: boolean; +} + +export const PaginationPerPage = ({ + id, + perPage = TablePaginationDefaultValue.PerPage, + defaultPerPageSelected = UrlUtils.getSearchParams?.()?.[Params.pageSize], + label = TablePaginationDefaultValue.perPageLabel, + disabled, + hidden, +}: IPaginationPerPageProps) => ( + ({ + id: nb?.toString(), + option: {content: nb}, + }))} + defaultSelectedOptionId={defaultPerPageSelected?.toString()} + prependClassName={classNames('items-per-page', {hidden: hidden})} + prepend={label} + disabled={disabled} + /> +); diff --git a/packages/react-vapor/src/components/pagination/PaginationSelect.tsx b/packages/react-vapor/src/components/pagination/PaginationSelect.tsx new file mode 100644 index 0000000000..e33de1e885 --- /dev/null +++ b/packages/react-vapor/src/components/pagination/PaginationSelect.tsx @@ -0,0 +1,35 @@ +import * as classNames from 'classnames'; +import {FunctionComponent} from 'react'; +import * as React from 'react'; + +export interface IPaginationSelectProps extends React.HTMLAttributes { + disabled?: boolean; + selected: boolean; + pageNb: number; + onPageClick: (pageNb: number) => void; +} + +export const PaginationSelect: FunctionComponent = ({ + disabled, + selected, + pageNb, + onPageClick, + className, + ...linkProps +}) => ( + onPageClick(pageNb)} + > + {pageNb + 1} + +); diff --git a/packages/react-vapor/src/components/pagination/PaginationSelectors.tsx b/packages/react-vapor/src/components/pagination/PaginationSelectors.tsx new file mode 100644 index 0000000000..621790c9f9 --- /dev/null +++ b/packages/react-vapor/src/components/pagination/PaginationSelectors.tsx @@ -0,0 +1,17 @@ +import {createSelector} from 'reselect'; +import * as _ from 'underscore'; +import {IReactVaporState} from '../../ReactVapor'; +import {IPaginationState} from '../navigation/pagination'; + +const getPaginationState = (state: IReactVaporState, {id}: {id: string}) => + _.findWhere(state.paginationComposite, {id: id}); + +const getPaginationPageNumber = createSelector( + getPaginationState, + (paginationState: IPaginationState): number => paginationState?.pageNb ?? 0 +); + +export const PaginationSelectors = { + getPaginationState, + getPaginationPageNumber, +}; diff --git a/packages/react-vapor/src/components/pagination/PaginationUtils.tsx b/packages/react-vapor/src/components/pagination/PaginationUtils.tsx new file mode 100644 index 0000000000..5be28473ea --- /dev/null +++ b/packages/react-vapor/src/components/pagination/PaginationUtils.tsx @@ -0,0 +1,7 @@ +const getPaginationPagesNumbersId = (id: string) => `${id}_PagesNumber`; +const getPaginationPerPageId = (id: string) => `${id}_PerPage`; + +export const PaginationUtils = { + getPaginationPagesNumbersId, + getPaginationPerPageId, +}; diff --git a/packages/react-vapor/src/components/pagination/TablePagination.tsx b/packages/react-vapor/src/components/pagination/TablePagination.tsx new file mode 100644 index 0000000000..c1ca25d196 --- /dev/null +++ b/packages/react-vapor/src/components/pagination/TablePagination.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import {TableHOCUtils} from '../table-hoc/utils/TableHOCUtils'; +import {PaginationPagesNumber} from './PaginationPagesNumber'; +import {PaginationPerPage} from './PaginationPerPage'; + +export interface ITablePaginationProps { + id: string; + totalPages: number; + perPageNumbers: number[]; + defaultPerPageSelected: number; + totalEntries: number; + disabled?: boolean; +} + +export const TablePagination = ({ + id, + disabled = false, + totalPages, + perPageNumbers, + defaultPerPageSelected, + totalEntries, +}: ITablePaginationProps) => ( +
+
}} - > - - {data.city} - {data.email.toLowerCase()} - {data.username.toLowerCase()} - {data.dateOfBirth.toLocaleDateString()} - - )); - const renderHeader = () => ( - - - - - City - - - Email - - - Username - - Date of Birth - {/* Empty th for the collapsible */} - - + + {({isLoading}) => ( + + + + + City + + + Email + + + Username + + Date of Birth + + + + )} + ); -const tablePredicates = [ - { - id: 'address.city', - prepend: City:, - values: [ - {displayValue: 'All', value: '', selected: true}, - {displayValue: 'Lebsackbury', value: 'Lebsackbury'}, - ], - }, - { - id: 'username', - prepend: Username:, - values: [ - {displayValue: 'All', value: '', selected: true}, - {displayValue: 'bret', value: 'Bret'}, - ], - }, -]; - -const tableDatePickerConfig = { - datesSelectionBoxes: SELECTION_BOXES_LONG, - years: [...DateUtils.getPreviousYears(25), DateUtils.currentYear.toString()], - initialDateRange: [ - moment() - .subtract(25, 'years') - .toDate(), - moment().toDate(), - ], -}; - -const mapStateToProps = (state: any) => ({ - isLoading: state.tableHOCExample.isLoading, - serverData: state.tableHOCExample.data, -}); const mapDispatchToProps = (dispatch: IDispatch) => ({ - fetch: _.debounce(() => dispatch(TableHOCServerActions.fetchData()), 400), + fetch: () => dispatch(TableHOCServerActions.fetchData()), + resetFilter: () => dispatch(filterThrough(TableHOCServerExampleId, '')), }); -const TableExampleDisconnected: React.FunctionComponent = (props) => { - const onUpdate = () => { - props.fetch(); +class TableExampleDisconnected extends React.PureComponent { + state: TableHOCServerExamplesState = { + data: null, + isLoading: true, }; - const updateUrl = (query: string) => { - props.history.push({search: query}); + private ServerTableComposed = _.compose( + withServerSideProcessing, + tableWithUrlState, + tableWithBlankSlate({ + title: 'No data fetched from the server', + description: 'Try reviewing the specified filters above or clearing all filters.', + buttons: [ + { + name: 'Clear filter', + enabled: true, + onClick: () => this.props.resetFilter(), + }, + ], + }), + tableWithPredicate(TableHOCExampleUtils.tablePredicates[0]), + tableWithPredicate(TableHOCExampleUtils.tablePredicates[1]), + tableWithFilter({ + placeholder: 'Filter all', + blankSlate: { + title: 'No results found', + }, + }), + tableWithSort(), + tableWithDatePicker({...(TableHOCExampleUtils.tableDatePickerConfig as any)}), + tableWithNewPagination({perPageNumbers: [3, 5, 10]}), + tableWithActions() + )(TableHOC); + + private fetch = _.debounce(() => { + this.setState({...this.state, isLoading: true}); + window.setTimeout( + () => + this.props.fetch().done((data: any) => { + this.setState({data, isLoading: false}); + }), + 500 + ); + }, 40); + + private onUpdate = () => { + this.fetch(); }; - return ( -
- - Please note that the backend service doesn't support dates but we still make a request for every change - in the date range. - - - - -
- ); -}; - -const ServerTableComposed = _.compose( - withServerSideProcessing, - tableWithUrlState, - tableWithBlankSlate({title: 'No data fetched from the server'}), - tableWithPredicate({...tablePredicates[0]}), - tableWithPredicate({...tablePredicates[1]}), - tableWithFilter(), - tableWithDatePicker({...(tableDatePickerConfig as any)}), - tableWithSort(), - tableWithPagination({perPageNumbers: [3, 5, 10]}), - tableWithActions() -)(TableHOC); - -const TableHOCServer = connect( - mapStateToProps, - - mapDispatchToProps -)(withRouter(TableExampleDisconnected)); - -/* ACTIONS */ + private updateUrl = (query: string) => { + this.props.history.push({search: query}); + }; -export const TableHOCServerActionsType = { - setData: 'TABLE_HOC_SET_DATA', - setIsLoading: 'TABLE_HOC_SET_IS_LOADING', - fetch: 'TABLE_HOC_FETCH_DATA', -}; + componentDidMount() { + this.fetch(); + } -const setData = (data: any[]): IReduxAction => ({ - type: TableHOCServerActionsType.setData, - payload: {data}, -}); + render() { + return ( +
+ + Please note that the backend service doesn't support dates but we still make a request for every + change in the date range. + + + + + + +
+ ); + } +} -const setIsLoading = (isLoading: boolean): IReduxAction => ({ - type: TableHOCServerActionsType.setIsLoading, - payload: {isLoading}, -}); +const TableHOCServer = connect(undefined, mapDispatchToProps)(withRouter(TableExampleDisconnected)); -const fetchData = (): IThunkAction => (dispatch: IDispatch, getState) => { +const fetchData = (): IThunkAction => (dispatch: IDispatch, getState: () => IReactVaporTestState) => { const compositeState: ITableHOCCompositeState = TableHOCUtils.getCompositeState( TableHOCServerExampleId, getState() @@ -237,10 +193,9 @@ const fetchData = (): IThunkAction => (dispatch: IDispatch, getState) => { _.each(compositeState.predicates, (predicate: {id: string; value: string}) => { params[predicate.id] = predicate.value; }); - dispatch(setIsLoading(true)); - $.get('https://jsonplaceholder.typicode.com/users', params).done((response: any[], status, request) => { + return $.get('https://jsonplaceholder.typicode.com/users', params).then((response: any[], status, request) => { const count = request.getResponseHeader('x-total-count'); - const users = response.map((user: any) => ({ + const users = _.map(response, (user: any) => ({ city: user.address.city, username: user.username, email: user.email, @@ -248,38 +203,14 @@ const fetchData = (): IThunkAction => (dispatch: IDispatch, getState) => { .subtract(user.address.city.length, 'years') .toDate(), // fake a year of birth })); - dispatch(setData(users)); - dispatch(turnOffLoading([TableHOCServerExampleId])); - dispatch(TableWithPaginationActions.setCount(TableHOCServerExampleId, count)); + dispatch(TableWithPaginationActions.setCount(TableHOCServerExampleId, count as any)); + return { + count, + users, + }; }); }; -export const TableHOCServerActions = { - setData, - setIsLoading, +const TableHOCServerActions = { fetchData, }; - -/* REDUCER */ - -export const TableHOCServerExampleReducer = ( - state: IExampleServerTableState = {data: [], isLoading: true}, - action: IReduxAction -) => { - if (action.type === TableHOCServerActionsType.setData) { - const payload = action.payload as ISetExampleDataPayload; - return { - ...state, - data: [...payload.data], - isLoading: false, - }; - } - if (action.type === TableHOCServerActionsType.setIsLoading) { - const payload = action.payload as ISetExampleIsLoadingPayload; - return { - ...state, - isLoading: payload.isLoading, - }; - } - return state; -}; diff --git a/packages/react-vapor/src/components/table-hoc/examples/TableHOCwithBlankSlateExample.tsx b/packages/react-vapor/src/components/table-hoc/examples/TableHOCwithBlankSlateExamples.tsx similarity index 58% rename from packages/react-vapor/src/components/table-hoc/examples/TableHOCwithBlankSlateExample.tsx rename to packages/react-vapor/src/components/table-hoc/examples/TableHOCwithBlankSlateExamples.tsx index 4b330bc08f..1208948d1f 100644 --- a/packages/react-vapor/src/components/table-hoc/examples/TableHOCwithBlankSlateExample.tsx +++ b/packages/react-vapor/src/components/table-hoc/examples/TableHOCwithBlankSlateExamples.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import {connect} from 'react-redux'; import * as _ from 'underscore'; import {ExampleComponent} from '../../../../docs/src/components/ComponentsInterface'; import {Section} from '../../section/Section'; @@ -8,12 +7,12 @@ import {tableWithBlankSlate} from '../TableWithBlankSlate'; import {tableWithFilter} from '../TableWithFilter'; import {generateDataWithFacker, generateTableRow} from './TableHOCExamples'; -export const TableHocWithBlankSlateExample: ExampleComponent = () => ( +export const TableHOCwithBlankSlateExamples: ExampleComponent = () => (
); -TableHocWithBlankSlateExample.title = 'TableHOC blankSlate'; +TableHOCwithBlankSlateExamples.title = 'TableHOC blankSlate'; export interface IExampleRowData { city: string; @@ -24,25 +23,17 @@ export interface IExampleRowData { } // start-print -const mapStateToProps = () => ({ - data: generateDataWithFacker(0), -}); -const TableWithBlankSlateExampleDisconnected: React.FunctionComponent = () => { - const tableId = 'tableWithBlankSlate'; - - return ( -
- -
- ); -}; -const TableWithBlankSlateExample = connect(mapStateToProps)(TableWithBlankSlateExampleDisconnected); +const TableWithBlankSlateExample: React.FunctionComponent = () => ( +
+ +
+); const TableWithBlankSlateComposed = _.compose( tableWithBlankSlate({title: 'No data caused the table to be empty'}), diff --git a/packages/react-vapor/src/components/table-hoc/index.ts b/packages/react-vapor/src/components/table-hoc/index.ts index 265679d24e..84fdef5525 100644 --- a/packages/react-vapor/src/components/table-hoc/index.ts +++ b/packages/react-vapor/src/components/table-hoc/index.ts @@ -1,5 +1,5 @@ export * from './TableHOC'; -export * from './TableHOCUtils'; +export * from './utils/TableHOCUtils'; export * from './TableHeaderWithSort'; export * from './TableRowConnected'; export * from './TableRowNumberColumn'; @@ -10,6 +10,7 @@ export * from './TableWithBlankSlate'; export * from './TableWithDatePicker'; export * from './TableWithFilter'; export * from './TableWithPagination'; +export * from './TableWithNewPagination'; export * from './TableWithPredicate'; export * from './TableWithSort'; export * from './TableWithUrlState'; diff --git a/packages/react-vapor/src/components/table-hoc/reducers/TableRowReducers.ts b/packages/react-vapor/src/components/table-hoc/reducers/TableRowReducers.ts index d6a12e9fbe..1b4b8ea9a3 100644 --- a/packages/react-vapor/src/components/table-hoc/reducers/TableRowReducers.ts +++ b/packages/react-vapor/src/components/table-hoc/reducers/TableRowReducers.ts @@ -9,7 +9,7 @@ import { ITableRowToggleCollapsiblePayload, TableHOCRowActionsType, } from '../actions/TableHOCRowActions'; -import {TableHOCUtils} from '../TableHOCUtils'; +import {TableHOCUtils} from '../utils/TableHOCUtils'; export interface HOCTableRowState { id: string; diff --git a/packages/react-vapor/src/components/table-hoc/tests/TableHOC.spec.tsx b/packages/react-vapor/src/components/table-hoc/tests/TableHOC.spec.tsx index 794e1821be..ce4ec81fe0 100644 --- a/packages/react-vapor/src/components/table-hoc/tests/TableHOC.spec.tsx +++ b/packages/react-vapor/src/components/table-hoc/tests/TableHOC.spec.tsx @@ -4,6 +4,7 @@ import * as _ from 'underscore'; import {ActionBarConnected} from '../../actions/ActionBar'; import {FilterBoxConnected} from '../../filterBox/FilterBoxConnected'; +import {TableLoading} from '../../loading/components/TableLoading'; import {ITableHOCOwnProps, TableHOC} from '../TableHOC'; describe('TableHOC', () => { @@ -55,14 +56,16 @@ describe('TableHOC', () => { expect(wrapper.find('.table-container').hasClass(expectedClass)).toBe(true); }); - it('should add the loading-component class on the table if isLoading is true', () => { - const wrapper = shallow(); - expect(wrapper.find('.table-container').hasClass('loading-component')).toBe(false); + it('should add the TableLoading.Body Component instead of the tbody if isLoading is true', () => { + const wrapper = shallow(); + + expect(wrapper.find(TableLoading.Body).length).toBe(1); + }); - wrapper.setProps({isLoading: true}); - wrapper.update(); + it('should not render the TableLoading.Body Component if isLoading is false', () => { + const wrapper = shallow(); - expect(wrapper.find('.table-container').hasClass('loading-component')).toBe(true); + expect(wrapper.find(TableLoading.Body).length).toBe(0); }); it('should not render an ActionBarConnected if the table prop hasActionButtons is false and the table have no actions', () => { diff --git a/packages/react-vapor/src/components/table-hoc/tests/TableHOCUtils.spec.ts b/packages/react-vapor/src/components/table-hoc/tests/TableHOCUtils.spec.ts index 9349148946..df7b1d2d90 100644 --- a/packages/react-vapor/src/components/table-hoc/tests/TableHOCUtils.spec.ts +++ b/packages/react-vapor/src/components/table-hoc/tests/TableHOCUtils.spec.ts @@ -1,4 +1,5 @@ -import {TableHOCUtils} from '../TableHOCUtils'; +import {PaginationUtils} from '../../pagination/PaginationUtils'; +import {TableHOCUtils} from '../utils/TableHOCUtils'; describe('TableHOCUtils', () => { const defaultProps = {id: 'some-id', componentId: 'some-componentId', tableId: 'some-tableId'}; @@ -55,9 +56,9 @@ describe('TableHOCUtils', () => { const defaultState: any = { tableHOCHeader: [{id: defaultProps.id, tableId: defaultProps.tableId, isAsc: true}], paginationComposite: [{id: `pagination-${defaultProps.tableId}`, pageNb: 2}], - perPageComposite: [{id: defaultProps.tableId, perPage: 10}], filters: [{id: defaultProps.tableId, filterText: 'some-text'}], listBoxes: [{id: predicateId, selected: [predicateValue]}], + flatSelect: [{id: PaginationUtils.getPaginationPerPageId(defaultProps.tableId), selectedOptionId: '10'}], }; it('should return composite state', () => { diff --git a/packages/react-vapor/src/components/table-hoc/tests/TableHeaderWithSort.spec.tsx b/packages/react-vapor/src/components/table-hoc/tests/TableHeaderWithSort.spec.tsx index 8e7f2feda8..01eebb8bb4 100644 --- a/packages/react-vapor/src/components/table-hoc/tests/TableHeaderWithSort.spec.tsx +++ b/packages/react-vapor/src/components/table-hoc/tests/TableHeaderWithSort.spec.tsx @@ -1,7 +1,8 @@ -import {shallowWithStore} from 'enzyme-redux'; +import {shallowWithState, shallowWithStore} from 'enzyme-redux'; import * as React from 'react'; import {getStoreMock, ReactVaporMockStore} from '../../../utils/tests/TestUtils'; +import {TextLoadingPlaceholder} from '../../loading'; import {TableHeaderActions} from '../actions/TableHeaderActions'; import {TableHeaderWithSort} from '../TableHeaderWithSort'; @@ -73,5 +74,11 @@ describe('Table HOC', () => { expect(store.getActions()).toContain(expectedAction); }); + + it('should render a if the prop isLoading is set to true', () => { + const wrapper = shallowWithState(, store).dive(); + + expect(wrapper.find(TextLoadingPlaceholder).length).toBe(1); + }); }); }); diff --git a/packages/react-vapor/src/components/table-hoc/tests/TableRowHeader.spec.tsx b/packages/react-vapor/src/components/table-hoc/tests/TableRowHeader.spec.tsx new file mode 100644 index 0000000000..08aa9c8b46 --- /dev/null +++ b/packages/react-vapor/src/components/table-hoc/tests/TableRowHeader.spec.tsx @@ -0,0 +1,27 @@ +import {shallowWithState} from 'enzyme-redux'; +import * as React from 'react'; +import {TextLoadingPlaceholder} from '../../loading'; +import {TableRowHeader} from '../TableRowHeader'; + +describe('TableRowHeader', () => { + describe(' tests', () => { + it('should mount and unmount without errors', () => { + expect(() => { + const wrapper = shallowWithState( + + + , + {} + ); + wrapper.unmount(); + }); + }); + + describe('once mounted', () => { + it('should render a if loading', () => { + const wrapper = shallowWithState(, {}); + expect(wrapper.find(TextLoadingPlaceholder).length).toBe(1); + }); + }); + }); +}); diff --git a/packages/react-vapor/src/components/table-hoc/tests/TableWithBlankslate.spec.tsx b/packages/react-vapor/src/components/table-hoc/tests/TableWithBlankslate.spec.tsx index 9292b05c12..37e7e6fcc5 100644 --- a/packages/react-vapor/src/components/table-hoc/tests/TableWithBlankslate.spec.tsx +++ b/packages/react-vapor/src/components/table-hoc/tests/TableWithBlankslate.spec.tsx @@ -1,7 +1,6 @@ import {shallowWithState} from 'enzyme-redux'; import * as React from 'react'; import * as _ from 'underscore'; -import {BlankSlate} from '../../blankSlate/BlankSlate'; import {ITableHOCProps, TableHOC} from '../TableHOC'; import {tableWithBlankSlate} from '../TableWithBlankSlate'; @@ -43,11 +42,11 @@ describe('Table HOC', () => { expect(renderSpy).not.toHaveBeenCalled(); }); - it('should override the TableHOC renderBody method to return null when the data is empty', () => { - const wrapper = shallowWithProps().dive(); - const tableRenderBody = wrapper.find(TableHOC).prop('renderBody') as () => any; + it('should override the TableHOC renderBody method to return a BlankSlateWithTable when the data is empty', () => { + const wrapper = shallowWithProps({renderBody: renderSpy}).dive(); + wrapper.find(TableHOC).prop('renderBody'); - expect(tableRenderBody()).toBe(null); + expect(renderSpy).toHaveBeenCalledTimes(0); }); it('should not override the TableHOC renderBody method when there is data', () => { @@ -58,14 +57,16 @@ describe('Table HOC', () => { expect(renderSpy).toHaveBeenCalledTimes(1); }); - it('should render a BlankSlate when the data is empty', () => { - const wrapper = shallowWithProps().dive(); - expect(wrapper.find(BlankSlate).exists()).toBe(true); + it('should update the renderBody when the data is empty', () => { + shallowWithProps({renderBody: renderSpy}).dive(); + expect(renderSpy).toHaveBeenCalledTimes(0); }); it('should not render a BlankSlate when the data is null', () => { - const wrapper = shallowWithProps({data: null}).dive(); - expect(wrapper.find(BlankSlate).exists()).toBe(false); + shallowWithProps({data: null, renderBody: renderSpy}) + .dive() + .dive(); + expect(renderSpy).toHaveBeenCalledTimes(1); }); it('should render the first BlankSlate when the data is empty', () => { @@ -75,8 +76,8 @@ describe('Table HOC', () => { tableWithBlankSlate({title: 'Second'}) )(TableHOC); - const wrapper = shallowWithState(, {}).dive(); - expect(wrapper.find(BlankSlate).prop('title')).toBe(expectedTitle); + const wrapper = shallowWithState(, {}); + expect((wrapper.dive().props() as any).renderBody().props.title).toBe(expectedTitle); }); }); }); diff --git a/packages/react-vapor/src/components/table-hoc/tests/TableWithNewPagination.spec.tsx b/packages/react-vapor/src/components/table-hoc/tests/TableWithNewPagination.spec.tsx new file mode 100644 index 0000000000..35645b7e11 --- /dev/null +++ b/packages/react-vapor/src/components/table-hoc/tests/TableWithNewPagination.spec.tsx @@ -0,0 +1,146 @@ +import {ShallowWrapper} from 'enzyme'; +import {shallowWithState, shallowWithStore} from 'enzyme-redux'; +import * as React from 'react'; +import * as _ from 'underscore'; +import {TableHOCUtils} from '..'; +import {getStoreMock} from '../../../utils/tests/TestUtils'; +import {PER_PAGE_NUMBERS} from '../../navigation/perPage'; +import {PaginationUtils} from '../../pagination/PaginationUtils'; +import {TablePagination} from '../../pagination/TablePagination'; +import {TableWithPaginationActions} from '../actions'; +import {TableHOC} from '../TableHOC'; +import { + ITableWithNewPaginationConfig, + ITableWithNewPaginationProps, + tableWithNewPagination, +} from '../TableWithNewPagination'; +import {ITableWithPaginationProps} from '../TableWithPagination'; + +describe('TableWithNewPagination tests', () => { + describe('', () => { + it('should mount and unmount without errors', () => { + const TableWithNewPagination = _.compose(tableWithNewPagination())(TableHOC); + expect(() => { + const wrapper = shallowWithState(, {}).dive(); + wrapper.unmount(); + }); + }); + + describe('once mounted', () => { + let wrapper: ShallowWrapper; + const shallowTableWithNewPagination = ( + config: Partial = {}, + props: Partial = {}, + state = {} + ) => { + const TableWithNewPagination = _.compose(tableWithNewPagination(config))(TableHOC); + return shallowWithState(, state); + }; + + it('should set the defaultPerPageSelected for with the second value in the array if defined', () => { + wrapper = shallowTableWithNewPagination({perPageNumbers: [3, 5, 10]}).dive(); + expect(wrapper.find(TablePagination).props().defaultPerPageSelected).toBe(5); + }); + + it('should set the defaultPerPageSelected for with the first value if only 1 value in the array', () => { + wrapper = shallowTableWithNewPagination({perPageNumbers: [3]}).dive(); + expect(wrapper.find(TablePagination).props().defaultPerPageSelected).toBe(3); + }); + + it('should set the defaultPerPageSelected for with the default value in PER_PAGE_NUMBERS if perPageNumbers is empty', () => { + wrapper = shallowTableWithNewPagination({perPageNumbers: []}).dive(); + expect(wrapper.find(TablePagination).props().defaultPerPageSelected).toBe(PER_PAGE_NUMBERS[1]); + }); + + it('should not call onUpdate if the pageNb dont change', () => { + const spy = jasmine.createSpy('onUpdate'); + wrapper = shallowTableWithNewPagination( + {perPageNumbers: []}, + {onUpdate: spy, id: 'test'}, + { + flatSelect: [{id: PaginationUtils.getPaginationPerPageId('test'), selectedOptionId: 2}], + paginationComposite: [{id: TableHOCUtils.getPaginationId('test'), pageNb: 2}], + } + ).dive(); + + wrapper.setProps({pageNb: 2, perPage: 2}); + expect(spy).toHaveBeenCalledTimes(0); + }); + + it('should call onUpdate if the pageNb change', () => { + const spy = jasmine.createSpy('onUpdate'); + wrapper = shallowTableWithNewPagination( + {perPageNumbers: []}, + {onUpdate: spy, id: 'test'}, + { + flatSelect: [{id: PaginationUtils.getPaginationPerPageId('test'), selectedOptionId: 2}], + paginationComposite: [{id: TableHOCUtils.getPaginationId('test'), pageNb: 2}], + } + ).dive(); + + wrapper.setProps({pageNb: 3, perPage: 2}); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should not call onUpdate if the perPage dont change', () => { + const spy = jasmine.createSpy('onUpdate'); + wrapper = shallowTableWithNewPagination( + {perPageNumbers: []}, + {onUpdate: spy, id: 'test'}, + { + flatSelect: [{id: PaginationUtils.getPaginationPerPageId('test'), selectedOptionId: 2}], + paginationComposite: [{id: TableHOCUtils.getPaginationId('test'), pageNb: 2}], + } + ).dive(); + + wrapper.setProps({pageNb: 2, perPage: 2}); + expect(spy).toHaveBeenCalledTimes(0); + }); + + it('should call onUpdate if the perPage change', () => { + const spy = jasmine.createSpy('onUpdate'); + wrapper = shallowTableWithNewPagination( + {perPageNumbers: []}, + {onUpdate: spy, id: 'test'}, + { + flatSelect: [{id: PaginationUtils.getPaginationPerPageId('test'), selectedOptionId: 2}], + paginationComposite: [{id: TableHOCUtils.getPaginationId('test'), pageNb: 2}], + } + ).dive(); + + wrapper.setProps({pageNb: 2, perPage: 3}); + expect(spy).toHaveBeenCalledTimes(1); + }); + }); + + describe('dispatch', () => { + const shallowTableWithNewPaginationWithStore = ( + store: ReturnType, + config: Partial = {}, + props: Partial = {} + ) => { + const TableWithNewPagination = _.compose(tableWithNewPagination(config))(TableHOC); + return shallowWithStore(, store); + }; + + it('should add the table pagination in the store on mount', () => { + const store = getStoreMock(); + shallowTableWithNewPaginationWithStore(store, {perPageNumbers: []}, {id: 'test'}).dive(); + + expect(store.getActions()).toContain(TableWithPaginationActions.add('test')); + }); + + it('should remove the table pagination in the store on unmount', () => { + const store = getStoreMock(); + const wrapper = shallowTableWithNewPaginationWithStore( + store, + {perPageNumbers: []}, + {id: 'test'} + ).dive(); + wrapper.unmount(); + + expect(store.getActions()).toContain(TableWithPaginationActions.remove('test')); + }); + }); + }); +}); diff --git a/packages/react-vapor/src/components/table-hoc/tests/TableWithPagination.spec.tsx b/packages/react-vapor/src/components/table-hoc/tests/TableWithPagination.spec.tsx index 13a28a2d6a..a43255f396 100644 --- a/packages/react-vapor/src/components/table-hoc/tests/TableWithPagination.spec.tsx +++ b/packages/react-vapor/src/components/table-hoc/tests/TableWithPagination.spec.tsx @@ -8,8 +8,8 @@ import {turnOffLoading} from '../../loading/LoadingActions'; import {NavigationConnected} from '../../navigation/NavigationConnected'; import {TableWithPaginationActions} from '../actions/TableWithPaginationActions'; import {ITableHOCProps, TableHOC} from '../TableHOC'; -import {TableHOCUtils} from '../TableHOCUtils'; import {ITableWithPaginationProps, tableWithPagination} from '../TableWithPagination'; +import {TableHOCUtils} from '../utils/TableHOCUtils'; describe('Table HOC', () => { describe('TableWithPagination', () => { diff --git a/packages/react-vapor/src/components/table-hoc/tests/TableWithPredicate.spec.tsx b/packages/react-vapor/src/components/table-hoc/tests/TableWithPredicate.spec.tsx index 4f695e4da8..ccc87c6d72 100644 --- a/packages/react-vapor/src/components/table-hoc/tests/TableWithPredicate.spec.tsx +++ b/packages/react-vapor/src/components/table-hoc/tests/TableWithPredicate.spec.tsx @@ -4,8 +4,8 @@ import * as _ from 'underscore'; import {withServerSideProcessing} from '../../../hoc/withServerSideProcessing/withServerSideProcessing'; import {ITableHOCProps, TableHOC} from '../TableHOC'; -import {TableHOCUtils} from '../TableHOCUtils'; import {tableWithPredicate} from '../TableWithPredicate'; +import {TableHOCUtils} from '../utils/TableHOCUtils'; describe('Table HOC', () => { describe('TableWithPredicate', () => { diff --git a/packages/react-vapor/src/components/table-hoc/tests/TableWithUrlState.spec.tsx b/packages/react-vapor/src/components/table-hoc/tests/TableWithUrlState.spec.tsx index 6e9a877e31..53368b8f75 100644 --- a/packages/react-vapor/src/components/table-hoc/tests/TableWithUrlState.spec.tsx +++ b/packages/react-vapor/src/components/table-hoc/tests/TableWithUrlState.spec.tsx @@ -16,13 +16,13 @@ import {changePage} from '../../navigation/pagination/NavigationPaginationAction import {changePerPage} from '../../navigation/perPage/NavigationPerPageActions'; import {TableHeaderActions} from '../actions/TableHeaderActions'; import {ITableHOCOwnProps, TableHOC} from '../TableHOC'; -import {TableHOCUtils} from '../TableHOCUtils'; import {tableWithDatePicker} from '../TableWithDatePicker'; import {tableWithFilter} from '../TableWithFilter'; import {tableWithPagination} from '../TableWithPagination'; import {tableWithPredicate} from '../TableWithPredicate'; import {tableWithSort} from '../TableWithSort'; import {tableWithUrlState} from '../TableWithUrlState'; +import {TableHOCUtils} from '../utils/TableHOCUtils'; describe('Table HOC', () => { describe('tableWithUrlState', () => { diff --git a/packages/react-vapor/src/components/table-hoc/utils/TableHOCExampleUtils.tsx b/packages/react-vapor/src/components/table-hoc/utils/TableHOCExampleUtils.tsx new file mode 100644 index 0000000000..4f14986b20 --- /dev/null +++ b/packages/react-vapor/src/components/table-hoc/utils/TableHOCExampleUtils.tsx @@ -0,0 +1,84 @@ +import * as moment from 'moment'; +import * as React from 'react'; +import {DateUtils} from '../../../utils/DateUtils'; +import {SELECTION_BOXES_LONG} from '../../datePicker/examples/DatePickerExamplesCommon'; +import {IExampleRowData, TableHOCServerExampleId} from '../examples/TableHOCServerExamples'; +import {TableRowConnected} from '../TableRowConnected'; +import {TableRowNumberColumn} from '../TableRowNumberColumn'; + +export interface ITableHOCServerExampleContext { + isLoading: boolean; + id: string; +} + +export const TableHOCServerExampleContext = React.createContext({ + isLoading: true, + id: undefined, +}); + +const generateRows = (allData: IExampleRowData[]) => + allData.map((data: IExampleRowData, i: number) => ( + 👋
}} + > + + {data.city} + {data.email.toLowerCase()} + {data.username.toLowerCase()} + {data.dateOfBirth.toLocaleDateString()} + + )); + +const tableActions = (username: string) => [ + { + primary: true, + icon: 'edit', + name: 'edit', + enabled: true, + trigger: () => alert(username), + callOnDoubleClick: true, + }, +]; + +const tablePredicates = [ + { + id: 'address.city', + prepend: City:, + values: [ + {displayValue: 'All', value: '', selected: true}, + {displayValue: 'Lebsackbury', value: 'Lebsackbury'}, + ], + }, + { + id: 'username', + prepend: Username:, + values: [ + {displayValue: 'All', value: '', selected: true}, + {displayValue: 'bret', value: 'Bret'}, + ], + }, +]; + +const tableDatePickerConfig = { + datesSelectionBoxes: SELECTION_BOXES_LONG, + years: [...DateUtils.getPreviousYears(25), DateUtils.currentYear.toString()], + initialDateRange: [ + moment() + .subtract(25, 'years') + .toDate(), + moment().toDate(), + ], +}; + +export const TableHOCExampleUtils = { + generateRows, + tableActions, + tablePredicates, + tableDatePickerConfig, +}; diff --git a/packages/react-vapor/src/components/table-hoc/TableHOCUtils.ts b/packages/react-vapor/src/components/table-hoc/utils/TableHOCUtils.ts similarity index 74% rename from packages/react-vapor/src/components/table-hoc/TableHOCUtils.ts rename to packages/react-vapor/src/components/table-hoc/utils/TableHOCUtils.ts index 140acb6e67..464af1f374 100644 --- a/packages/react-vapor/src/components/table-hoc/TableHOCUtils.ts +++ b/packages/react-vapor/src/components/table-hoc/utils/TableHOCUtils.ts @@ -1,10 +1,12 @@ import * as _ from 'underscore'; -import {IReactVaporState} from '../../ReactVapor'; -import {DatePickerSelectors} from '../datePicker/DatePickerSelectors'; -import {IFilterState} from '../filterBox/FilterBoxReducers'; -import {IListBoxState} from '../listBox/ListBoxReducers'; -import {ITableWithSortState} from './reducers/TableWithSortReducers'; +import {IReactVaporState} from '../../../ReactVapor'; +import {DatePickerSelectors} from '../../datePicker/DatePickerSelectors'; +import {IFilterState} from '../../filterBox/FilterBoxReducers'; +import {FlatSelectSelectors} from '../../flatSelect/FlatSelectSelectors'; +import {IListBoxState} from '../../listBox/ListBoxReducers'; +import {PaginationUtils} from '../../pagination/PaginationUtils'; +import {ITableWithSortState} from '../reducers/TableWithSortReducers'; export interface ITableHOCPredicateValue { id: string; @@ -28,8 +30,15 @@ const getCompositeState = (id: string, state: IReactVaporState): ITableHOCCompos state.tableHOCHeader, (v: ITableWithSortState) => v.tableId === id && _.isBoolean(v.isAsc) ); - const paginationState = _.findWhere(state.paginationComposite, {id: getPaginationId(id)}); - const perPageState = _.findWhere(state.perPageComposite, {id}); + + const perPage = + parseInt( + FlatSelectSelectors.getSelectedOptionId(state, { + id: PaginationUtils.getPaginationPerPageId(id), + }), + 10 + ) || undefined; + const filter: IFilterState = _.findWhere(state.filters, {id}); const predicates = getTablePredicates(id, state); @@ -42,8 +51,8 @@ const getCompositeState = (id: string, state: IReactVaporState): ITableHOCCompos sortAscending: (tableSort && tableSort.isAsc) || null, // pagination - perPage: perPageState && perPageState.perPage, - pageNb: paginationState && paginationState.pageNb, + perPage, + pageNb: _.findWhere(state.paginationComposite, {id: getPaginationId(id)})?.pageNb, // filter filter: filter && filter.filterText, diff --git a/packages/react-vapor/src/components/toast/examples/ToastConnectedExamples.1.use.md b/packages/react-vapor/src/components/toast/examples/ToastConnectedExamples.1.use.md index a8414b171f..266d4ea8a8 100644 --- a/packages/react-vapor/src/components/toast/examples/ToastConnectedExamples.1.use.md +++ b/packages/react-vapor/src/components/toast/examples/ToastConnectedExamples.1.use.md @@ -16,11 +16,11 @@ The Toast type choice depends on the status of the operation to which the toast refers. -| Type | Purpose | -| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Success | Used to confirm that an operation was successfully executed. If the Toast contains a link or action, it has to me manually dismissed by the user. If not, it is self-dismissible. | +| Type | Purpose | +| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Success | Used to confirm that an operation was successfully executed. If the Toast contains a link or action, it has to me manually dismissed by the user. If not, it is self-dismissible. | | Warning | Used to notify users that the operation they have launched may not yet be completed, or may not have had the expected result. If the Toast contains a link or action, it has to me manually dismissed by the user. If not, it is self-dismissible. | -| Error | Used to notify users about an operation that was not successful. Users must interact with the Toast to dismiss it (i.e., it is not self-dismissible) to ensure they do not miss critical information. | +| Error | Used to notify users about an operation that was not successful. Users must interact with the Toast to dismiss it (i.e., it is not self-dismissible) to ensure they do not miss critical information. | --- diff --git a/packages/react-vapor/src/utils/ComponentUtils.ts b/packages/react-vapor/src/utils/ComponentUtils.ts index 0cdf929de0..a2bd1cfaf0 100644 --- a/packages/react-vapor/src/utils/ComponentUtils.ts +++ b/packages/react-vapor/src/utils/ComponentUtils.ts @@ -16,6 +16,11 @@ export const DisplayClass = { TABLE_CELL: 'table-cell', }; +export interface IComponentBehaviour { + isLoading?: boolean; + disabled?: boolean; +} + export const getBasicDocumentLink = (url: string, title: string = ''): ILinkSvgProps => { return { url, diff --git a/packages/react-vapor/src/utils/UrlUtils.ts b/packages/react-vapor/src/utils/UrlUtils.ts index 77aa88279c..646c697b01 100644 --- a/packages/react-vapor/src/utils/UrlUtils.ts +++ b/packages/react-vapor/src/utils/UrlUtils.ts @@ -21,7 +21,7 @@ function toQueryString(obj: object): string { return QueryString.stringify(obj, {sort: false}); } -function getSearchParams() { +function getSearchParams(): {[key: string]: any} { return UrlUtils.toObject(UrlUtils.getQuery()); } diff --git a/packages/vapor/scss/tables/pagination.scss b/packages/vapor/scss/tables/pagination.scss index b5dac805c0..2dd0bab836 100644 --- a/packages/vapor/scss/tables/pagination.scss +++ b/packages/vapor/scss/tables/pagination.scss @@ -31,6 +31,29 @@ .spinner { display: none; } + + .change-page-link { + padding: 0; + color: $medium-blue; + font-size: $default-font-size; + background-color: $pure-white; + border: none; + cursor: pointer; + + .icon { + fill: $medium-blue; + } + + &.disabled { + color: $medium-grey; + cursor: default; + pointer-events: none; + + .icon { + fill: $medium-grey; + } + } + } } .modal, diff --git a/packages/vapor/scss/tables/table-loading.scss b/packages/vapor/scss/tables/table-loading.scss index 3b0f21b99c..55bdc19c29 100644 --- a/packages/vapor/scss/tables/table-loading.scss +++ b/packages/vapor/scss/tables/table-loading.scss @@ -4,7 +4,15 @@ } .table-cell-content-loading { - @extend .btn, .full-content-x, .content-placeholder, .cursor-auto, .mod-no-border, .mod-small; + @extend .btn, .content-placeholder, .cursor-auto, .mod-no-border, .mod-small; + + width: 90%; + min-width: 90%; + + &.mod-half { + width: 60%; + min-width: 60%; + } } .table-header-action-button-loading-width { @@ -19,3 +27,10 @@ align-self: center; width: $table-header-action-bar-button; } + +table.big-table { + td.table-cell-loading div.table-cell-content-loading { + margin-top: 7.5px; + margin-bottom: 7.5px; + } +}