From c4eb2f835fb4457282f491c4d1f208add1d734b1 Mon Sep 17 00:00:00 2001 From: AllenFang Date: Sun, 24 Dec 2017 16:02:05 +0800 Subject: [PATCH] refine remote mode on filter and pagination --- .../src/wrapper.js | 41 +++++-- .../test/wrapper.test.js | 69 +++++++++-- .../src/wrapper.js | 30 ++++- .../test/wrapper.test.js | 108 +++++++++++++----- .../react-bootstrap-table2/src/container.js | 35 +++++- .../react-bootstrap-table2/src/store/index.js | 18 ++- .../test/container.test.js | 82 ++++++++++++- 7 files changed, 315 insertions(+), 68 deletions(-) diff --git a/packages/react-bootstrap-table2-filter/src/wrapper.js b/packages/react-bootstrap-table2-filter/src/wrapper.js index 0cd8f44..48e4d73 100644 --- a/packages/react-bootstrap-table2-filter/src/wrapper.js +++ b/packages/react-bootstrap-table2-filter/src/wrapper.js @@ -1,12 +1,16 @@ +/* eslint react/prop-types: 0 */ import { Component } from 'react'; import PropTypes from 'prop-types'; import { filters } from './filter'; +import { LIKE } from './comparison'; export default class FilterWrapper extends Component { static propTypes = { store: PropTypes.object.isRequired, columns: PropTypes.array.isRequired, baseElement: PropTypes.func.isRequired, + onRemoteFilterChange: PropTypes.func.isRequired, + // refactoring later _: PropTypes.object.isRequired } @@ -16,28 +20,51 @@ export default class FilterWrapper extends Component { this.onFilter = this.onFilter.bind(this); } - componentWillReceiveProps() { - this.setState(() => ({ isDataChanged: false })); + componentWillReceiveProps(nextProps) { + // consider to use lodash.isEqual + if (JSON.stringify(this.state.currFilters) !== JSON.stringify(nextProps.store.filters)) { + this.setState(() => ({ isDataChanged: true, currFilters: nextProps.store.filters })); + } else { + this.setState(() => ({ isDataChanged: false })); + } } onFilter(column, filterVal, filterType) { - const { store, columns, _ } = this.props; - const { currFilters } = this.state; + const { store, columns, _, onRemoteFilterChange } = this.props; + const currFilters = Object.assign({}, this.state.currFilters); const { dataField, filter } = column; if (!_.isDefined(filterVal) || filterVal === '') { delete currFilters[dataField]; } else { - const { comparator } = filter.props; + const { comparator = LIKE } = filter.props; currFilters[dataField] = { filterVal, filterType, comparator }; } + store.filters = currFilters; + + if (this.isRemote() || this.isPaginationRemote()) { + onRemoteFilterChange(this.isPaginationRemote()); + // when remote filtering is enable, dont set currFilters state + // in the componentWillReceiveProps, it's the key point that we can know the filter is changed + return; + } store.filteredData = filters(store, columns, _)(currFilters); - store.filtering = Object.keys(currFilters).length > 0; - this.setState(() => ({ currFilters, isDataChanged: true })); } + // refactoring later + isRemote() { + const { remote } = this.props; + return remote === true || (typeof remote === 'object' && remote.filter); + } + + // refactoring later + isPaginationRemote() { + const { remote } = this.props; + return remote === true || (typeof remote === 'object' && remote.pagination); + } + render() { return this.props.baseElement({ ...this.props, diff --git a/packages/react-bootstrap-table2-filter/test/wrapper.test.js b/packages/react-bootstrap-table2-filter/test/wrapper.test.js index c877c62..9541c8d 100644 --- a/packages/react-bootstrap-table2-filter/test/wrapper.test.js +++ b/packages/react-bootstrap-table2-filter/test/wrapper.test.js @@ -1,4 +1,5 @@ import React from 'react'; +import sinon from 'sinon'; import { shallow } from 'enzyme'; import _ from 'react-bootstrap-table2/src/utils'; @@ -20,7 +21,11 @@ for (let i = 0; i < 20; i += 1) { describe('Wrapper', () => { let wrapper; let instance; + const onRemoteFilterChangeCB = sinon.stub(); + afterEach(() => { + onRemoteFilterChangeCB.reset(); + }); const createTableProps = () => { const tableProps = { @@ -40,7 +45,8 @@ describe('Wrapper', () => { data, filter: filter(), _, - store: new Store('id') + store: new Store('id'), + onRemoteFilterChange: onRemoteFilterChangeCB }; tableProps.store.data = data; return tableProps; @@ -84,13 +90,28 @@ describe('Wrapper', () => { describe('componentWillReceiveProps', () => { let nextProps; - beforeEach(() => { - nextProps = createTableProps(); - instance.componentWillReceiveProps(nextProps); + describe('when props.store.filters is same as current state.currFilters', () => { + beforeEach(() => { + nextProps = createTableProps(); + instance.componentWillReceiveProps(nextProps); + }); + + it('should setting isDataChanged as false (Temporary solution)', () => { + expect(instance.state.isDataChanged).toBeFalsy(); + }); }); - it('should setting isDataChanged as false always(Temporary solution)', () => { - expect(instance.state.isDataChanged).toBeFalsy(); + describe('when props.store.filters is different from current state.currFilters', () => { + beforeEach(() => { + nextProps = createTableProps(); + nextProps.store.filters = { price: { filterVal: 20, filterType: FILTER_TYPE.TEXT } }; + instance.componentWillReceiveProps(nextProps); + }); + + it('should setting states correctly', () => { + expect(instance.state.isDataChanged).toBeTruthy(); + expect(instance.state.currFilters).toBe(nextProps.store.filters); + }); }); }); @@ -126,7 +147,7 @@ describe('Wrapper', () => { it('should setting store object correctly', () => { instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT); - expect(props.store.filtering).toBeTruthy(); + expect(props.store.filters).toEqual(instance.state.currFilters); }); it('should setting state correctly', () => { @@ -136,30 +157,54 @@ describe('Wrapper', () => { }); }); + describe('when remote filter is enabled', () => { + const filterVal = '3'; + + beforeEach(() => { + props = createTableProps(); + props.remote = { filter: true }; + createFilterWrapper(props); + instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT); + }); + + it('should not setting store object correctly', () => { + expect(props.store.filters).not.toEqual(instance.state.currFilters); + }); + + it('should not setting state', () => { + expect(instance.state.isDataChanged).toBeFalsy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(0); + }); + + it('should calling props.onRemoteFilterChange correctly', () => { + expect(onRemoteFilterChangeCB.calledOnce).toBeTruthy(); + }); + }); + describe('combination', () => { it('should setting store object correctly', () => { instance.onFilter(props.columns[1], '3', FILTER_TYPE.TEXT); - expect(props.store.filtering).toBeTruthy(); + expect(props.store.filters).toEqual(instance.state.currFilters); expect(instance.state.isDataChanged).toBeTruthy(); expect(Object.keys(instance.state.currFilters)).toHaveLength(1); instance.onFilter(props.columns[1], '2', FILTER_TYPE.TEXT); - expect(props.store.filtering).toBeTruthy(); + expect(props.store.filters).toEqual(instance.state.currFilters); expect(instance.state.isDataChanged).toBeTruthy(); expect(Object.keys(instance.state.currFilters)).toHaveLength(1); instance.onFilter(props.columns[2], '2', FILTER_TYPE.TEXT); - expect(props.store.filtering).toBeTruthy(); + expect(props.store.filters).toEqual(instance.state.currFilters); expect(instance.state.isDataChanged).toBeTruthy(); expect(Object.keys(instance.state.currFilters)).toHaveLength(2); instance.onFilter(props.columns[2], '', FILTER_TYPE.TEXT); - expect(props.store.filtering).toBeTruthy(); + expect(props.store.filters).toEqual(instance.state.currFilters); expect(instance.state.isDataChanged).toBeTruthy(); expect(Object.keys(instance.state.currFilters)).toHaveLength(1); instance.onFilter(props.columns[1], '', FILTER_TYPE.TEXT); - expect(props.store.filtering).toBeFalsy(); + expect(props.store.filters).toEqual(instance.state.currFilters); expect(instance.state.isDataChanged).toBeTruthy(); expect(Object.keys(instance.state.currFilters)).toHaveLength(0); }); diff --git a/packages/react-bootstrap-table2-paginator/src/wrapper.js b/packages/react-bootstrap-table2-paginator/src/wrapper.js index f54a8d9..e01cd51 100644 --- a/packages/react-bootstrap-table2-paginator/src/wrapper.js +++ b/packages/react-bootstrap-table2-paginator/src/wrapper.js @@ -11,7 +11,8 @@ import { getByCurrPage } from './page'; class PaginationWrapper extends Component { static propTypes = { store: PropTypes.object.isRequired, - baseElement: PropTypes.func.isRequired + baseElement: PropTypes.func.isRequired, + onRemotePageChange: PropTypes.func.isRequired } constructor(props) { @@ -43,14 +44,15 @@ class PaginationWrapper extends Component { } this.state = { currPage, currSizePerPage }; + this.saveToStore(currPage, currSizePerPage); } componentWillReceiveProps(nextProps) { let needNewState = false; let { currPage, currSizePerPage } = this.state; - const { page, sizePerPage, pageStartIndex } = nextProps.pagination.options; + const { page, sizePerPage, pageStartIndex, onPageChange } = nextProps.pagination.options; - if (typeof page !== 'undefined') { // user defined page + if (typeof page !== 'undefined' && currPage !== page) { // user defined page currPage = page; needNewState = true; } else if (nextProps.isDataChanged) { // user didn't defined page but data change @@ -63,7 +65,19 @@ class PaginationWrapper extends Component { needNewState = true; } - if (needNewState) this.setState(() => ({ currPage, currSizePerPage })); + this.saveToStore(currPage, currSizePerPage); + + if (needNewState) { + if (onPageChange) { + onPageChange(currPage, currSizePerPage); + } + this.setState(() => ({ currPage, currSizePerPage })); + } + } + + saveToStore(page, sizePerPage) { + this.props.store.page = page; + this.props.store.sizePerPage = sizePerPage; } isRemote() { @@ -74,11 +88,13 @@ class PaginationWrapper extends Component { handleChangePage(currPage) { const { currSizePerPage } = this.state; const { pagination: { options }, onRemotePageChange } = this.props; + this.saveToStore(currPage, currSizePerPage); + if (options.onPageChange) { options.onPageChange(currPage, currSizePerPage); } if (this.isRemote()) { - onRemotePageChange(currPage, currSizePerPage); + onRemotePageChange(); return; } this.setState(() => { @@ -90,11 +106,13 @@ class PaginationWrapper extends Component { handleChangeSizePerPage(currSizePerPage, currPage) { const { pagination: { options }, onRemotePageChange } = this.props; + this.saveToStore(currPage, currSizePerPage); + if (options.onSizePerPageChange) { options.onSizePerPageChange(currSizePerPage, currPage); } if (this.isRemote()) { - onRemotePageChange(currPage, currSizePerPage); + onRemotePageChange(); return; } this.setState(() => { diff --git a/packages/react-bootstrap-table2-paginator/test/wrapper.test.js b/packages/react-bootstrap-table2-paginator/test/wrapper.test.js index 7254e2a..947971d 100644 --- a/packages/react-bootstrap-table2-paginator/test/wrapper.test.js +++ b/packages/react-bootstrap-table2-paginator/test/wrapper.test.js @@ -21,6 +21,11 @@ for (let i = 0; i < 100; i += 1) { describe('Wrapper', () => { let wrapper; let instance; + const onRemotePageChangeCB = sinon.stub(); + + afterEach(() => { + onRemotePageChangeCB.reset(); + }); const createTableProps = (props = {}) => { const tableProps = { @@ -34,7 +39,8 @@ describe('Wrapper', () => { }], data, pagination: paginator(props.options), - store: new Store('id') + store: new Store('id'), + onRemotePageChange: onRemotePageChangeCB }; tableProps.store.data = data; return tableProps; @@ -69,6 +75,11 @@ describe('Wrapper', () => { expect(instance.state.currSizePerPage).toEqual(Const.SIZE_PER_PAGE_LIST[0]); }); + it('should saving page and sizePerPage to store correctly', () => { + expect(props.store.page).toBe(instance.state.currPage); + expect(props.store.sizePerPage).toBe(instance.state.currSizePerPage); + }); + it('should rendering BootstraTable correctly', () => { const table = wrapper.find(BootstrapTable); expect(table.length).toBe(1); @@ -105,10 +116,19 @@ describe('Wrapper', () => { nextProps = createTableProps(); }); - it('should setting currPage state correctly by options.page', () => { - nextProps.pagination.options.page = 2; - instance.componentWillReceiveProps(nextProps); - expect(instance.state.currPage).toEqual(nextProps.pagination.options.page); + describe('when options.page is existing', () => { + beforeEach(() => { + nextProps.pagination.options.page = 2; + instance.componentWillReceiveProps(nextProps); + }); + + it('should setting currPage state correctly', () => { + expect(instance.state.currPage).toEqual(nextProps.pagination.options.page); + }); + + it('should saving store.page correctly', () => { + expect(props.store.page).toEqual(instance.state.currPage); + }); }); it('should not setting currPage state if options.page not existing', () => { @@ -117,10 +137,19 @@ describe('Wrapper', () => { expect(instance.state.currPage).toBe(currPage); }); - it('should setting currSizePerPage state correctly by options.sizePerPage', () => { - nextProps.pagination.options.sizePerPage = 20; - instance.componentWillReceiveProps(nextProps); - expect(instance.state.currSizePerPage).toEqual(nextProps.pagination.options.sizePerPage); + describe('when options.sizePerPage is existing', () => { + beforeEach(() => { + nextProps.pagination.options.sizePerPage = 20; + instance.componentWillReceiveProps(nextProps); + }); + + it('should setting currSizePerPage state correctly', () => { + expect(instance.state.currSizePerPage).toEqual(nextProps.pagination.options.sizePerPage); + }); + + it('should saving store.sizePerPage correctly', () => { + expect(props.store.sizePerPage).toEqual(instance.state.currSizePerPage); + }); }); it('should not setting currSizePerPage state if options.sizePerPage not existing', () => { @@ -129,17 +158,35 @@ describe('Wrapper', () => { expect(instance.state.currSizePerPage).toBe(currSizePerPage); }); - it('should setting currPage state when nextProps.isDataChanged is true', () => { - nextProps.isDataChanged = true; - instance.componentWillReceiveProps(nextProps); - expect(instance.state.currPage).toBe(Const.PAGE_START_INDEX); + describe('when nextProps.isDataChanged is true', () => { + beforeEach(() => { + nextProps.isDataChanged = true; + instance.componentWillReceiveProps(nextProps); + }); + + it('should setting currPage state correctly', () => { + expect(instance.state.currPage).toBe(Const.PAGE_START_INDEX); + }); + + it('should saving store.page correctly', () => { + expect(props.store.page).toEqual(instance.state.currPage); + }); }); - it('should setting currPage state when nextProps.isDataChanged is true and options.pageStartIndex is existing', () => { - nextProps.isDataChanged = true; - nextProps.pagination.options.pageStartIndex = 0; - instance.componentWillReceiveProps(nextProps); - expect(instance.state.currPage).toBe(nextProps.pagination.options.pageStartIndex); + describe('when nextProps.isDataChanged is true and options.pageStartIndex is existing', () => { + beforeEach(() => { + nextProps.isDataChanged = true; + nextProps.pagination.options.pageStartIndex = 0; + instance.componentWillReceiveProps(nextProps); + }); + + it('should setting currPage state correctly', () => { + expect(instance.state.currPage).toBe(nextProps.pagination.options.pageStartIndex); + }); + + it('should saving store.page correctly', () => { + expect(props.store.page).toEqual(instance.state.currPage); + }); }); }); }); @@ -448,11 +495,16 @@ describe('Wrapper', () => { expect(onPageChange.calledWith(newPage, instance.state.currSizePerPage)).toBeTruthy(); }); + it('should saving page and sizePerPage to store correctly', () => { + expect(props.store.page).toBe(newPage); + expect(props.store.sizePerPage).toBe(instance.state.currSizePerPage); + }); + describe('when pagination remote is enable', () => { beforeEach(() => { props.remote = true; - props.onRemotePageChange = sinon.stub(); createPaginationWrapper(props, false); + onRemotePageChangeCB.reset(); instance.handleChangePage(newPage); }); @@ -460,10 +512,8 @@ describe('Wrapper', () => { expect(instance.state.currPage).not.toEqual(newPage); }); - it('should calling options.onRemotePageChange correctly', () => { - expect(props.onRemotePageChange.calledOnce).toBeTruthy(); - expect(props.onRemotePageChange.calledWith( - newPage, instance.state.currSizePerPage)).toBeTruthy(); + it('should calling props.onRemotePageChange correctly', () => { + expect(onRemotePageChangeCB.calledOnce).toBeTruthy(); }); }); }); @@ -492,11 +542,16 @@ describe('Wrapper', () => { expect(onSizePerPageChange.calledWith(newSizePerPage, newPage)).toBeTruthy(); }); + it('should saving page and sizePerPage to store correctly', () => { + expect(props.store.page).toBe(newPage); + expect(props.store.sizePerPage).toBe(newSizePerPage); + }); + describe('when pagination remote is enable', () => { beforeEach(() => { props.remote = true; - props.onRemotePageChange = sinon.stub(); createPaginationWrapper(props, false); + onRemotePageChangeCB.reset(); instance.handleChangeSizePerPage(newSizePerPage, newPage); }); @@ -505,9 +560,8 @@ describe('Wrapper', () => { expect(instance.state.currSizePerPage).not.toEqual(newSizePerPage); }); - it('should calling options.onRemotePageChange correctly', () => { - expect(props.onRemotePageChange.calledOnce).toBeTruthy(); - expect(props.onRemotePageChange.calledWith(newPage, newSizePerPage)).toBeTruthy(); + it('should calling props.onRemotePageChange correctly', () => { + expect(onRemotePageChangeCB.calledOnce).toBeTruthy(); }); }); }); diff --git a/packages/react-bootstrap-table2/src/container.js b/packages/react-bootstrap-table2/src/container.js index a0866a7..8fb7959 100644 --- a/packages/react-bootstrap-table2/src/container.js +++ b/packages/react-bootstrap-table2/src/container.js @@ -20,16 +20,35 @@ const withDataStore = Base => this.store = new Store(props.keyField); this.store.data = props.data; this.handleUpdateCell = this.handleUpdateCell.bind(this); - this.onRemotePageChange = this.onRemotePageChange.bind(this); + this.handleRemotePageChange = this.handleRemotePageChange.bind(this); + this.handleRemoteFilterChange = this.handleRemoteFilterChange.bind(this); } componentWillReceiveProps(nextProps) { this.store.data = nextProps.data; } - onRemotePageChange(page, sizePerPage) { - const newState = { page, sizePerPage }; - this.props.onTableChange(newState); + getNewestState(state = {}) { + return { + page: this.store.page, + sizePerPage: this.store.sizePerPage, + filters: this.store.filters, + ...state + }; + } + + handleRemotePageChange() { + this.props.onTableChange('pagination', this.getNewestState()); + } + + // refactoring later for isRemotePagination + handleRemoteFilterChange(isRemotePagination) { + const newState = {}; + if (isRemotePagination) { + const options = this.props.pagination.options || {}; + newState.page = _.isDefined(options.pageStartIndex) ? options.pageStartIndex : 1; + } + this.props.onTableChange('filter', this.getNewestState(newState)); } handleUpdateCell(rowId, dataField, newValue) { @@ -72,13 +91,17 @@ const withDataStore = Base => } else if (this.props.selectRow) { return wrapWithSelection(baseProps); } else if (this.props.filter) { - return wrapWithFilter(baseProps); + return wrapWithFilter({ + ...baseProps, + onRemoteFilterChange: this.handleRemoteFilterChange, + onRemotePageChange: this.handleRemotePageChange + }); } else if (this.props.columns.filter(col => col.sort).length > 0) { return wrapWithSort(baseProps); } else if (this.props.pagination) { return wrapWithPagination({ ...baseProps, - onRemotePageChange: this.onRemotePageChange + onRemotePageChange: this.handleRemotePageChange }); } diff --git a/packages/react-bootstrap-table2/src/store/index.js b/packages/react-bootstrap-table2/src/store/index.js index 97d1aba..b735f86 100644 --- a/packages/react-bootstrap-table2/src/store/index.js +++ b/packages/react-bootstrap-table2/src/store/index.js @@ -11,7 +11,9 @@ export default class Store { this._sortOrder = undefined; this._sortField = undefined; this._selected = []; - this._filtering = false; + this._filters = {}; + this._page = undefined; + this._sizePerPage = undefined; } edit(rowId, dataField, newValue) { @@ -30,13 +32,13 @@ export default class Store { } get data() { - if (this._filtering) { + if (Object.keys(this._filters).length > 0) { return this._filteredData; } return this._data; } set data(data) { - if (this._filtering) { + if (Object.keys(this._filters).length > 0) { this._filteredData = data; } else { this._data = (data ? JSON.parse(JSON.stringify(data)) : []); @@ -52,12 +54,18 @@ export default class Store { get sortOrder() { return this._sortOrder; } set sortOrder(sortOrder) { this._sortOrder = sortOrder; } + get page() { return this._page; } + set page(page) { this._page = page; } + + get sizePerPage() { return this._sizePerPage; } + set sizePerPage(sizePerPage) { this._sizePerPage = sizePerPage; } + get sortField() { return this._sortField; } set sortField(sortField) { this._sortField = sortField; } get selected() { return this._selected; } set selected(selected) { this._selected = selected; } - get filtering() { return this._filtering; } - set filtering(filtering) { this._filtering = filtering; } + get filters() { return this._filters; } + set filters(filters) { this._filters = filters; } } diff --git a/packages/react-bootstrap-table2/test/container.test.js b/packages/react-bootstrap-table2/test/container.test.js index 8d1c614..fd4068a 100644 --- a/packages/react-bootstrap-table2/test/container.test.js +++ b/packages/react-bootstrap-table2/test/container.test.js @@ -202,9 +202,7 @@ describe('withDataStore', () => { }); }); - describe('onRemotePageChange', () => { - const page = 2; - const sizePerPage = 25; + describe('handleRemotePageChange', () => { const onTableChangeCallBack = sinon.stub(); beforeEach(() => { @@ -216,12 +214,86 @@ describe('withDataStore', () => { onTableChange={ onTableChangeCallBack } /> ); - wrapper.instance().onRemotePageChange(page, sizePerPage); + wrapper.instance().handleRemotePageChange(); }); it('should calling onTableChange correctly', () => { expect(onTableChangeCallBack.calledOnce).toBeTruthy(); - expect(onTableChangeCallBack.calledWith({ page, sizePerPage })).toBeTruthy(); + expect(onTableChangeCallBack.calledWith('pagination', wrapper.instance().getNewestState())).toBeTruthy(); + }); + }); + + describe('handleRemoteFilterChange', () => { + const onTableChangeCallBack = sinon.stub(); + + beforeEach(() => { + onTableChangeCallBack.reset(); + wrapper = shallow( + + ); + }); + + describe('when isRemotePagination argument is false', () => { + beforeEach(() => { + wrapper.instance().handleRemoteFilterChange(false); + }); + + it('should calling onTableChange correctly', () => { + expect(onTableChangeCallBack.calledOnce).toBeTruthy(); + expect(onTableChangeCallBack.calledWith('filter', wrapper.instance().getNewestState())).toBeTruthy(); + }); + }); + + describe('when isRemotePagination argument is false', () => { + describe('and pagination.options.pageStartIndex is defined', () => { + const options = { pageStartIndex: 0 }; + beforeEach(() => { + wrapper = shallow( + {} } } + onTableChange={ onTableChangeCallBack } + /> + ); + wrapper.instance().handleRemoteFilterChange(true); + }); + + it('should calling onTableChange correctly', () => { + expect(onTableChangeCallBack.calledOnce).toBeTruthy(); + const newState = wrapper.instance().getNewestState(); + newState.page = options.pageStartIndex; + expect(onTableChangeCallBack.calledWith('filter', newState)).toBeTruthy(); + }); + }); + + describe('and pagination.options.pageStartIndex is not defined', () => { + beforeEach(() => { + wrapper = shallow( + {} } } + onTableChange={ onTableChangeCallBack } + /> + ); + wrapper.instance().handleRemoteFilterChange(true); + }); + + it('should calling onTableChange correctly', () => { + expect(onTableChangeCallBack.calledOnce).toBeTruthy(); + const newState = wrapper.instance().getNewestState(); + newState.page = 1; + expect(onTableChangeCallBack.calledWith('filter', newState)).toBeTruthy(); + }); + }); }); }); });