├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── examples ├── brewed │ ├── .babelrc │ ├── app │ │ ├── App.jsx │ │ ├── Main.jsx │ │ ├── api.js │ │ ├── recipes │ │ │ ├── Index.jsx │ │ │ ├── Loading.jsx │ │ │ ├── Show.jsx │ │ │ ├── actionTypes.js │ │ │ ├── actions.js │ │ │ └── reducer.js │ │ └── reducers.js │ ├── config │ │ └── locales │ │ │ ├── en.js │ │ │ └── index.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── styles.scss │ └── webpack.config.js └── server-rendering │ ├── app │ ├── App.jsx │ ├── client.jsx │ ├── recipesConfig.js │ ├── server.jsx │ └── template.jsx │ ├── package.json │ └── webpack.config.js ├── lib ├── DataTable.js ├── Flipper.js ├── Next.js ├── PageLink.js ├── PageSizeDropdown.js ├── PaginationWrapper.js ├── Paginator.js ├── Prev.js ├── SortLink.js ├── actionTypes.js ├── actions.js ├── actions │ ├── actionTypes.js │ ├── actions.js │ ├── fetchingComposables.js │ ├── index.js │ └── simpleComposables.js ├── containers │ └── PaginationWrapper.js ├── decorators │ ├── decorate.js │ ├── flip.js │ ├── index.js │ ├── paginate.js │ ├── selectors.js │ ├── sort.js │ ├── stretch.js │ ├── tabulate.js │ └── violetPaginator.js ├── index.js ├── lib │ ├── range.js │ ├── reduxResolver.js │ └── stateManagement.js ├── pageInfoTranslator.js └── reducer.js ├── package.json ├── spec ├── .eslintrc ├── Next.spec.jsx ├── PageLink.spec.jsx ├── Prev.spec.jsx ├── SortLink.spec.jsx ├── actions.spec.js ├── containers │ └── PaginationWrapper.spec.jsx ├── decorators │ ├── flip.spec.js │ ├── paginate.spec.js │ ├── shared.jsx │ ├── sort.spec.js │ ├── stretch.spec.js │ ├── tabulate.spec.js │ └── violetPaginator.spec.js ├── pageInfoTranslator.spec.js ├── reducer.spec.js ├── specHelper.js └── stateManagement.spec.js └── src ├── DataTable.jsx ├── Flipper.jsx ├── Next.jsx ├── PageLink.jsx ├── PageSizeDropdown.jsx ├── Paginator.jsx ├── Prev.jsx ├── SortLink.jsx ├── actions ├── actionTypes.js ├── fetchingComposables.js ├── index.js └── simpleComposables.js ├── containers └── PaginationWrapper.jsx ├── decorators ├── decorate.jsx ├── flip.js ├── index.js ├── paginate.js ├── selectors.js ├── sort.js ├── stretch.js ├── tabulate.js └── violetPaginator.js ├── index.js ├── lib ├── range.js ├── reduxResolver.js └── stateManagement.js ├── pageInfoTranslator.js └── reducer.js /.babelrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "presets": ["es2015", "react", "stage-0"] 4 | } 5 | 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "plugins": ["react"], 5 | "rules": { 6 | "import/prefer-default-export": ["off"], 7 | "import/no-named-as-default": ["off"], 8 | "func-names": ["off"], 9 | "new-cap": [2, { "capIsNewExceptions": ["List", "Map", "Set"] }], 10 | "semi": [2, "never"], 11 | "comma-dangle": [2, "never"], 12 | "no-constant-condition": ["off"], 13 | "space-infix-ops": ["off"], 14 | "no-unused-vars": ["error", { "varsIgnorePattern": "^_" }], 15 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.swp 3 | *.swo 4 | coverage/ 5 | .nyc_output/ 6 | examples/server-rendering/assets/bundle.js 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | after_success: 5 | - bash <(curl -s https://codecov.io/bash) 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Sam Slotsky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/violet-paginator.svg)](https://github.com/sslotsky/violet-paginator) 2 | [![npm](https://img.shields.io/npm/dt/violet-paginator.svg)](https://www.npmjs.com/package/violet-paginator) 3 | [![npm](https://img.shields.io/npm/dm/violet-paginator.svg)](https://www.npmjs.com/package/violet-paginator) 4 | [![Build Status](https://travis-ci.org/sslotsky/violet-paginator.svg?branch=master)](https://travis-ci.org/sslotsky/violet-paginator) 5 | [![npm](https://img.shields.io/npm/l/express.svg)](https://github.com/sslotsky/violet-paginator) 6 | [![dependencies](https://david-dm.org/sslotsky/violet-paginator.svg)](https://david-dm.org/sslotsky/violet-paginator) 7 | 8 | # violet-paginator 9 | 10 | VioletPaginator is a react-redux package allowing users to manage arbitrarily many filtered, paginated lists 11 | of records. We provide a set of premade components including both simple and robust pagination controls, 12 | sort links, and data tables. We also make it ridiculously easy to write your own components and configure 13 | and extend VioletPaginator's default behavior by composing actions. 14 | 15 | ## Demo 16 | 17 | https://sslotsky.github.io/violet-paginator/ 18 | 19 | ## Extended Documentation 20 | 21 | https://sslotsky.gitbooks.io/violet-paginator/content/ 22 | 23 | ## Installation 24 | 25 | ``` 26 | npm i --save violet-paginator 27 | ``` 28 | 29 | ### Dependencies 30 | 31 | The current version of this package includes the following peer dependencies: 32 | 33 | ```javascript 34 | "peerDependencies": { 35 | "immutable": "^3.7.6", 36 | "react": "^0.14.8 || ^15.1.0", 37 | "react-redux": "^4.4.4 || 5.x", 38 | "redux": "^3.4.0" 39 | }, 40 | ``` 41 | 42 | Additionally, it is assumed that you are running some middleware that allows action creators to return 43 | promises, such as [redux-thunk](https://github.com/gaearon/redux-thunk). 44 | 45 | Finally, if you wish to use the premade `VioletPaginator` components, it is recommended that you include the `violet` 46 | and `font-awesome` stylesheets as described later in this document. 47 | 48 | ## Usage 49 | 50 | `VioletPaginator` is intended to be flexible so that it can be used in many ways without much fuss. We provide premade components, but our library is broken down into small, exposed pieces that allow you to easily override default settings, abstract core functionality, and create your own components. 51 | 52 | ### Creating a reducer 53 | 54 | Rather than exposing a single reducer, `violet-paginator` uses a 55 | [higher order reducer function](http://redux.js.org/docs/recipes/reducers/ReusingReducerLogic.html#customizing-behavior-with-higher-order-reducers) 56 | that creates a reducer and ties it to a `listId` and a `fetch` function (this has changed since version 1, see the [upgrade guide](https://sslotsky.gitbooks.io/violet-paginator/content/upgrade_guide.html) for details). 57 | 58 | ```javascript 59 | import { createPaginator } from 'violet-paginator' 60 | import { combineReducers } from 'redux' 61 | import users from './users/reducer' 62 | import { fetch } from './recipes/actions' 63 | 64 | export default combineReducers({ 65 | users, 66 | recipes: createPaginator({ 67 | listId: 'recipes', 68 | fetch 69 | }) 70 | }) 71 | ``` 72 | 73 | ### Configuration 74 | 75 | VioletPaginator aims to make client-server communication painless. For us, usability means: 76 | 77 | 1. We know how to read data from your server. 78 | 2. We will provide you with the _correctly formatted_ parameters that you need to send to your server. 79 | 80 | Because different backends will use different property names for pagination and sorting, we make this 81 | fully configurable. Example config: 82 | 83 | ```javascript 84 | import { configurePageParams } from 'violet-paginator' 85 | 86 | configurePageParams({ 87 | perPage: 'results_per_page', 88 | sortOrder: 'sort_reverse', 89 | sortReverse: true // Means that a boolean will be used to indicate sort direction. 90 | }) 91 | 92 | ``` 93 | 94 | An example URL with this configuration: 95 | 96 | ``` 97 | https://brewed-dev.herokuapp.com/v1/recipes?page=6&results_per_page=15&sort=name&sort_reverse=true 98 | ``` 99 | 100 | Another example config: 101 | 102 | ```javascript 103 | configurePageParams({ 104 | perPage: 'page_size', 105 | sortOrder: 'direction' 106 | }) 107 | ``` 108 | 109 | And a corresponding example URL: 110 | 111 | ``` 112 | https://www.example.com/v1/users?page=6&page_size=15&sort=name&direction=asc 113 | ``` 114 | 115 | The complete list of configuration options and their defaults can be found in the [pageInfoTranslator](https://github.com/sslotsky/violet-paginator/blob/master/src/pageInfoTranslator.js): 116 | 117 | Property Name | Default Value | Description 118 | ---|:---:|:--- 119 | page | `'page'` | The page number being requested 120 | perPage | `'pageSize'` | The page size being requested 121 | sort | `'sort'` | The field to sort by when requesting a page 122 | sortOrder | `'sortOrder'` | The sort direction for the requested page 123 | sortReverse | `false` | Use a boolean to indicate sort direction 124 | totalCount | `'total_count'` | The name of the property on the server response that indicates total record count 125 | results | `'results'` | The name of the property on the server that contains the page of results 126 | id | `'id'` | The name of the property on the record to be used as the unique identifer 127 | 128 | ### Using Premade VioletPaginator Components 129 | 130 | The following will display a 3 column data table with full pagination controls above and below the table. 131 | All pagination components require the `listId` prop, and they will use the `fetch` function that was supplied 132 | in the `createPaginator` call to retrieve the results at the appropriate times. _**You never actually call `fetch` yourself.**_ 133 | The `VioletDataTable` component also takes an array of headers. 134 | 135 | ```javascript 136 | import React, { PropTypes } from 'react' 137 | import { VioletDataTable, VioletPaginator } from 'violet-paginator' 138 | 139 | export default function RecipeList() { 140 | const headers = [{ 141 | field: 'name', 142 | text: 'Name' 143 | }, { 144 | field: 'created_at', 145 | text: 'Date Created' 146 | }, { 147 | field: 'boil_time', 148 | sortable: false, 149 | text: 'Boil Time' 150 | }] 151 | 152 | const paginator = ( 153 | 154 | ) 155 | 156 | return ( 157 |
158 | {paginator} 159 | 160 | {paginator} 161 |
162 | ) 163 | } 164 | ``` 165 | 166 | The `fetch` function that you supply to the paginator is an action creator that returns a promise. Therefore, 167 | while [redux-thunk](https://github.com/gaearon/redux-thunk) isn't explicitly required as a peer dependency, you will need to have some such middleware 168 | hooked up that allows action creators to return promises. Below is an example fetch function. 169 | 170 | ```javascript 171 | export default function fetchRecipes(pageInfo) { 172 | return () => api.recipes.index(pageInfo.query); 173 | } 174 | ``` 175 | 176 | Unlike most asynchronous action creators, notice that ours has no success and error handlers. `VioletPaginator` has its own 177 | handlers, so supplying your own is not necessary. However, if you wish to handle the response before passing it along to 178 | `VioletPaginator`, this isn't a problem as long as your success handler returns the response and your failure handler re-throws 179 | for us to catch, like below. 180 | 181 | ```javascript 182 | export default function fetchRecipes(pageInfo) { 183 | return dispatch => { 184 | dispatch({ type: actionTypes.FETCH_RECIPES }) 185 | return api.recipes.index(pageInfo.query).then(resp => { 186 | dispatch({ type: actionTypes.FETCH_RECIPES_SUCCESS, ...resp.data }) 187 | return resp 188 | }).catch(error => { 189 | dispatch({ type: actionTypes.FETCH_RECIPES_ERROR, error }) 190 | throw error 191 | }) 192 | } 193 | } 194 | ``` 195 | 196 | #### Styling 197 | 198 | Our premade components were built to be dispalyed using the [Violet CSS framework](https://github.com/kkestell/violet) 199 | and [Font Awesome](http://fontawesome.io/). We don't expose these stylesheets from our package. We leave it to you to 200 | include those in your project however you see fit. The easiest way is with CDN links: 201 | 202 | ```html 203 | 204 | 205 | 206 | ``` 207 | 208 | If Violet isn't for you but you still want to use our components, just write your own CSS. Our components 209 | use very few CSS classess, since Violet CSS rules are mostly structural in nature. However, we do recommend 210 | keeping the font-awesome link for displaying the icons. 211 | 212 | ### Customizing VioletDataTable 213 | 214 | By default, the `VioletDataTable` will simply display the raw values from the data that correspond to the headers that 215 | are specified. However, each header can be supplied with a `format` function, which can return a simple value, some markup, 216 | or a full-fledged react component. Example: 217 | 218 | ```javascript 219 | const activeColumn = recipe => { 220 | const icon = recipe.get('active') ? 'check' : 'ban' 221 | return ( 222 | 223 | ) 224 | } 225 | 226 | const headers = [{ 227 | field: 'active', 228 | sortable: false, 229 | text: 'Active', 230 | format: activeColumn 231 | }, { 232 | ... 233 | }] 234 | ``` 235 | 236 | ### Composing Actions 237 | 238 | `violet-paginator` is a plugin for redux apps, and as such, it dispatches its own actions and stores state in its own reducer. To give you complete control of the pagination state, the API provides access to all of these actions via the [composables](composables.md) and [simpleComposables](simplecomposables.md) functions. This allows you the flexibility to call them directly as part of a more complex operation. The most common use case for this would be [updating an item within the list](updating_items.md). 239 | 240 | As an example, consider a datatable where one column has a checkbox that's supposed to mark an item as active or inactive. 241 | Assuming that you have a `listId` of `'recipes'`, you could write an action creator like this to update the record on the server 242 | and then toggle the active state of the corresponding recipe within the list: 243 | 244 | ```javascript 245 | import api from 'ROOT/api' 246 | import { composables } from 'violet-paginator' 247 | 248 | const pageActions = composables({ listId: 'recipes' }) 249 | 250 | export function toggleActive(recipe) { 251 | const data = { 252 | active: !recipe.get('active') 253 | } 254 | 255 | return pageActions.updateAsync( 256 | recipe.get('id'), 257 | data, 258 | api.recipes.update(data) 259 | ) 260 | } 261 | ``` 262 | 263 | Now you can bring this action creator into your connected component using `connect` and `mapDispatchToProps`: 264 | 265 | ```javascript 266 | import { toggleActive } from './actions' 267 | 268 | export function Recipes({ toggle }) { 269 | ... 270 | } 271 | 272 | export default connect( 273 | undefined, 274 | { toggle: toggleActive } 275 | )(Recipes) 276 | ``` 277 | 278 | Finally, the `format` function for the `active` column in your data table might look like this: 279 | 280 | ```javascript 281 | const activeColumn = recipe => ( 282 | 287 | ) 288 | ``` 289 | 290 | ### Building Custom Components 291 | 292 | We understand that every product team could potentially want something different, and our premade components sometimes just won't fit that mold. We want to make it painless 293 | to write your own components, so to accomplish that, we made sure that it was every bit as painless to write ours. The best way to see how to build a custom component 294 | is to look at some of the simpler premade components. For example, here's a link that retrieves the next page of records: 295 | 296 | ```javascript 297 | import React from 'react' 298 | import FontAwesome from 'react-fontawesome' 299 | import { flip } from './decorators' 300 | 301 | export function Next({ pageActions, hasNextPage }) { 302 | const next = 303 | const link = hasNextPage ? ( 304 | {next} 305 | ) : next 306 | 307 | return link 308 | } 309 | 310 | export default flip(Next) 311 | ``` 312 | 313 | And here's a link that can sort our list in either direction by a given field name: 314 | 315 | ```javascript 316 | import React, { PropTypes } from 'react' 317 | import FontAwesome from 'react-fontawesome' 318 | import { sort as decorate } from './decorators' 319 | 320 | export function SortLink({ pageActions, field, text, sort, sortReverse, sortable=true }) { 321 | if (!sortable) { 322 | return {text} 323 | } 324 | 325 | const sortByField = () => 326 | pageActions.sort(field, !sortReverse) 327 | 328 | const arrow = sort === field && ( 329 | sortReverse ? 'angle-up' : 'angle-down' 330 | ) 331 | 332 | return ( 333 | 334 | {text} 335 | 336 | ) 337 | } 338 | 339 | SortLink.propTypes = { 340 | sort: PropTypes.string, 341 | sortReverse: PropTypes.bool, 342 | pageActions: PropTypes.object, 343 | field: PropTypes.string.isRequired, 344 | text: PropTypes.string.isRequired, 345 | sortable: PropTypes.bool 346 | } 347 | 348 | export default decorate(SortLink) 349 | 350 | ``` 351 | 352 | These components are simple and small enough to be written as pure functions rather than classes, and you should be able 353 | to accomplish the same. As you might have guessed, we expose the `flip` and `sorter` functions that are being called as the default export 354 | for our components, and those functions decorate your components with props that allow you to read and update the pagination state. 355 | The only prop that callers need to supply to these components is 356 | a `listId`, and one or two additional props in some cases. Simply import our decorators into your custom component: 357 | 358 | ```javascript 359 | import { decorators } from 'violet-paginator' 360 | ``` 361 | 362 | and you are ready to roll your own: 363 | 364 | ```javascript 365 | // Supports 'previous' and 'next' links 366 | export defaut decorators.flip(MyFlipperComponent) 367 | 368 | // Supports full pagination controls 369 | export default decorators.paginate(MyPaginationComponent) 370 | 371 | // Supports grids/datatables 372 | export default decorators.tabulate(MyDataGridComponent) 373 | 374 | // Supprts controls for changing the page size 375 | export default decorators.stretch(MyPageSizeDropdown) 376 | 377 | // Supports a control for sorting the list by the field name 378 | export default decorators.sort(MySortLink) 379 | 380 | // The kitchen sink! Injects properties from all decorators 381 | export default decorators.violetPaginator(MyPaginatedGridComponent) 382 | ``` 383 | 384 | For more on using decorators or creating your own, [check the docs on decorators](the_paginationwrapper.md). 385 | 386 | ## Contributing 387 | 388 | If you wish to contribute, please create a fork and submit a pull request, which will be reviewed as soon as humanly possible. A couple of 389 | key points: 390 | 391 | 1. Don't check in any changes to the `lib` folder. When we are ready to publish a new version, we will do a build and commit the `lib` changes and the new version number. 392 | 2. Add tests for your feature, and make sure all existing tests still pass and that the code passes lint (described further below). 393 | 394 | ### Testing 395 | 396 | This package is tested with mocha. The project uses CI through Travis which includes running tests, linting, and code coverage. 397 | Please make sure to write tests for any new pull requests. Code coverage will block the PR if your code is not sufficiently covered. 398 | -------------------------------------------------------------------------------- /examples/brewed/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "plugins": [ 4 | ["module-resolver", { 5 | "root": ["./"], 6 | "alias": { 7 | "ROOT": "./app", 8 | "CONF": "./config", 9 | "LIB": "./lib" 10 | } 11 | }] 12 | ] 13 | } 14 | 15 | -------------------------------------------------------------------------------- /examples/brewed/app/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import Recipes from './recipes/Index' 3 | 4 | export default function App() { 5 | return ( 6 |
7 |
8 |
9 |

VioletPaginator

10 | 11 | 12 | View On Github 13 | 14 | 15 |
16 |
17 | 18 |
19 | ) 20 | } 21 | 22 | App.propTypes = { 23 | children: PropTypes.object 24 | } 25 | -------------------------------------------------------------------------------- /examples/brewed/app/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import thunk from 'redux-thunk' 5 | import { compose, createStore, applyMiddleware } from 'redux' 6 | import { Provider } from 'react-redux' 7 | 8 | import { loadTranslations, setLocale, syncTranslationWithStore, I18n } from 'react-redux-i18n' 9 | 10 | import '../styles.scss' 11 | 12 | import translations from 'CONF/locales' 13 | import { configurePageParams } from 'violet-paginator' 14 | 15 | import reducers from './reducers' 16 | import App from './App' 17 | 18 | configurePageParams({ 19 | perPage: 'results_per_page', 20 | sortOrder: 'sort_reverse', 21 | sortReverse: true 22 | }) 23 | 24 | const devtools = window.devToolsExtension ? window.devToolsExtension() : f => f 25 | const store = createStore( 26 | reducers, 27 | compose(applyMiddleware(thunk), devtools) 28 | ) 29 | 30 | syncTranslationWithStore(store) 31 | store.dispatch(loadTranslations(translations)) 32 | store.dispatch(setLocale('en')) // TODO: resolve dynamically 33 | 34 | ReactDOM.render(( 35 | 36 |
37 | 38 |
39 |
40 | ), document.getElementById('app')) 41 | -------------------------------------------------------------------------------- /examples/brewed/app/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import PubSub from 'pubsub-js' 3 | 4 | const API_BASE = 'https://brewed-dev.herokuapp.com/v1' 5 | 6 | function api() { 7 | const adapter = axios.create({ 8 | baseURL: API_BASE, 9 | timeout: 10000, 10 | withCredentials: true, 11 | headers: { 12 | 'X-Api-Key': 'b780aac581de488cf77a629517ac999b', 13 | Accept: 'application/json' 14 | } 15 | }) 16 | 17 | adapter.interceptors.response.use( 18 | undefined, 19 | (error) => { 20 | if (error.response.status === 403) { 21 | PubSub.publish('session.expired') 22 | } 23 | 24 | return Promise.reject(error) 25 | } 26 | ) 27 | 28 | return adapter 29 | } 30 | 31 | export default { 32 | sessions: { 33 | create: (username, password) => 34 | api().post('/users/authenticate', { 35 | user_agent: navigator.userAgent, 36 | username, 37 | password 38 | }).then(resp => { 39 | localStorage.setItem('refresh', resp.data.refresh_token) 40 | return resp 41 | }) 42 | }, 43 | users: { 44 | create: (username, email, password) => 45 | api().post('/users', { 46 | user: { 47 | username, 48 | email, 49 | password 50 | } 51 | }) 52 | }, 53 | recipes: { 54 | index: (filters={}) => 55 | api().get('/recipes', { params: { ...filters } }), 56 | show: (id) => 57 | api().get(`/recipes/${id}`) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/brewed/app/recipes/Index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { I18n } from 'react-redux-i18n' 4 | import { 5 | VioletFlipper, 6 | VioletDataTable, 7 | VioletPaginator, 8 | VioletPageSizeDropdown 9 | } from 'violet-paginator' 10 | import { Link } from 'react-router' 11 | 12 | import Loading from './Loading' 13 | import fetchRecipes from './actions' 14 | 15 | export class Index extends Component { 16 | static propTypes = { 17 | loading: PropTypes.bool, 18 | fetch: PropTypes.func.isRequired 19 | } 20 | 21 | nameColumn(recipe) { 22 | return ( 23 | 24 | {recipe.get('name')} 25 | 26 | ) 27 | } 28 | 29 | headers() { 30 | return [{ 31 | field: 'name', 32 | text: I18n.t('recipes.name') 33 | }, { 34 | field: 'created_at', 35 | text: I18n.t('recipes.created_at') 36 | }, { 37 | field: 'boil_time', 38 | sortable: false, 39 | text: I18n.t('recipes.boil_time') 40 | }] 41 | } 42 | 43 | render() { 44 | const { fetch, loading } = this.props 45 | const flipper = ( 46 | 47 | ) 48 | 49 | return ( 50 |
51 | 52 | 53 | 54 | {flipper} 55 | 56 | {flipper} 57 | 58 |
59 | ) 60 | } 61 | } 62 | 63 | export default connect( 64 | state => ({ 65 | loading: !state.recipes.get('connected') 66 | }), 67 | { fetch: fetchRecipes } 68 | )(Index) 69 | -------------------------------------------------------------------------------- /examples/brewed/app/recipes/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import DropModal from 'boron/DropModal' 3 | 4 | export default class Loading extends Component { 5 | static propTypes = { 6 | loading: PropTypes.bool 7 | } 8 | 9 | componentDidMount() { 10 | if (this.props.loading) { 11 | this.modal.show() 12 | } 13 | } 14 | 15 | componentWillReceiveProps(nextProps) { 16 | if (nextProps.loading) { 17 | this.modal.show() 18 | } else { 19 | setTimeout(() => { 20 | this.modal.hide() 21 | }, 1000) 22 | } 23 | } 24 | 25 | render() { 26 | return ( 27 | this.modal = node}> 28 |
29 |

Connecting to Brewed API...

30 |
31 |
32 | ) 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /examples/brewed/app/recipes/Show.jsx: -------------------------------------------------------------------------------- 1 | import { PropTypes, Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import * as actions from './actions' 4 | 5 | export class Show extends Component { 6 | static propTypes = { 7 | id: PropTypes.string.isRequired, 8 | fetch: PropTypes.func.isRequired 9 | } 10 | 11 | componentDidMount() { 12 | const { fetch, id } = this.props 13 | fetch(id) 14 | } 15 | 16 | render() { 17 | return false 18 | } 19 | } 20 | 21 | export default connect( 22 | (_, ownProps) => ({ 23 | id: ownProps.params.id 24 | }), 25 | { fetch: actions.fetchRecipe } 26 | )(Show) 27 | -------------------------------------------------------------------------------- /examples/brewed/app/recipes/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const CONNECTED = 'CONNECTED' 2 | -------------------------------------------------------------------------------- /examples/brewed/app/recipes/actions.js: -------------------------------------------------------------------------------- 1 | import api from 'ROOT/api' 2 | import * as actionTypes from './actionTypes' 3 | 4 | export default function fetchRecipes(pageInfo) { 5 | return dispatch => 6 | api.recipes.index(pageInfo.query).then(resp => { 7 | dispatch({ type: actionTypes.CONNECTED }) 8 | return resp 9 | }) 10 | } 11 | 12 | -------------------------------------------------------------------------------- /examples/brewed/app/recipes/reducer.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable' 2 | import { resolveEach } from 'redux-resolver' 3 | import * as actionTypes from './actionTypes' 4 | 5 | const initialState = Map({ 6 | connected: false 7 | }) 8 | 9 | function connected(state) { 10 | return state.set('connected', true) 11 | } 12 | 13 | export default resolveEach(initialState, { 14 | [actionTypes.CONNECTED]: connected 15 | }) 16 | -------------------------------------------------------------------------------- /examples/brewed/app/reducers.js: -------------------------------------------------------------------------------- 1 | import { i18nReducer } from 'react-redux-i18n' 2 | import { combineReducers } from 'redux' 3 | import { createPaginator } from 'violet-paginator' 4 | import recipes from './recipes/reducer' 5 | import fetch from './recipes/actions' 6 | 7 | export default combineReducers({ 8 | recipes, 9 | recipeGrid: createPaginator({ 10 | listId: 'recipeGrid', 11 | fetch 12 | }), 13 | i18n: i18nReducer 14 | }) 15 | -------------------------------------------------------------------------------- /examples/brewed/config/locales/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | greetings: "Hello, %{name}!", 3 | home: { 4 | obey: "Obey the Toad", 5 | sign_in: "Sign In", 6 | home: "Home", 7 | new_user: "Create New User", 8 | recipes: "Recipes", 9 | }, 10 | validation: { 11 | required: "Required", 12 | email: "Please enter a valid email address", 13 | matches: "Does not match" 14 | }, 15 | recipes: { 16 | name: "Name", 17 | boil_time: "Boil Time", 18 | created_at: "Date Created" 19 | }, 20 | sign_in: { 21 | prompt: "Please Sign In", 22 | success: "Authenticated!", 23 | error: "Invalid username or password", 24 | submit: "Sign In", 25 | username: "Username", 26 | password: "Password" 27 | }, 28 | users: { 29 | form: { 30 | username: "UserName", 31 | email: "Email", 32 | password: "Password", 33 | password_confirmation: "Confirm Password", 34 | register: "Register", 35 | validation: { 36 | password_confirmation: { 37 | matches: "Passwords don't match" 38 | }, 39 | email: { 40 | taken: "Email address already taken" 41 | }, 42 | username: { 43 | taken: "UserName already taken" 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/brewed/config/locales/index.js: -------------------------------------------------------------------------------- 1 | import en from './en' 2 | 3 | export default { 4 | en 5 | } 6 | -------------------------------------------------------------------------------- /examples/brewed/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Brewed 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/brewed/index.js: -------------------------------------------------------------------------------- 1 | export Main from './app/Main' 2 | -------------------------------------------------------------------------------- /examples/brewed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brewtoad-react-client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --progress --colors -w", 8 | "lint": "eslint ./app/** --ext=js,jsx", 9 | "test": "npm run lint", 10 | "build-example": "webpack ./index.js" 11 | }, 12 | "author": "", 13 | "license": "MIT", 14 | "dependencies": { 15 | "axios": "^0.13.1", 16 | "babel-core": "^6.13.2", 17 | "babel-loader": "^6.2.4", 18 | "babel-preset-es2015": "^6.13.2", 19 | "babel-preset-react": "^6.11.1", 20 | "babel-preset-stage-0": "^6.5.0", 21 | "babel-regenerator-runtime": "^6.5.0", 22 | "boron": "^0.2.3", 23 | "classnames": "^2.2.5", 24 | "css-loader": "^0.23.1", 25 | "fbjs": "^0.8.3", 26 | "immutable": "^3.8.1", 27 | "node-sass": "^3.8.0", 28 | "pubsub-js": "^1.5.4", 29 | "react": "^15.3.0", 30 | "react-dom": "^15.3.0", 31 | "react-fontawesome": "^1.1.0", 32 | "react-redux": "^4.4.5", 33 | "react-redux-i18n": "0.0.3", 34 | "react-router": "^3.0.0", 35 | "redux": "^3.5.2", 36 | "redux-resolver": "^1.0.1", 37 | "redux-thunk": "^2.1.0", 38 | "sass-loader": "^4.0.0", 39 | "style-loader": "^0.13.1", 40 | "violet-paginator": "2.0.0", 41 | "webpack": "^1.13.1" 42 | }, 43 | "devDependencies": { 44 | "babel-eslint": "^6.1.2", 45 | "babel-plugin-module-resolver": "^2.2.0", 46 | "babel-register": "^6.11.6", 47 | "browser-sync": "^2.14.0", 48 | "browser-sync-webpack-plugin": "^1.1.2", 49 | "eslint": "^3.2.2", 50 | "eslint-config-airbnb": "^10.0.0", 51 | "eslint-import-resolver-babel-module": "^2.0.1", 52 | "eslint-plugin-import": "^1.13.0", 53 | "eslint-plugin-jsx-a11y": "^2.1.0", 54 | "eslint-plugin-react": "^6.0.0", 55 | "expect": "^1.20.2", 56 | "mocha": "^3.0.2", 57 | "react-addons-test-utils": "^15.3.1", 58 | "redux-mock-store": "^1.1.4", 59 | "webpack-dev-server": "^1.14.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/brewed/styles.scss: -------------------------------------------------------------------------------- 1 | $alert-danger: rgb(202, 60, 60); 2 | 3 | a:hover { 4 | cursor: pointer; 5 | } 6 | 7 | form.pure-form-brewed { 8 | div.has-error { 9 | color: $alert-danger; 10 | 11 | input, input:focus { 12 | border-color: $alert-danger; 13 | } 14 | } 15 | } 16 | 17 | ul.pagination { 18 | margin-top: 1rem; 19 | margin-bottom: 1rem; 20 | 21 | li a:hover { 22 | background-color: #7e69c6; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/brewed/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var BrowserSyncPlugin = require('browser-sync-webpack-plugin') 4 | 5 | module.exports = { 6 | entry: './index.js', 7 | output: { path: __dirname, filename: 'bundle.js' }, 8 | devtool: "eval-source-map", 9 | resolve: { 10 | extensions: ['', '.js', '.jsx'] 11 | }, 12 | module: { 13 | loaders: [{ 14 | test: /.jsx?$/, 15 | loader: 'babel-loader', 16 | exclude: /node_modules/ 17 | }, { 18 | test: /\.scss$/, 19 | loaders: ["style", "css", "sass"] 20 | }] 21 | }, 22 | devServer: { 23 | historyApiFallback: true 24 | }, 25 | plugins: [ 26 | new BrowserSyncPlugin({ 27 | host: 'localhost', 28 | port: 3000, 29 | proxy: 'http://localhost:8080/' 30 | } 31 | )] 32 | } 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /examples/server-rendering/app/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { VioletPaginator, VioletDataTable } from 'violet-paginator' 3 | import { connect } from 'react-redux' 4 | import { preloaded } from './recipesConfig' 5 | 6 | export default function App() { 7 | const headers = [{ 8 | field: 'name', 9 | text: 'Name', 10 | sortable: false 11 | }] 12 | 13 | return( 14 |
15 |

Hello World!

16 | 17 | 18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /examples/server-rendering/app/client.jsx: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable' 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | import { createPaginator } from 'violet-paginator' 5 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux' 6 | import thunk from 'redux-thunk' 7 | import { Provider } from 'react-redux' 8 | import config from './recipesConfig' 9 | import App from './App' 10 | 11 | const preloadedState = window.__PRELOADED_STATE__ 12 | console.log(preloadedState) 13 | const reducer = combineReducers({ 14 | recipes: createPaginator(config) 15 | }) 16 | 17 | const store = createStore(reducer, { 18 | recipes: Immutable.fromJS(preloadedState.recipes) 19 | }, compose(applyMiddleware(thunk))) 20 | 21 | render( 22 | 23 | 24 | , 25 | document.getElementById('root') 26 | ) 27 | 28 | -------------------------------------------------------------------------------- /examples/server-rendering/app/recipesConfig.js: -------------------------------------------------------------------------------- 1 | const results = [{ 2 | name: 'Ewe and IPA' 3 | }, { 4 | name: 'Pouty Stout' 5 | }] 6 | 7 | export const preloaded = { 8 | results: results.slice(0, 1), 9 | totalCount: 2 10 | } 11 | 12 | const mockFetch = pageInfo => () => { 13 | const data = { 14 | ...preloaded, 15 | results: results.slice(pageInfo.query.page - 1, pageInfo.query.page) 16 | } 17 | 18 | return Promise.resolve({ data }) 19 | } 20 | 21 | export default { 22 | listId: 'recipes', 23 | fetch: mockFetch, 24 | pageParams: { 25 | totalCountProp: 'totalCount' 26 | }, 27 | initialSettings: { 28 | pageSize: 1 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/server-rendering/app/server.jsx: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable' 2 | import path from 'path' 3 | import express from 'express' 4 | import React from 'react' 5 | import { createPaginator } from 'violet-paginator' 6 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux' 7 | import thunk from 'redux-thunk' 8 | import { Provider } from 'react-redux' 9 | import { renderToString } from 'react-dom/server' 10 | import config from './recipesConfig' 11 | import App from './App' 12 | import template from './template' 13 | 14 | const app = express() 15 | const port = 9999 16 | 17 | app.use('/assets', express.static('assets')); 18 | app.use(handleRender) 19 | 20 | function handleRender(req, resp) { 21 | const reducer = combineReducers({ 22 | recipes: createPaginator(config) 23 | }) 24 | 25 | const store = createStore(reducer, compose(applyMiddleware(thunk))) 26 | 27 | const html = renderToString( 28 | 29 | 30 | 31 | ) 32 | 33 | resp.send(template({ 34 | body: html, 35 | preloadedState: store.getState() 36 | })) 37 | } 38 | 39 | app.listen(port) 40 | -------------------------------------------------------------------------------- /examples/server-rendering/app/template.jsx: -------------------------------------------------------------------------------- 1 | export default ({ body, preloadedState }) => { 2 | return ` 3 | 4 | 5 | 6 | VioletPaginator 7 | 8 | 9 | 10 | 11 | 12 |
${body}
13 | 16 | 17 | 18 | 19 | ` 20 | } 21 | -------------------------------------------------------------------------------- /examples/server-rendering/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server-rendering", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run build-client && babel-node app/server.jsx --presets es2015,react,stage-2", 8 | "build-client": "webpack ./app/client.jsx" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "babel-cli": "^6.18.0", 14 | "babel-core": "^6.20.0", 15 | "babel-preset-es2015": "^6.18.0", 16 | "babel-preset-react": "^6.16.0", 17 | "babel-preset-stage-2": "^6.18.0", 18 | "browser-sync": "^2.18.2", 19 | "browser-sync-webpack-plugin": "^1.1.3", 20 | "express": "^4.14.0", 21 | "immutable": "^3.8.1", 22 | "nodemon": "^1.11.0", 23 | "react": "^15.4.1", 24 | "react-dom": "^15.4.1", 25 | "react-redux": "^4.4.6", 26 | "redux": "^3.6.0", 27 | "redux-immutablejs": "0.0.8", 28 | "redux-thunk": "^2.1.0", 29 | "violet-paginator": "2.0.0", 30 | "webpack": "^1.14.0" 31 | }, 32 | "devDependencies": {} 33 | } 34 | -------------------------------------------------------------------------------- /examples/server-rendering/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var BrowserSyncPlugin = require('browser-sync-webpack-plugin') 4 | 5 | module.exports = { 6 | entry: './app/client.jsx', 7 | output: { path: __dirname + '/assets', filename: 'bundle.js' }, 8 | devtool: "eval-source-map", 9 | resolve: { 10 | extensions: ['', '.js', '.jsx'] 11 | }, 12 | module: { 13 | loaders: [{ 14 | test: /.jsx?$/, 15 | loader: 'babel-loader', 16 | exclude: /node_modules/ 17 | }] 18 | }, 19 | devServer: { 20 | historyApiFallback: true 21 | }, 22 | plugins: [ 23 | new BrowserSyncPlugin({ 24 | host: 'localhost', 25 | port: 3000, 26 | proxy: 'http://localhost:8080/' 27 | } 28 | )] 29 | } 30 | 31 | -------------------------------------------------------------------------------- /lib/DataTable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | exports.DataTable = DataTable; 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _reactFontawesome = require('react-fontawesome'); 16 | 17 | var _reactFontawesome2 = _interopRequireDefault(_reactFontawesome); 18 | 19 | var _classnames = require('classnames'); 20 | 21 | var _classnames2 = _interopRequireDefault(_classnames); 22 | 23 | var _SortLink = require('./SortLink'); 24 | 25 | var _SortLink2 = _interopRequireDefault(_SortLink); 26 | 27 | var _decorators = require('./decorators'); 28 | 29 | var _pageInfoTranslator = require('./pageInfoTranslator'); 30 | 31 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 32 | 33 | function DataTable(props) { 34 | var results = props.results, 35 | headers = props.headers, 36 | isLoading = props.isLoading, 37 | updating = props.updating, 38 | removing = props.removing, 39 | _props$className = props.className, 40 | className = _props$className === undefined ? 'border' : _props$className; 41 | 42 | 43 | if (isLoading) { 44 | return _react2.default.createElement( 45 | 'center', 46 | null, 47 | _react2.default.createElement(_reactFontawesome2.default, { 48 | name: 'spinner', 49 | spin: true, 50 | size: '5x' 51 | }) 52 | ); 53 | } 54 | 55 | var headerRow = headers.map(function (h) { 56 | return _react2.default.createElement( 57 | 'th', 58 | { key: h.field }, 59 | _react2.default.createElement(_SortLink2.default, _extends({}, props, h)) 60 | ); 61 | }); 62 | 63 | var rows = results.map(function (r, i) { 64 | var columns = headers.map(function (h) { 65 | var field = h.field, 66 | format = h.format; 67 | 68 | var data = r.get(field); 69 | var displayData = format && format(r, i) || data; 70 | 71 | return _react2.default.createElement( 72 | 'td', 73 | { key: field }, 74 | displayData 75 | ); 76 | }); 77 | 78 | var classes = (0, _classnames2.default)({ 79 | updating: updating.includes(r.get((0, _pageInfoTranslator.recordProps)().identifier)), 80 | removing: removing.includes(r.get((0, _pageInfoTranslator.recordProps)().identifier)) 81 | }); 82 | 83 | return _react2.default.createElement( 84 | 'tr', 85 | { className: classes, key: 'results-' + i }, 86 | columns 87 | ); 88 | }); 89 | 90 | return _react2.default.createElement( 91 | 'table', 92 | { className: className }, 93 | _react2.default.createElement( 94 | 'thead', 95 | null, 96 | _react2.default.createElement( 97 | 'tr', 98 | null, 99 | headerRow 100 | ) 101 | ), 102 | _react2.default.createElement( 103 | 'tbody', 104 | null, 105 | rows 106 | ) 107 | ); 108 | } 109 | 110 | DataTable.propTypes = { 111 | headers: _react.PropTypes.array.isRequired, 112 | isLoading: _react.PropTypes.bool, 113 | results: _react.PropTypes.object, 114 | updating: _react.PropTypes.object, 115 | removing: _react.PropTypes.object, 116 | className: _react.PropTypes.string 117 | }; 118 | 119 | exports.default = (0, _decorators.tabulate)(DataTable); -------------------------------------------------------------------------------- /lib/Flipper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.Flipper = Flipper; 7 | 8 | var _react = require('react'); 9 | 10 | var _react2 = _interopRequireDefault(_react); 11 | 12 | var _classnames = require('classnames'); 13 | 14 | var _classnames2 = _interopRequireDefault(_classnames); 15 | 16 | var _decorators = require('./decorators'); 17 | 18 | var _Prev = require('./Prev'); 19 | 20 | var _Next = require('./Next'); 21 | 22 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 23 | 24 | function Flipper(props) { 25 | var prevClasses = (0, _classnames2.default)({ disabled: !props.hasPreviousPage }); 26 | var nextClasses = (0, _classnames2.default)({ disabled: !props.hasNextPage }); 27 | 28 | return _react2.default.createElement( 29 | 'ul', 30 | { className: 'pagination' }, 31 | _react2.default.createElement( 32 | 'li', 33 | { className: prevClasses }, 34 | _react2.default.createElement(_Prev.Prev, props) 35 | ), 36 | _react2.default.createElement( 37 | 'li', 38 | { className: nextClasses }, 39 | _react2.default.createElement(_Next.Next, props) 40 | ) 41 | ); 42 | } 43 | 44 | Flipper.propTypes = { 45 | hasPreviousPage: _react.PropTypes.bool, 46 | hasNextPage: _react.PropTypes.bool 47 | }; 48 | 49 | exports.default = (0, _decorators.flip)(Flipper); -------------------------------------------------------------------------------- /lib/Next.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.Next = Next; 7 | 8 | var _react = require('react'); 9 | 10 | var _react2 = _interopRequireDefault(_react); 11 | 12 | var _reactFontawesome = require('react-fontawesome'); 13 | 14 | var _reactFontawesome2 = _interopRequireDefault(_reactFontawesome); 15 | 16 | var _decorators = require('./decorators'); 17 | 18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 19 | 20 | function Next(_ref) { 21 | var pageActions = _ref.pageActions, 22 | hasNextPage = _ref.hasNextPage; 23 | 24 | var next = _react2.default.createElement(_reactFontawesome2.default, { name: 'chevron-right' }); 25 | var link = hasNextPage ? _react2.default.createElement( 26 | 'a', 27 | { onClick: pageActions.next }, 28 | next 29 | ) : next; 30 | 31 | return link; 32 | } 33 | 34 | exports.default = (0, _decorators.flip)(Next); -------------------------------------------------------------------------------- /lib/PageLink.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.PageLink = PageLink; 7 | 8 | var _react = require('react'); 9 | 10 | var _react2 = _interopRequireDefault(_react); 11 | 12 | var _decorators = require('./decorators'); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 15 | 16 | function PageLink(_ref) { 17 | var pageActions = _ref.pageActions, 18 | page = _ref.page, 19 | currentPage = _ref.currentPage; 20 | 21 | var navigate = function navigate() { 22 | return pageActions.goTo(page); 23 | }; 24 | 25 | var pageNumber = _react2.default.createElement( 26 | 'span', 27 | null, 28 | page 29 | ); 30 | var link = page === currentPage ? pageNumber : _react2.default.createElement( 31 | 'a', 32 | { onClick: navigate }, 33 | pageNumber 34 | ); 35 | 36 | return link; 37 | } 38 | 39 | exports.default = (0, _decorators.paginate)(PageLink); -------------------------------------------------------------------------------- /lib/PageSizeDropdown.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.PageSizeDropdown = PageSizeDropdown; 7 | 8 | var _react = require('react'); 9 | 10 | var _react2 = _interopRequireDefault(_react); 11 | 12 | var _decorators = require('./decorators'); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 15 | 16 | var defaultOptions = [15, 25, 50, 100]; 17 | 18 | function PageSizeDropdown(_ref) { 19 | var pageSize = _ref.pageSize, 20 | pageActions = _ref.pageActions, 21 | _ref$options = _ref.options, 22 | options = _ref$options === undefined ? defaultOptions : _ref$options; 23 | 24 | var optionTags = options.map(function (n) { 25 | return _react2.default.createElement( 26 | 'option', 27 | { key: n, value: n }, 28 | n 29 | ); 30 | }); 31 | 32 | var setPageSize = function setPageSize(e) { 33 | return pageActions.setPageSize(parseInt(e.target.value, 10)); 34 | }; 35 | 36 | return _react2.default.createElement( 37 | 'select', 38 | { value: pageSize, onChange: setPageSize }, 39 | optionTags 40 | ); 41 | } 42 | 43 | PageSizeDropdown.propTypes = { 44 | pageSize: _react.PropTypes.number, 45 | pageActions: _react.PropTypes.object, 46 | options: _react.PropTypes.array 47 | }; 48 | 49 | exports.default = (0, _decorators.stretch)(PageSizeDropdown); -------------------------------------------------------------------------------- /lib/PaginationWrapper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.PaginationWrapper = undefined; 7 | 8 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 9 | 10 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 11 | 12 | exports.default = paginate; 13 | 14 | var _react = require('react'); 15 | 16 | var _react2 = _interopRequireDefault(_react); 17 | 18 | var _reactRedux = require('react-redux'); 19 | 20 | var _redux = require('redux'); 21 | 22 | var _actions = require('./actions'); 23 | 24 | var _actions2 = _interopRequireDefault(_actions); 25 | 26 | var _reducer = require('./reducer'); 27 | 28 | var _stateManagement = require('./lib/stateManagement'); 29 | 30 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 31 | 32 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 33 | 34 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 35 | 36 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 37 | 38 | var connector = (0, _reactRedux.connect)(function (state, ownProps) { 39 | return { 40 | paginator: (0, _stateManagement.preloadedPaginator)(state, ownProps.listId, ownProps.preloaded) 41 | }; 42 | }, function (dispatch, ownProps) { 43 | return { 44 | actions: (0, _redux.bindActionCreators)((0, _actions2.default)(ownProps), dispatch) 45 | }; 46 | }); 47 | 48 | var PaginationWrapper = exports.PaginationWrapper = function (_Component) { 49 | _inherits(PaginationWrapper, _Component); 50 | 51 | function PaginationWrapper() { 52 | _classCallCheck(this, PaginationWrapper); 53 | 54 | return _possibleConstructorReturn(this, (PaginationWrapper.__proto__ || Object.getPrototypeOf(PaginationWrapper)).apply(this, arguments)); 55 | } 56 | 57 | _createClass(PaginationWrapper, [{ 58 | key: 'componentDidMount', 59 | value: function componentDidMount() { 60 | this.props.actions.initialize(); 61 | this.reloadIfStale(this.props); 62 | } 63 | }, { 64 | key: 'componentWillReceiveProps', 65 | value: function componentWillReceiveProps(nextProps) { 66 | this.reloadIfStale(nextProps); 67 | } 68 | }, { 69 | key: 'reloadIfStale', 70 | value: function reloadIfStale(props) { 71 | var paginator = props.paginator; 72 | var actions = props.actions; 73 | 74 | if (paginator.get('stale') && !paginator.get('isLoading') && !paginator.get('loadError')) { 75 | actions.reload(); 76 | } 77 | } 78 | }, { 79 | key: 'render', 80 | value: function render() { 81 | return this.props.children; 82 | } 83 | }]); 84 | 85 | return PaginationWrapper; 86 | }(_react.Component); 87 | 88 | PaginationWrapper.propTypes = { 89 | actions: _react.PropTypes.object.isRequired, 90 | paginator: _react.PropTypes.object, 91 | children: _react.PropTypes.element.isRequired 92 | }; 93 | PaginationWrapper.defaultProps = { 94 | paginator: _reducer.defaultPaginator 95 | }; 96 | 97 | 98 | function info(paginator) { 99 | var totalPages = Math.ceil(paginator.get('totalCount') / paginator.get('pageSize')); 100 | 101 | return { 102 | hasPreviousPage: paginator.get('page') > 1, 103 | hasNextPage: paginator.get('page') < totalPages, 104 | currentPage: paginator.get('page'), 105 | pageSize: paginator.get('pageSize'), 106 | results: paginator.get('results'), 107 | isLoading: paginator.get('isLoading'), 108 | updating: paginator.get('updating'), 109 | removing: paginator.get('removing'), 110 | totalPages: totalPages 111 | }; 112 | } 113 | 114 | function paginate(ComponentClass) { 115 | return connector(function (props) { 116 | return _react2.default.createElement( 117 | PaginationWrapper, 118 | props, 119 | _react2.default.createElement(ComponentClass, _extends({}, props, info(props.paginator))) 120 | ); 121 | }); 122 | } -------------------------------------------------------------------------------- /lib/Paginator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | exports.Paginator = Paginator; 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _reactFontawesome = require('react-fontawesome'); 16 | 17 | var _reactFontawesome2 = _interopRequireDefault(_reactFontawesome); 18 | 19 | var _classnames = require('classnames'); 20 | 21 | var _classnames2 = _interopRequireDefault(_classnames); 22 | 23 | var _paginate = require('./decorators/paginate'); 24 | 25 | var _paginate2 = _interopRequireDefault(_paginate); 26 | 27 | var _range = require('./lib/range'); 28 | 29 | var _range2 = _interopRequireDefault(_range); 30 | 31 | var _PageLink = require('./PageLink'); 32 | 33 | var _Prev = require('./Prev'); 34 | 35 | var _Next = require('./Next'); 36 | 37 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 38 | 39 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 40 | 41 | function Paginator(props) { 42 | var currentPage = props.currentPage, 43 | totalPages = props.totalPages, 44 | hasPreviousPage = props.hasPreviousPage, 45 | hasNextPage = props.hasNextPage; 46 | 47 | 48 | var upperOffset = Math.max(0, currentPage - totalPages + 3); 49 | var minPage = Math.max(props.currentPage - 3 - upperOffset, 1); 50 | var maxPage = Math.min(minPage + 6, totalPages); 51 | var prevClasses = (0, _classnames2.default)({ disabled: !hasPreviousPage }); 52 | var nextClasses = (0, _classnames2.default)({ disabled: !hasNextPage }); 53 | 54 | var pageLinks = [].concat(_toConsumableArray((0, _range2.default)(minPage, maxPage))).map(function (page) { 55 | var pageLinkClass = (0, _classnames2.default)({ current: page === currentPage }); 56 | 57 | return _react2.default.createElement( 58 | 'li', 59 | { className: pageLinkClass, key: page }, 60 | _react2.default.createElement(_PageLink.PageLink, _extends({}, props, { page: page })) 61 | ); 62 | }); 63 | 64 | var separator = totalPages > 7 ? _react2.default.createElement( 65 | 'li', 66 | { className: 'skip' }, 67 | _react2.default.createElement(_reactFontawesome2.default, { name: 'ellipsis-h' }) 68 | ) : false; 69 | 70 | var begin = separator && minPage > 1 ? _react2.default.createElement( 71 | 'li', 72 | null, 73 | _react2.default.createElement(_PageLink.PageLink, _extends({}, props, { page: 1 })) 74 | ) : false; 75 | 76 | var end = separator && maxPage < totalPages ? _react2.default.createElement( 77 | 'li', 78 | null, 79 | _react2.default.createElement(_PageLink.PageLink, _extends({}, props, { page: totalPages })) 80 | ) : false; 81 | 82 | return _react2.default.createElement( 83 | 'ul', 84 | { className: 'pagination' }, 85 | _react2.default.createElement( 86 | 'li', 87 | { className: prevClasses }, 88 | _react2.default.createElement(_Prev.Prev, props) 89 | ), 90 | begin, 91 | begin && separator, 92 | pageLinks, 93 | end && separator, 94 | end, 95 | _react2.default.createElement( 96 | 'li', 97 | { className: nextClasses }, 98 | _react2.default.createElement(_Next.Next, props) 99 | ) 100 | ); 101 | } 102 | 103 | Paginator.propTypes = { 104 | currentPage: _react.PropTypes.number, 105 | totalPages: _react.PropTypes.number, 106 | hasPreviousPage: _react.PropTypes.bool, 107 | hasNextPage: _react.PropTypes.bool 108 | }; 109 | 110 | exports.default = (0, _paginate2.default)(Paginator); -------------------------------------------------------------------------------- /lib/Prev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.Prev = Prev; 7 | 8 | var _react = require('react'); 9 | 10 | var _react2 = _interopRequireDefault(_react); 11 | 12 | var _reactFontawesome = require('react-fontawesome'); 13 | 14 | var _reactFontawesome2 = _interopRequireDefault(_reactFontawesome); 15 | 16 | var _decorators = require('./decorators'); 17 | 18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 19 | 20 | function Prev(_ref) { 21 | var pageActions = _ref.pageActions, 22 | hasPreviousPage = _ref.hasPreviousPage; 23 | 24 | var prev = _react2.default.createElement(_reactFontawesome2.default, { name: 'chevron-left' }); 25 | var link = hasPreviousPage ? _react2.default.createElement( 26 | 'a', 27 | { onClick: pageActions.prev }, 28 | prev 29 | ) : prev; 30 | 31 | return link; 32 | } 33 | 34 | exports.default = (0, _decorators.flip)(Prev); -------------------------------------------------------------------------------- /lib/SortLink.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.SortLink = SortLink; 7 | 8 | var _react = require('react'); 9 | 10 | var _react2 = _interopRequireDefault(_react); 11 | 12 | var _reactFontawesome = require('react-fontawesome'); 13 | 14 | var _reactFontawesome2 = _interopRequireDefault(_reactFontawesome); 15 | 16 | var _decorators = require('./decorators'); 17 | 18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 19 | 20 | function SortLink(_ref) { 21 | var pageActions = _ref.pageActions, 22 | field = _ref.field, 23 | text = _ref.text, 24 | sort = _ref.sort, 25 | sortReverse = _ref.sortReverse, 26 | _ref$sortable = _ref.sortable, 27 | sortable = _ref$sortable === undefined ? true : _ref$sortable; 28 | 29 | if (!sortable) { 30 | return _react2.default.createElement( 31 | 'span', 32 | null, 33 | text 34 | ); 35 | } 36 | 37 | var sortByField = function sortByField() { 38 | return pageActions.sort(field, !sortReverse); 39 | }; 40 | 41 | var arrow = sort === field && (sortReverse ? 'angle-up' : 'angle-down'); 42 | 43 | return _react2.default.createElement( 44 | 'a', 45 | { onClick: sortByField }, 46 | text, 47 | ' ', 48 | _react2.default.createElement(_reactFontawesome2.default, { name: arrow || '' }) 49 | ); 50 | } 51 | 52 | SortLink.propTypes = { 53 | sort: _react.PropTypes.string, 54 | sortReverse: _react.PropTypes.bool, 55 | pageActions: _react.PropTypes.object, 56 | field: _react.PropTypes.string.isRequired, 57 | text: _react.PropTypes.string.isRequired, 58 | sortable: _react.PropTypes.bool 59 | }; 60 | 61 | exports.default = (0, _decorators.sort)(SortLink); -------------------------------------------------------------------------------- /lib/actionTypes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var INITIALIZE_PAGINATOR = exports.INITIALIZE_PAGINATOR = '@@violet-paginator/INITIALIZE_PAGINATOR'; 7 | var DESTROY_PAGINATOR = exports.DESTROY_PAGINATOR = '@@violet-paginator/DESTROY_PAGINATOR'; 8 | var DESTROY_ALL = exports.DESTROY_ALL = '@@violet-paginator/DESTROY_ALL'; 9 | var EXPIRE_PAGINATOR = exports.EXPIRE_PAGINATOR = '@@violet-paginator/EXPIRE_PAGINATOR'; 10 | var EXPIRE_ALL = exports.EXPIRE_ALL = '@@violet-paginator/EXPIRE_ALL'; 11 | var FOUND_PAGINATOR = exports.FOUND_PAGINATOR = '@@violet-paginator/FOUND_PAGINATOR'; 12 | var PREVIOUS_PAGE = exports.PREVIOUS_PAGE = '@@violet-paginator/PREVIOUS_PAGE'; 13 | var NEXT_PAGE = exports.NEXT_PAGE = '@@violet-paginator/NEXT_PAGE'; 14 | var GO_TO_PAGE = exports.GO_TO_PAGE = '@@violet-paginator/GO_TO_PAGE'; 15 | var SET_PAGE_SIZE = exports.SET_PAGE_SIZE = '@@violet-paginator/SET_PAGE_SIZE'; 16 | var FETCH_RECORDS = exports.FETCH_RECORDS = '@@violet-paginator/FETCH_RECORDS'; 17 | var RESULTS_UPDATED = exports.RESULTS_UPDATED = '@@violet-paginator/RESULTS_UPDATED'; 18 | var RESULTS_UPDATED_ERROR = exports.RESULTS_UPDATED_ERROR = '@@violet-paginator/RESULTS_UPDATED_ERROR'; 19 | var TOGGLE_FILTER_ITEM = exports.TOGGLE_FILTER_ITEM = '@@violet-paginator/TOGGLE_FILTER_ITEM'; 20 | var SET_FILTER = exports.SET_FILTER = '@@violet-paginator/SET_FILTER'; 21 | var SET_FILTERS = exports.SET_FILTERS = '@@violet-paginator/SET_FILTERS'; 22 | var RESET_FILTERS = exports.RESET_FILTERS = '@@violet-paginator/RESET_FILTERS'; 23 | var SORT_CHANGED = exports.SORT_CHANGED = '@@violet-paginator/SORT_CHANGED'; 24 | var UPDATING_ITEM = exports.UPDATING_ITEM = '@@violet-paginator/UPDATING_ITEM'; 25 | var UPDATE_ITEMS = exports.UPDATE_ITEMS = '@@violet-paginator/UPDATE_ITEMS'; 26 | var UPDATE_ITEM = exports.UPDATE_ITEM = '@@violet-paginator/UPDATE_ITEM'; 27 | var UPDATING_ITEMS = exports.UPDATING_ITEMS = '@@violet-paginator/UPDATING_ITEMS'; 28 | var RESET_ITEM = exports.RESET_ITEM = '@@violet-paginator/RESET_ITEM'; 29 | var UPDATING_ALL = exports.UPDATING_ALL = '@@violet-paginator/UPDATING_ALL'; 30 | var UPDATE_ALL = exports.UPDATE_ALL = '@@violet-paginator/UPDATE_ALL'; 31 | var BULK_ERROR = exports.BULK_ERROR = '@@violet-paginator/BULK_ERROR'; 32 | var MARK_ITEMS_ERRORED = exports.MARK_ITEMS_ERRORED = '@@violet-paginator/MARK_ITEMS_ERRORED'; 33 | var RESET_RESULTS = exports.RESET_RESULTS = '@@violet-paginator/RESET_RESULTS'; 34 | var REMOVING_ITEM = exports.REMOVING_ITEM = '@@violet-paginator/REMOVING_ITEM'; 35 | var REMOVE_ITEM = exports.REMOVE_ITEM = '@@violet-paginator/REMOVE_ITEM'; 36 | var ITEM_ERROR = exports.ITEM_ERROR = '@@violet-paginator/ITEM_ERROR'; -------------------------------------------------------------------------------- /lib/actions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | exports.destroyAll = destroyAll; 10 | exports.expireAll = expireAll; 11 | exports.default = register; 12 | exports.composables = composables; 13 | 14 | var _actionTypes = require('./actionTypes'); 15 | 16 | var actionTypes = _interopRequireWildcard(_actionTypes); 17 | 18 | var _simpleComposables = require('./actions/simpleComposables'); 19 | 20 | var _simpleComposables2 = _interopRequireDefault(_simpleComposables); 21 | 22 | var _fetchingComposables = require('./actions/fetchingComposables'); 23 | 24 | var _fetchingComposables2 = _interopRequireDefault(_fetchingComposables); 25 | 26 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 27 | 28 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 29 | 30 | function destroyAll() { 31 | return { 32 | type: actionTypes.DESTROY_ALL 33 | }; 34 | } 35 | 36 | function expireAll() { 37 | return { 38 | type: actionTypes.EXPIRE_ALL 39 | }; 40 | } 41 | 42 | function register(config) { 43 | return _extends({}, (0, _fetchingComposables2.default)(config), (0, _simpleComposables2.default)(config.listId)); 44 | } 45 | 46 | function composables(config) { 47 | return register(_extends({}, config, { 48 | isBoundToDispatch: false 49 | })); 50 | } -------------------------------------------------------------------------------- /lib/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = actionType; 7 | var INITIALIZE_PAGINATOR = exports.INITIALIZE_PAGINATOR = '@@violet-paginator/INITIALIZE_PAGINATOR'; 8 | var EXPIRE_PAGINATOR = exports.EXPIRE_PAGINATOR = '@@violet-paginator/EXPIRE_PAGINATOR'; 9 | var EXPIRE_ALL = exports.EXPIRE_ALL = '@@violet-paginator/EXPIRE_ALL'; 10 | var FOUND_PAGINATOR = exports.FOUND_PAGINATOR = '@@violet-paginator/FOUND_PAGINATOR'; 11 | var PREVIOUS_PAGE = exports.PREVIOUS_PAGE = '@@violet-paginator/PREVIOUS_PAGE'; 12 | var NEXT_PAGE = exports.NEXT_PAGE = '@@violet-paginator/NEXT_PAGE'; 13 | var GO_TO_PAGE = exports.GO_TO_PAGE = '@@violet-paginator/GO_TO_PAGE'; 14 | var SET_PAGE_SIZE = exports.SET_PAGE_SIZE = '@@violet-paginator/SET_PAGE_SIZE'; 15 | var FETCH_RECORDS = exports.FETCH_RECORDS = '@@violet-paginator/FETCH_RECORDS'; 16 | var RESULTS_UPDATED = exports.RESULTS_UPDATED = '@@violet-paginator/RESULTS_UPDATED'; 17 | var RESULTS_UPDATED_ERROR = exports.RESULTS_UPDATED_ERROR = '@@violet-paginator/RESULTS_UPDATED_ERROR'; 18 | var TOGGLE_FILTER_ITEM = exports.TOGGLE_FILTER_ITEM = '@@violet-paginator/TOGGLE_FILTER_ITEM'; 19 | var SET_FILTER = exports.SET_FILTER = '@@violet-paginator/SET_FILTER'; 20 | var SET_FILTERS = exports.SET_FILTERS = '@@violet-paginator/SET_FILTERS'; 21 | var RESET_FILTERS = exports.RESET_FILTERS = '@@violet-paginator/RESET_FILTERS'; 22 | var SORT_CHANGED = exports.SORT_CHANGED = '@@violet-paginator/SORT_CHANGED'; 23 | var UPDATING_ITEM = exports.UPDATING_ITEM = '@@violet-paginator/UPDATING_ITEM'; 24 | var UPDATE_ITEMS = exports.UPDATE_ITEMS = '@@violet-paginator/UPDATE_ITEMS'; 25 | var UPDATE_ITEM = exports.UPDATE_ITEM = '@@violet-paginator/UPDATE_ITEM'; 26 | var UPDATING_ITEMS = exports.UPDATING_ITEMS = '@@violet-paginator/UPDATING_ITEMS'; 27 | var RESET_ITEM = exports.RESET_ITEM = '@@violet-paginator/RESET_ITEM'; 28 | var MARK_ITEMS_ERRORED = exports.MARK_ITEMS_ERRORED = '@@violet-paginator/MARK_ITEMS_ERRORED'; 29 | var RESET_RESULTS = exports.RESET_RESULTS = '@@violet-paginator/RESET_RESULTS'; 30 | var REMOVING_ITEM = exports.REMOVING_ITEM = '@@violet-paginator/REMOVING_ITEM'; 31 | var REMOVE_ITEM = exports.REMOVE_ITEM = '@@violet-paginator/REMOVE_ITEM'; 32 | var ITEM_ERROR = exports.ITEM_ERROR = '@@violet-paginator/ITEM_ERROR'; 33 | 34 | function actionType(t, id) { 35 | return t + '_' + id; 36 | } -------------------------------------------------------------------------------- /lib/actions/actions.js: -------------------------------------------------------------------------------- 1 | "use strict"; -------------------------------------------------------------------------------- /lib/actions/fetchingComposables.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = fetchingComposables; 7 | 8 | var _uuid = require('uuid'); 9 | 10 | var _uuid2 = _interopRequireDefault(_uuid); 11 | 12 | var _actionTypes = require('./actionTypes'); 13 | 14 | var actionTypes = _interopRequireWildcard(_actionTypes); 15 | 16 | var _pageInfoTranslator = require('../pageInfoTranslator'); 17 | 18 | var _stateManagement = require('../lib/stateManagement'); 19 | 20 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 21 | 22 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 23 | 24 | var fetcher = function fetcher(id) { 25 | return function (dispatch, getState) { 26 | var _listConfig = (0, _stateManagement.listConfig)(id), 27 | fetch = _listConfig.fetch, 28 | params = _listConfig.params; 29 | 30 | var pageInfo = (0, _stateManagement.getPaginator)(id, getState()); 31 | var requestId = _uuid2.default.v1(); 32 | 33 | dispatch({ type: (0, actionTypes.default)(actionTypes.FETCH_RECORDS, id), requestId: requestId }); 34 | 35 | var promise = dispatch(fetch((0, _pageInfoTranslator.translate)(pageInfo))); 36 | 37 | return promise.then(function (resp) { 38 | return dispatch({ 39 | type: (0, actionTypes.default)(actionTypes.RESULTS_UPDATED, id), 40 | results: resp.data[params.resultsProp], 41 | totalCount: resp.data[params.totalCountProp], 42 | requestId: requestId 43 | }); 44 | }).catch(function (error) { 45 | return dispatch({ 46 | type: (0, actionTypes.default)(actionTypes.RESULTS_UPDATED_ERROR, id), 47 | error: error 48 | }); 49 | }); 50 | }; 51 | }; 52 | 53 | function fetchingComposables(config) { 54 | var id = config.listId; 55 | var resolve = function resolve(t) { 56 | return (0, actionTypes.default)(t, id); 57 | }; 58 | 59 | return { 60 | initialize: function initialize() { 61 | return { 62 | type: resolve(actionTypes.INITIALIZE_PAGINATOR), 63 | preloaded: config.preloaded 64 | }; 65 | }, 66 | reload: function reload() { 67 | return fetcher(id); 68 | }, 69 | next: function next() { 70 | return { 71 | type: resolve(actionTypes.NEXT_PAGE) 72 | }; 73 | }, 74 | prev: function prev() { 75 | return { 76 | type: resolve(actionTypes.PREVIOUS_PAGE) 77 | }; 78 | }, 79 | goTo: function goTo(page) { 80 | return { 81 | type: resolve(actionTypes.GO_TO_PAGE), 82 | page: page 83 | }; 84 | }, 85 | setPageSize: function setPageSize(size) { 86 | return { 87 | type: resolve(actionTypes.SET_PAGE_SIZE), 88 | size: size 89 | }; 90 | }, 91 | toggleFilterItem: function toggleFilterItem(field, value) { 92 | return { 93 | type: resolve(actionTypes.TOGGLE_FILTER_ITEM), 94 | field: field, 95 | value: value 96 | }; 97 | }, 98 | setFilter: function setFilter(field, value) { 99 | return { 100 | type: resolve(actionTypes.SET_FILTER), 101 | field: field, 102 | value: value 103 | }; 104 | }, 105 | setFilters: function setFilters(filters) { 106 | return { 107 | type: resolve(actionTypes.SET_FILTERS), 108 | filters: filters 109 | }; 110 | }, 111 | resetFilters: function resetFilters(filters) { 112 | return { 113 | type: resolve(actionTypes.RESET_FILTERS), 114 | filters: filters 115 | }; 116 | }, 117 | sort: function sort(field, reverse) { 118 | return { 119 | type: resolve(actionTypes.SORT_CHANGED), 120 | field: field, 121 | reverse: reverse 122 | }; 123 | } 124 | }; 125 | } -------------------------------------------------------------------------------- /lib/actions/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | exports.expireAll = expireAll; 10 | exports.default = composables; 11 | 12 | var _actionTypes = require('./actionTypes'); 13 | 14 | var actionTypes = _interopRequireWildcard(_actionTypes); 15 | 16 | var _simpleComposables = require('./simpleComposables'); 17 | 18 | var _simpleComposables2 = _interopRequireDefault(_simpleComposables); 19 | 20 | var _fetchingComposables = require('./fetchingComposables'); 21 | 22 | var _fetchingComposables2 = _interopRequireDefault(_fetchingComposables); 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 27 | 28 | function expireAll() { 29 | return { 30 | type: actionTypes.EXPIRE_ALL 31 | }; 32 | } 33 | 34 | function composables(config) { 35 | return _extends({}, (0, _fetchingComposables2.default)(config), (0, _simpleComposables2.default)(config.listId)); 36 | } -------------------------------------------------------------------------------- /lib/actions/simpleComposables.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | exports.default = simpleComposables; 10 | 11 | var _immutable = require('immutable'); 12 | 13 | var _pageInfoTranslator = require('../pageInfoTranslator'); 14 | 15 | var _actionTypes = require('./actionTypes'); 16 | 17 | var actionTypes = _interopRequireWildcard(_actionTypes); 18 | 19 | var _stateManagement = require('../lib/stateManagement'); 20 | 21 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 22 | 23 | var _recordProps = (0, _pageInfoTranslator.recordProps)(), 24 | identifier = _recordProps.identifier; 25 | 26 | function simpleComposables(id) { 27 | var basic = { 28 | expire: function expire() { 29 | return { 30 | type: (0, actionTypes.default)(actionTypes.EXPIRE_PAGINATOR, id) 31 | }; 32 | }, 33 | updatingItem: function updatingItem(itemId) { 34 | return { 35 | type: (0, actionTypes.default)(actionTypes.UPDATING_ITEM, id), 36 | itemId: itemId 37 | }; 38 | }, 39 | updateItem: function updateItem(itemId, data) { 40 | return { 41 | type: (0, actionTypes.default)(actionTypes.UPDATE_ITEM, id), 42 | itemId: itemId, 43 | data: data 44 | }; 45 | }, 46 | updatingItems: function updatingItems(itemIds) { 47 | return { 48 | type: (0, actionTypes.default)(actionTypes.UPDATING_ITEMS, id), 49 | itemIds: itemIds 50 | }; 51 | }, 52 | updateItems: function updateItems(itemIds, data) { 53 | return { 54 | type: (0, actionTypes.default)(actionTypes.UPDATE_ITEMS, id), 55 | itemIds: itemIds, 56 | data: data 57 | }; 58 | }, 59 | resetItem: function resetItem(itemId, data) { 60 | return { 61 | type: (0, actionTypes.default)(actionTypes.RESET_ITEM, id), 62 | itemId: itemId, 63 | data: data 64 | }; 65 | }, 66 | updatingAll: function updatingAll() { 67 | return { 68 | type: (0, actionTypes.default)(actionTypes.UPDATING_ALL, id) 69 | }; 70 | }, 71 | updateAll: function updateAll(data) { 72 | return { 73 | type: (0, actionTypes.default)(actionTypes.UPDATE_ALL, id), 74 | data: data 75 | }; 76 | }, 77 | markItemsErrored: function markItemsErrored(itemIds, error) { 78 | return { 79 | type: (0, actionTypes.default)(actionTypes.MARK_ITEMS_ERRORED, id), 80 | itemIds: itemIds, 81 | error: error 82 | }; 83 | }, 84 | resetResults: function resetResults(results) { 85 | return { 86 | type: (0, actionTypes.default)(actionTypes.RESET_RESULTS, id), 87 | results: results 88 | }; 89 | }, 90 | removingItem: function removingItem(itemId) { 91 | return { 92 | type: (0, actionTypes.default)(actionTypes.REMOVING_ITEM, id), 93 | itemId: itemId 94 | }; 95 | }, 96 | removeItem: function removeItem(itemId) { 97 | return { 98 | type: (0, actionTypes.default)(actionTypes.REMOVE_ITEM, id), 99 | itemId: itemId 100 | }; 101 | }, 102 | itemError: function itemError(itemId, error) { 103 | return { 104 | type: (0, actionTypes.default)(actionTypes.ITEM_ERROR, id), 105 | itemId: itemId, 106 | error: error 107 | }; 108 | } 109 | }; 110 | 111 | var updateAsync = function updateAsync(itemId, data, update) { 112 | return function (dispatch, getState) { 113 | var item = (0, _stateManagement.getPaginator)(id, getState()).get('results').find(function (r) { 114 | return r.get(identifier) === itemId; 115 | }) || (0, _immutable.Map)(); 116 | 117 | dispatch(basic.updateItem(itemId, data)); 118 | dispatch(basic.updatingItem(itemId)); 119 | return update.then(function (serverUpdate) { 120 | return dispatch(basic.updateItem(itemId, serverUpdate)); 121 | }).catch(function (err) { 122 | dispatch(basic.resetItem(itemId, item.toJS())); 123 | return dispatch(basic.itemError(itemId, err)); 124 | }); 125 | }; 126 | }; 127 | 128 | var updateItemsAsync = function updateItemsAsync(itemIds, data, update) { 129 | var showUpdating = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; 130 | return function (dispatch, getState) { 131 | var results = (0, _stateManagement.getPaginator)(id, getState()).get('results'); 132 | 133 | dispatch(basic.updateItems(itemIds, data)); 134 | if (showUpdating) { 135 | dispatch(basic.updatingItems(itemIds)); 136 | } 137 | 138 | return update.then(function (resp) { 139 | if (showUpdating) { 140 | dispatch(basic.updateItems(itemIds, data)); 141 | } 142 | 143 | return resp; 144 | }).catch(function (err) { 145 | dispatch(basic.resetResults(results.toJS())); 146 | return dispatch(basic.markItemsErrored(itemIds, err)); 147 | }); 148 | }; 149 | }; 150 | 151 | var removeAsync = function removeAsync(itemId, remove) { 152 | return function (dispatch, getState) { 153 | var item = (0, _stateManagement.getPaginator)(id, getState()).get('results').find(function (r) { 154 | return r.get(identifier) === itemId; 155 | }) || (0, _immutable.Map)(); 156 | 157 | dispatch(basic.removingItem(itemId)); 158 | return remove.then(function () { 159 | return dispatch(basic.removeItem(itemId)); 160 | }).catch(function (err) { 161 | dispatch(basic.resetItem(itemId, item.toJS())); 162 | return dispatch(basic.itemError(itemId, err)); 163 | }); 164 | }; 165 | }; 166 | 167 | return _extends({}, basic, { 168 | updateAsync: updateAsync, 169 | updateItemsAsync: updateItemsAsync, 170 | removeAsync: removeAsync 171 | }); 172 | } -------------------------------------------------------------------------------- /lib/containers/PaginationWrapper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.PaginationWrapper = exports.connector = undefined; 7 | 8 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 9 | 10 | var _react = require('react'); 11 | 12 | var _reactRedux = require('react-redux'); 13 | 14 | var _redux = require('redux'); 15 | 16 | var _actions = require('../actions'); 17 | 18 | var _actions2 = _interopRequireDefault(_actions); 19 | 20 | var _reducer = require('../reducer'); 21 | 22 | var _stateManagement = require('../lib/stateManagement'); 23 | 24 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 25 | 26 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 27 | 28 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 29 | 30 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 31 | 32 | var connector = exports.connector = (0, _reactRedux.connect)(function (state, ownProps) { 33 | return { 34 | paginator: (0, _stateManagement.preloadedPaginator)(state, ownProps.listId, ownProps.preloaded) 35 | }; 36 | }, function (dispatch, ownProps) { 37 | return { 38 | pageActions: (0, _redux.bindActionCreators)((0, _actions2.default)(ownProps), dispatch) 39 | }; 40 | }); 41 | 42 | var PaginationWrapper = exports.PaginationWrapper = function (_Component) { 43 | _inherits(PaginationWrapper, _Component); 44 | 45 | function PaginationWrapper() { 46 | _classCallCheck(this, PaginationWrapper); 47 | 48 | return _possibleConstructorReturn(this, (PaginationWrapper.__proto__ || Object.getPrototypeOf(PaginationWrapper)).apply(this, arguments)); 49 | } 50 | 51 | _createClass(PaginationWrapper, [{ 52 | key: 'componentDidMount', 53 | value: function componentDidMount() { 54 | var _props = this.props, 55 | paginator = _props.paginator, 56 | pageActions = _props.pageActions; 57 | 58 | 59 | if (!paginator.get('initialized')) { 60 | pageActions.initialize(); 61 | } 62 | 63 | this.reloadIfStale(this.props); 64 | } 65 | }, { 66 | key: 'componentWillReceiveProps', 67 | value: function componentWillReceiveProps(nextProps) { 68 | this.reloadIfStale(nextProps); 69 | } 70 | }, { 71 | key: 'reloadIfStale', 72 | value: function reloadIfStale(props) { 73 | var paginator = props.paginator, 74 | pageActions = props.pageActions; 75 | 76 | if (paginator.get('stale') && !paginator.get('isLoading') && !paginator.get('loadError')) { 77 | pageActions.reload(); 78 | } 79 | } 80 | }, { 81 | key: 'render', 82 | value: function render() { 83 | return this.props.children; 84 | } 85 | }]); 86 | 87 | return PaginationWrapper; 88 | }(_react.Component); 89 | 90 | PaginationWrapper.propTypes = { 91 | pageActions: _react.PropTypes.object.isRequired, 92 | paginator: _react.PropTypes.object, 93 | children: _react.PropTypes.element.isRequired 94 | }; 95 | PaginationWrapper.defaultProps = { 96 | paginator: _reducer.defaultPaginator 97 | }; -------------------------------------------------------------------------------- /lib/decorators/decorate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | exports.default = decorate; 10 | 11 | var _react = require('react'); 12 | 13 | var _react2 = _interopRequireDefault(_react); 14 | 15 | var _PaginationWrapper = require('../containers/PaginationWrapper'); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function decorate(Component, decorator) { 20 | return (0, _PaginationWrapper.connector)(function (props) { 21 | return _react2.default.createElement( 22 | _PaginationWrapper.PaginationWrapper, 23 | props, 24 | _react2.default.createElement(Component, _extends({}, props, decorator(props))) 25 | ); 26 | }); 27 | } -------------------------------------------------------------------------------- /lib/decorators/flip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = flip; 7 | 8 | var _decorate = require('./decorate'); 9 | 10 | var _decorate2 = _interopRequireDefault(_decorate); 11 | 12 | var _selectors = require('./selectors'); 13 | 14 | var _selectors2 = _interopRequireDefault(_selectors); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | function flip(Component) { 19 | return (0, _decorate2.default)(Component, function (props) { 20 | return (0, _selectors2.default)(props.paginator).flip(); 21 | }); 22 | } -------------------------------------------------------------------------------- /lib/decorators/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.violetPaginator = exports.tabulate = exports.sort = exports.stretch = exports.paginate = exports.flip = exports.decorate = undefined; 7 | 8 | var _decorate2 = require('./decorate'); 9 | 10 | var _decorate3 = _interopRequireDefault(_decorate2); 11 | 12 | var _flip2 = require('./flip'); 13 | 14 | var _flip3 = _interopRequireDefault(_flip2); 15 | 16 | var _paginate2 = require('./paginate'); 17 | 18 | var _paginate3 = _interopRequireDefault(_paginate2); 19 | 20 | var _stretch2 = require('./stretch'); 21 | 22 | var _stretch3 = _interopRequireDefault(_stretch2); 23 | 24 | var _sort2 = require('./sort'); 25 | 26 | var _sort3 = _interopRequireDefault(_sort2); 27 | 28 | var _tabulate2 = require('./tabulate'); 29 | 30 | var _tabulate3 = _interopRequireDefault(_tabulate2); 31 | 32 | var _violetPaginator2 = require('./violetPaginator'); 33 | 34 | var _violetPaginator3 = _interopRequireDefault(_violetPaginator2); 35 | 36 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 37 | 38 | exports.decorate = _decorate3.default; 39 | exports.flip = _flip3.default; 40 | exports.paginate = _paginate3.default; 41 | exports.stretch = _stretch3.default; 42 | exports.sort = _sort3.default; 43 | exports.tabulate = _tabulate3.default; 44 | exports.violetPaginator = _violetPaginator3.default; -------------------------------------------------------------------------------- /lib/decorators/paginate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = paginate; 7 | 8 | var _decorate = require('./decorate'); 9 | 10 | var _decorate2 = _interopRequireDefault(_decorate); 11 | 12 | var _selectors = require('./selectors'); 13 | 14 | var _selectors2 = _interopRequireDefault(_selectors); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | function paginate(Component) { 19 | return (0, _decorate2.default)(Component, function (props) { 20 | return (0, _selectors2.default)(props.paginator).paginate(); 21 | }); 22 | } -------------------------------------------------------------------------------- /lib/decorators/selectors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | exports.default = select; 10 | function select(paginator) { 11 | var totalPages = Math.ceil(paginator.get('totalCount') / paginator.get('pageSize')); 12 | 13 | var page = paginator.get('page'); 14 | 15 | var flip = function flip() { 16 | return { 17 | hasPreviousPage: page > 1, 18 | hasNextPage: page < totalPages 19 | }; 20 | }; 21 | 22 | var paginate = function paginate() { 23 | return _extends({ 24 | currentPage: page, 25 | totalPages: totalPages 26 | }, flip()); 27 | }; 28 | 29 | var tabulate = function tabulate() { 30 | return { 31 | results: paginator.get('results'), 32 | isLoading: paginator.get('isLoading'), 33 | updating: paginator.get('updating'), 34 | removing: paginator.get('removing') 35 | }; 36 | }; 37 | 38 | var stretch = function stretch() { 39 | return { 40 | pageSize: paginator.get('pageSize') 41 | }; 42 | }; 43 | 44 | var sort = function sort() { 45 | return { 46 | sort: paginator.get('sort'), 47 | sortReverse: paginator.get('sortReverse') 48 | }; 49 | }; 50 | 51 | var violetPaginator = function violetPaginator() { 52 | return _extends({}, paginate(), tabulate(), stretch(), sort()); 53 | }; 54 | 55 | return { 56 | flip: flip, 57 | paginate: paginate, 58 | tabulate: tabulate, 59 | stretch: stretch, 60 | sort: sort, 61 | violetPaginator: violetPaginator 62 | }; 63 | } -------------------------------------------------------------------------------- /lib/decorators/sort.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = sort; 7 | 8 | var _decorate = require('./decorate'); 9 | 10 | var _decorate2 = _interopRequireDefault(_decorate); 11 | 12 | var _selectors = require('./selectors'); 13 | 14 | var _selectors2 = _interopRequireDefault(_selectors); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | function sort(Component) { 19 | return (0, _decorate2.default)(Component, function (props) { 20 | return (0, _selectors2.default)(props.paginator).sort(); 21 | }); 22 | } -------------------------------------------------------------------------------- /lib/decorators/stretch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = stretch; 7 | 8 | var _decorate = require('./decorate'); 9 | 10 | var _decorate2 = _interopRequireDefault(_decorate); 11 | 12 | var _selectors = require('./selectors'); 13 | 14 | var _selectors2 = _interopRequireDefault(_selectors); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | function stretch(Component) { 19 | return (0, _decorate2.default)(Component, function (props) { 20 | return (0, _selectors2.default)(props.paginator).stretch(); 21 | }); 22 | } -------------------------------------------------------------------------------- /lib/decorators/tabulate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = tabulate; 7 | 8 | var _decorate = require('./decorate'); 9 | 10 | var _decorate2 = _interopRequireDefault(_decorate); 11 | 12 | var _selectors = require('./selectors'); 13 | 14 | var _selectors2 = _interopRequireDefault(_selectors); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | function tabulate(Component) { 19 | return (0, _decorate2.default)(Component, function (props) { 20 | return (0, _selectors2.default)(props.paginator).tabulate(); 21 | }); 22 | } -------------------------------------------------------------------------------- /lib/decorators/violetPaginator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = violetPaginator; 7 | 8 | var _decorate = require('./decorate'); 9 | 10 | var _decorate2 = _interopRequireDefault(_decorate); 11 | 12 | var _selectors = require('./selectors'); 13 | 14 | var _selectors2 = _interopRequireDefault(_selectors); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | function violetPaginator(Component) { 19 | return (0, _decorate2.default)(Component, function (props) { 20 | return (0, _selectors2.default)(props.paginator).violetPaginator(); 21 | }); 22 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.currentQuery = exports.isRemoving = exports.isUpdating = exports.configurePageParams = exports.decorators = exports.createPaginator = exports.VioletPageSizeDropdown = exports.VioletNext = exports.VioletPrev = exports.VioletSortLink = exports.VioletPaginator = exports.VioletFlipper = exports.VioletDataTable = exports.expireAll = exports.composables = undefined; 7 | 8 | var _actions = require('./actions'); 9 | 10 | Object.defineProperty(exports, 'expireAll', { 11 | enumerable: true, 12 | get: function get() { 13 | return _actions.expireAll; 14 | } 15 | }); 16 | 17 | var _pageInfoTranslator = require('./pageInfoTranslator'); 18 | 19 | Object.defineProperty(exports, 'configurePageParams', { 20 | enumerable: true, 21 | get: function get() { 22 | return _pageInfoTranslator.configurePageParams; 23 | } 24 | }); 25 | 26 | var _stateManagement = require('./lib/stateManagement'); 27 | 28 | Object.defineProperty(exports, 'isUpdating', { 29 | enumerable: true, 30 | get: function get() { 31 | return _stateManagement.isUpdating; 32 | } 33 | }); 34 | Object.defineProperty(exports, 'isRemoving', { 35 | enumerable: true, 36 | get: function get() { 37 | return _stateManagement.isRemoving; 38 | } 39 | }); 40 | Object.defineProperty(exports, 'currentQuery', { 41 | enumerable: true, 42 | get: function get() { 43 | return _stateManagement.currentQuery; 44 | } 45 | }); 46 | 47 | var _actions2 = _interopRequireDefault(_actions); 48 | 49 | var _DataTable = require('./DataTable'); 50 | 51 | var _DataTable2 = _interopRequireDefault(_DataTable); 52 | 53 | var _Flipper = require('./Flipper'); 54 | 55 | var _Flipper2 = _interopRequireDefault(_Flipper); 56 | 57 | var _Paginator = require('./Paginator'); 58 | 59 | var _Paginator2 = _interopRequireDefault(_Paginator); 60 | 61 | var _SortLink = require('./SortLink'); 62 | 63 | var _SortLink2 = _interopRequireDefault(_SortLink); 64 | 65 | var _Prev = require('./Prev'); 66 | 67 | var _Prev2 = _interopRequireDefault(_Prev); 68 | 69 | var _Next = require('./Next'); 70 | 71 | var _Next2 = _interopRequireDefault(_Next); 72 | 73 | var _PageSizeDropdown = require('./PageSizeDropdown'); 74 | 75 | var _PageSizeDropdown2 = _interopRequireDefault(_PageSizeDropdown); 76 | 77 | var _reducer = require('./reducer'); 78 | 79 | var _reducer2 = _interopRequireDefault(_reducer); 80 | 81 | var _decorators2 = require('./decorators'); 82 | 83 | var _decorators = _interopRequireWildcard(_decorators2); 84 | 85 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 86 | 87 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 88 | 89 | exports.composables = _actions2.default; 90 | exports.VioletDataTable = _DataTable2.default; 91 | exports.VioletFlipper = _Flipper2.default; 92 | exports.VioletPaginator = _Paginator2.default; 93 | exports.VioletSortLink = _SortLink2.default; 94 | exports.VioletPrev = _Prev2.default; 95 | exports.VioletNext = _Next2.default; 96 | exports.VioletPageSizeDropdown = _PageSizeDropdown2.default; 97 | exports.createPaginator = _reducer2.default; 98 | exports.decorators = _decorators; -------------------------------------------------------------------------------- /lib/lib/range.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = range; 7 | 8 | require('babel-regenerator-runtime'); 9 | 10 | var _marked = [range].map(regeneratorRuntime.mark); 11 | 12 | function range(low, high) { 13 | var i; 14 | return regeneratorRuntime.wrap(function range$(_context) { 15 | while (1) { 16 | switch (_context.prev = _context.next) { 17 | case 0: 18 | i = low; 19 | 20 | case 1: 21 | if (!(i <= high)) { 22 | _context.next = 7; 23 | break; 24 | } 25 | 26 | _context.next = 4; 27 | return i; 28 | 29 | case 4: 30 | i++; 31 | _context.next = 1; 32 | break; 33 | 34 | case 7: 35 | case 'end': 36 | return _context.stop(); 37 | } 38 | } 39 | }, _marked[0], this); 40 | } -------------------------------------------------------------------------------- /lib/lib/reduxResolver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.updateListItem = updateListItem; 7 | function updateListItem(list, id, update) { 8 | var identifier = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'id'; 9 | 10 | return list.map(function (i) { 11 | if (i.get(identifier) === id) { 12 | return update(i); 13 | } 14 | 15 | return i; 16 | }); 17 | } -------------------------------------------------------------------------------- /lib/lib/stateManagement.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 10 | 11 | exports.registerPaginator = registerPaginator; 12 | exports.getPaginator = getPaginator; 13 | exports.listConfig = listConfig; 14 | exports.preloadedPaginator = preloadedPaginator; 15 | exports.isUpdating = isUpdating; 16 | exports.isRemoving = isRemoving; 17 | exports.currentQuery = currentQuery; 18 | 19 | var _reducer = require('../reducer'); 20 | 21 | var _pageInfoTranslator = require('../pageInfoTranslator'); 22 | 23 | var stateMap = {}; 24 | var defaultLocator = function defaultLocator(listId) { 25 | return function (state) { 26 | return state[listId]; 27 | }; 28 | }; 29 | var preload = { results: [] }; 30 | 31 | var defaultPageParams = function defaultPageParams() { 32 | var _responseProps = (0, _pageInfoTranslator.responseProps)(), 33 | _responseProps2 = _slicedToArray(_responseProps, 2), 34 | totalCountProp = _responseProps2[0], 35 | resultsProp = _responseProps2[1]; 36 | 37 | return { 38 | totalCountProp: totalCountProp, 39 | resultsProp: resultsProp 40 | }; 41 | }; 42 | 43 | function registerPaginator(_ref) { 44 | var listId = _ref.listId, 45 | fetch = _ref.fetch, 46 | _ref$initialSettings = _ref.initialSettings, 47 | initialSettings = _ref$initialSettings === undefined ? {} : _ref$initialSettings, 48 | _ref$pageParams = _ref.pageParams, 49 | pageParams = _ref$pageParams === undefined ? {} : _ref$pageParams, 50 | _ref$locator = _ref.locator, 51 | locator = _ref$locator === undefined ? defaultLocator(listId) : _ref$locator; 52 | 53 | stateMap[listId] = { 54 | locator: locator, 55 | fetch: fetch, 56 | initialSettings: initialSettings, 57 | params: _extends({}, defaultPageParams(), pageParams) 58 | }; 59 | 60 | return stateMap[listId]; 61 | } 62 | 63 | function getPaginator(listId, state) { 64 | var config = stateMap[listId] || { 65 | locator: defaultLocator(listId) 66 | }; 67 | 68 | return config.locator(state) || _reducer.defaultPaginator; 69 | } 70 | 71 | function listConfig(listId) { 72 | return stateMap[listId]; 73 | } 74 | 75 | function preloadedPaginator(state, listId) { 76 | var preloaded = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : preload; 77 | 78 | var paginator = getPaginator(listId, state); 79 | return paginator.equals(_reducer.defaultPaginator) ? paginator.merge(preloaded) : paginator; 80 | } 81 | 82 | function isUpdating(state, listId, itemId) { 83 | var paginator = getPaginator(listId, state); 84 | return paginator.get('updating').includes(itemId); 85 | } 86 | 87 | function isRemoving(state, itemId) { 88 | return state.get('removing').includes(itemId); 89 | } 90 | 91 | function currentQuery(state, listId) { 92 | return (0, _pageInfoTranslator.translate)(getPaginator(listId, state)); 93 | } -------------------------------------------------------------------------------- /lib/pageInfoTranslator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | exports.responseProps = responseProps; 10 | exports.recordProps = recordProps; 11 | exports.configurePageParams = configurePageParams; 12 | exports.translate = translate; 13 | 14 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 15 | 16 | var pageParam = 'page', 17 | pageSizeParam = 'pageSize', 18 | sortParam = 'sort', 19 | sortOrderParam = 'sortOrder', 20 | useBooleanOrdering = false, 21 | totalCountProp = 'total_count', 22 | resultsProp = 'results', 23 | idProp = 'id'; 24 | function responseProps() { 25 | return [totalCountProp, resultsProp]; 26 | } 27 | 28 | function recordProps() { 29 | return { identifier: idProp }; 30 | } 31 | 32 | function configurePageParams(_ref) { 33 | var page = _ref.page, 34 | perPage = _ref.perPage, 35 | sort = _ref.sort, 36 | sortOrder = _ref.sortOrder, 37 | sortReverse = _ref.sortReverse, 38 | totalCount = _ref.totalCount, 39 | results = _ref.results, 40 | id = _ref.id; 41 | 42 | if (page) { 43 | pageParam = page; 44 | } 45 | 46 | if (perPage) { 47 | pageSizeParam = perPage; 48 | } 49 | 50 | if (sort) { 51 | sortParam = sort; 52 | } 53 | 54 | if (sortOrder) { 55 | sortOrderParam = sortOrder; 56 | } 57 | 58 | if (totalCount) { 59 | totalCountProp = totalCount; 60 | } 61 | 62 | if (results) { 63 | resultsProp = results; 64 | } 65 | 66 | if (id) { 67 | idProp = id; 68 | } 69 | 70 | useBooleanOrdering = !!sortReverse; 71 | } 72 | 73 | function sortDirection(value) { 74 | if (useBooleanOrdering) { 75 | return value; 76 | } 77 | 78 | return value ? 'desc' : 'asc'; 79 | } 80 | 81 | function sortParams(paginator) { 82 | if (paginator.get('sort')) { 83 | var _ref2; 84 | 85 | return _ref2 = {}, _defineProperty(_ref2, sortParam, paginator.get('sort')), _defineProperty(_ref2, sortOrderParam, sortDirection(paginator.get('sortReverse'))), _ref2; 86 | } 87 | 88 | return {}; 89 | } 90 | 91 | function translate(paginator) { 92 | var _extends2; 93 | 94 | var _paginator$toJS = paginator.toJS(), 95 | id = _paginator$toJS.id, 96 | page = _paginator$toJS.page, 97 | pageSize = _paginator$toJS.pageSize, 98 | filters = _paginator$toJS.filters; 99 | 100 | return { 101 | id: id, 102 | query: _extends((_extends2 = {}, _defineProperty(_extends2, pageParam, page), _defineProperty(_extends2, pageSizeParam, pageSize), _extends2), sortParams(paginator), filters) 103 | }; 104 | } -------------------------------------------------------------------------------- /lib/reducer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.defaultPaginator = undefined; 7 | 8 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 9 | 10 | exports.default = createPaginator; 11 | 12 | var _immutable = require('immutable'); 13 | 14 | var _immutable2 = _interopRequireDefault(_immutable); 15 | 16 | var _reduxResolver = require('redux-resolver'); 17 | 18 | var _reduxResolver2 = require('./lib/reduxResolver'); 19 | 20 | var _actionTypes = require('./actions/actionTypes'); 21 | 22 | var actionTypes = _interopRequireWildcard(_actionTypes); 23 | 24 | var _pageInfoTranslator = require('./pageInfoTranslator'); 25 | 26 | var _stateManagement = require('./lib/stateManagement'); 27 | 28 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 29 | 30 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 31 | 32 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 33 | 34 | var defaultPaginator = exports.defaultPaginator = (0, _immutable.Map)({ 35 | initialized: false, 36 | page: 1, 37 | pageSize: 15, 38 | totalCount: 0, 39 | sort: '', 40 | sortReverse: false, 41 | isLoading: false, 42 | stale: false, 43 | results: (0, _immutable.List)(), 44 | updating: (0, _immutable.Set)(), 45 | removing: (0, _immutable.Set)(), 46 | requestId: null, 47 | loadError: null, 48 | filters: (0, _immutable.Map)() 49 | }); 50 | 51 | function initialize(state, action) { 52 | return state.merge(_extends({ 53 | initialized: true, 54 | stale: !action.preloaded 55 | }, action.preloaded || {})); 56 | } 57 | 58 | function expire(state) { 59 | return state.merge({ stale: true, loadError: null }); 60 | } 61 | 62 | function next(state) { 63 | return expire(state.set('page', state.get('page') + 1)); 64 | } 65 | 66 | function prev(state) { 67 | return expire(state.set('page', state.get('page') - 1)); 68 | } 69 | 70 | function goToPage(state, action) { 71 | return expire(state.set('page', action.page)); 72 | } 73 | 74 | function setPageSize(state, action) { 75 | return expire(state.merge({ 76 | pageSize: action.size, 77 | page: 1 78 | })); 79 | } 80 | 81 | function toggleFilterItem(state, action) { 82 | var items = state.getIn(['filters', action.field], (0, _immutable.Set)()).toSet(); 83 | 84 | return expire(state.set('page', 1).setIn(['filters', action.field], items.includes(action.value) ? items.delete(action.value) : items.add(action.value))); 85 | } 86 | 87 | function setFilter(state, action) { 88 | return expire(state.setIn(['filters', action.field], _immutable2.default.fromJS(action.value)).set('page', 1)); 89 | } 90 | 91 | function setFilters(state, action) { 92 | return expire(state.set('filters', state.get('filters').merge(action.filters)).set('page', 1)); 93 | } 94 | 95 | function resetFilters(state, action) { 96 | return expire(state.set('filters', _immutable2.default.fromJS(action.filters || {})).set('page', 1)); 97 | } 98 | 99 | function sortChanged(state, action) { 100 | return expire(state.merge({ 101 | sort: action.field, 102 | sortReverse: action.reverse, 103 | page: 1 104 | })); 105 | } 106 | 107 | function fetching(state, action) { 108 | return state.merge({ 109 | isLoading: true, 110 | requestId: action.requestId 111 | }); 112 | } 113 | 114 | function updateResults(state, action) { 115 | if (action.requestId !== state.get('requestId')) { 116 | return state; 117 | } 118 | 119 | return state.merge({ 120 | results: _immutable2.default.fromJS(action.results), 121 | totalCount: action.totalCount, 122 | isLoading: false, 123 | stale: false 124 | }); 125 | } 126 | 127 | function resetResults(state, action) { 128 | return state.set('results', _immutable2.default.fromJS(action.results)); 129 | } 130 | 131 | function error(state, action) { 132 | return state.merge({ 133 | isLoading: false, 134 | loadError: action.error 135 | }); 136 | } 137 | 138 | function updatingItem(state, action) { 139 | return state.set('updating', state.get('updating').add(action.itemId)); 140 | } 141 | 142 | function updateItem(state, action) { 143 | return state.merge({ 144 | updating: state.get('updating').toSet().delete(action.itemId), 145 | results: (0, _reduxResolver2.updateListItem)(state.get('results'), action.itemId, function (item) { 146 | return item.merge(action.data).set('error', null); 147 | }, (0, _pageInfoTranslator.recordProps)().identifier) 148 | }); 149 | } 150 | 151 | function updateItems(state, action) { 152 | var itemIds = action.itemIds; 153 | 154 | 155 | return state.merge({ 156 | updating: state.get('updating').toSet().subtract(itemIds), 157 | results: state.get('results').map(function (r) { 158 | if (itemIds.includes(r.get((0, _pageInfoTranslator.recordProps)().identifier))) { 159 | return r.merge(action.data).set('error', null); 160 | } 161 | 162 | return r; 163 | }) 164 | }); 165 | } 166 | 167 | function updatingItems(state, action) { 168 | var itemIds = action.itemIds; 169 | 170 | 171 | return state.set('updating', state.get('updating').toSet().union(itemIds)); 172 | } 173 | 174 | function resetItem(state, action) { 175 | return state.merge({ 176 | updating: state.get('updating').toSet().delete(action.itemId), 177 | results: (0, _reduxResolver2.updateListItem)(state.get('results'), action.itemId, function () { 178 | return _immutable2.default.fromJS(action.data); 179 | }, (0, _pageInfoTranslator.recordProps)().identifier) 180 | }); 181 | } 182 | 183 | function removingItem(state, action) { 184 | return state.set('removing', state.get('removing').add(action.itemId)); 185 | } 186 | 187 | function removeItem(state, action) { 188 | return state.merge({ 189 | totalCount: state.get('totalCount') - 1, 190 | removing: state.get('removing').toSet().delete(action.itemId), 191 | results: state.get('results').filter(function (item) { 192 | return item.get((0, _pageInfoTranslator.recordProps)().identifier) !== action.itemId; 193 | }) 194 | }); 195 | } 196 | 197 | function itemError(state, action) { 198 | return state.merge({ 199 | updating: state.get('updating').toSet().delete(action.itemId), 200 | removing: state.get('removing').toSet().delete(action.itemId), 201 | results: (0, _reduxResolver2.updateListItem)(state.get('results'), action.itemId, function (item) { 202 | return item.set('error', _immutable2.default.fromJS(action.error)); 203 | }, (0, _pageInfoTranslator.recordProps)().identifier) 204 | }); 205 | } 206 | 207 | function markItemsErrored(state, action) { 208 | var itemIds = action.itemIds; 209 | 210 | 211 | return state.merge({ 212 | updating: state.get('updating').toSet().subtract(itemIds), 213 | removing: state.get('removing').toSet().subtract(itemIds), 214 | results: state.get('results').map(function (r) { 215 | if (itemIds.includes(r.get((0, _pageInfoTranslator.recordProps)().identifier))) { 216 | return r.set('error', _immutable2.default.fromJS(action.error)); 217 | } 218 | 219 | return r; 220 | }) 221 | }); 222 | } 223 | 224 | function createPaginator(config) { 225 | var _resolveEach; 226 | 227 | var _registerPaginator = (0, _stateManagement.registerPaginator)(config), 228 | initialSettings = _registerPaginator.initialSettings; 229 | 230 | var resolve = function resolve(t) { 231 | return (0, actionTypes.default)(t, config.listId); 232 | }; 233 | 234 | return (0, _reduxResolver.resolveEach)(defaultPaginator.merge(initialSettings), (_resolveEach = {}, _defineProperty(_resolveEach, actionTypes.EXPIRE_ALL, expire), _defineProperty(_resolveEach, resolve(actionTypes.INITIALIZE_PAGINATOR), initialize), _defineProperty(_resolveEach, resolve(actionTypes.EXPIRE_PAGINATOR), expire), _defineProperty(_resolveEach, resolve(actionTypes.PREVIOUS_PAGE), prev), _defineProperty(_resolveEach, resolve(actionTypes.NEXT_PAGE), next), _defineProperty(_resolveEach, resolve(actionTypes.GO_TO_PAGE), goToPage), _defineProperty(_resolveEach, resolve(actionTypes.SET_PAGE_SIZE), setPageSize), _defineProperty(_resolveEach, resolve(actionTypes.FETCH_RECORDS), fetching), _defineProperty(_resolveEach, resolve(actionTypes.RESULTS_UPDATED), updateResults), _defineProperty(_resolveEach, resolve(actionTypes.RESULTS_UPDATED_ERROR), error), _defineProperty(_resolveEach, resolve(actionTypes.TOGGLE_FILTER_ITEM), toggleFilterItem), _defineProperty(_resolveEach, resolve(actionTypes.SET_FILTER), setFilter), _defineProperty(_resolveEach, resolve(actionTypes.SET_FILTERS), setFilters), _defineProperty(_resolveEach, resolve(actionTypes.RESET_FILTERS), resetFilters), _defineProperty(_resolveEach, resolve(actionTypes.SORT_CHANGED), sortChanged), _defineProperty(_resolveEach, resolve(actionTypes.UPDATING_ITEM), updatingItem), _defineProperty(_resolveEach, resolve(actionTypes.UPDATE_ITEM), updateItem), _defineProperty(_resolveEach, resolve(actionTypes.UPDATING_ITEMS), updatingItems), _defineProperty(_resolveEach, resolve(actionTypes.UPDATE_ITEMS), updateItems), _defineProperty(_resolveEach, resolve(actionTypes.RESET_ITEM), resetItem), _defineProperty(_resolveEach, resolve(actionTypes.MARK_ITEMS_ERRORED), markItemsErrored), _defineProperty(_resolveEach, resolve(actionTypes.RESET_RESULTS), resetResults), _defineProperty(_resolveEach, resolve(actionTypes.REMOVING_ITEM), removingItem), _defineProperty(_resolveEach, resolve(actionTypes.REMOVE_ITEM), removeItem), _defineProperty(_resolveEach, resolve(actionTypes.ITEM_ERROR), itemError), _resolveEach)); 235 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "violet-paginator", 3 | "version": "2.0.5", 4 | "description": "Display, paginate, sort, filter, and update items from the server. violet-paginator is a complete list management library for react/redux applications.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "spec": "mocha './spec/**/*.spec.js*' --compilers js:babel-register --recursive", 8 | "spec-coverage": "nyc --require babel-core/register mocha --recursive './spec/**/*.spec.js*'", 9 | "lint": "eslint ./src/** ./spec/**/* --ext=js,jsx", 10 | "test": "npm run lint && npm run spec-coverage", 11 | "compile": "babel -d lib/ src/", 12 | "prepublish": "npm run compile" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/sslotsky/violet-paginator.git" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "redux", 21 | "pagination", 22 | "paginate", 23 | "paginator", 24 | "list management", 25 | "filter", 26 | "sort", 27 | "violet" 28 | ], 29 | "nyc": { 30 | "extension": [ 31 | ".jsx" 32 | ], 33 | "exclude": [ 34 | "examples", 35 | "build" 36 | ], 37 | "reporter": [ 38 | "lcov", 39 | "text-summary" 40 | ] 41 | }, 42 | "author": "Sam Slotsky", 43 | "license": "MIT", 44 | "dependencies": { 45 | "babel-regenerator-runtime": "^6.5.0", 46 | "classnames": "^2.2.5", 47 | "react-fontawesome": "^1.1.0", 48 | "redux-resolver": "^1.0.2", 49 | "uuid": "^3.0.1" 50 | }, 51 | "peerDependencies": { 52 | "immutable": "^3.7.6", 53 | "react": "^0.14.8 || ^15.1.0", 54 | "react-redux": "^4.4.4 || 5.x", 55 | "redux": "^3.4.0" 56 | }, 57 | "devDependencies": { 58 | "babel-cli": "^6.14.0", 59 | "babel-core": "^6.13.2", 60 | "babel-eslint": "6.1.2", 61 | "babel-loader": "^6.2.4", 62 | "babel-preset-es2015": "^6.13.2", 63 | "babel-preset-react": "^6.11.1", 64 | "babel-preset-stage-0": "^6.5.0", 65 | "babel-register": "^6.11.6", 66 | "enzyme": "^2.4.1", 67 | "eslint": "3.3.1", 68 | "eslint-config-airbnb": "10.0.0", 69 | "eslint-plugin-import": "1.13.0", 70 | "eslint-plugin-jsx-a11y": "2.1.0", 71 | "eslint-plugin-react": "6.0.0", 72 | "expect": "^1.20.2", 73 | "immutable": "^3.7.6", 74 | "istanbul": "^0.4.5", 75 | "jsdom": "^9.8.3", 76 | "mocha": "^3.0.2", 77 | "nyc": "^10.0.0", 78 | "promise-mock": "^1.1.0", 79 | "react": "^15.1.0", 80 | "react-addons-test-utils": "^15.3.1", 81 | "react-dom": "^15.3.1", 82 | "react-redux": "^4.4.5", 83 | "redux": "^3.4.0", 84 | "redux-mock-store": "^1.1.4", 85 | "redux-thunk": "^2.1.0", 86 | "webpack": "^1.13.1" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /spec/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /spec/Next.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import expect from 'expect' 3 | import { shallow } from 'enzyme' 4 | import FontAwesome from 'react-fontawesome' 5 | import { Next } from '../src/Next' 6 | 7 | function verifyIcon(node) { 8 | const { 9 | type, 10 | props: { name } 11 | } = node 12 | 13 | expect([ 14 | type, 15 | name 16 | ]).toEqual([ 17 | FontAwesome, 18 | 'chevron-right' 19 | ]) 20 | } 21 | 22 | describe('', () => { 23 | context('when no next page exists', () => { 24 | const wrapper = shallow( 25 | 26 | ) 27 | 28 | it('renders an icon', () => { 29 | verifyIcon(wrapper.node) 30 | }) 31 | }) 32 | 33 | context('when next page exists', () => { 34 | const pageActions = { 35 | next: expect.createSpy() 36 | } 37 | 38 | const wrapper = shallow( 39 | 40 | ) 41 | 42 | it('renders an anchor', () => { 43 | expect(wrapper.node.type).toEqual('a') 44 | expect(wrapper.find('a').length).toEqual(1) 45 | }) 46 | 47 | it('renders an icon inside the anchor', () => { 48 | const icon = wrapper.node.props.children 49 | verifyIcon(icon) 50 | }) 51 | 52 | context('when clicking the link', () => { 53 | wrapper.find('a').simulate('click') 54 | it('calls the prev action', () => { 55 | expect(pageActions.next).toHaveBeenCalled() 56 | }) 57 | }) 58 | }) 59 | }) 60 | 61 | -------------------------------------------------------------------------------- /spec/PageLink.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import expect from 'expect' 3 | import { shallow } from 'enzyme' 4 | import { PageLink } from '../src/PageLink' 5 | 6 | function verifyPageNumber(node, pageNumber) { 7 | const { type, props: { children } } = node 8 | expect([ 9 | type, 10 | children 11 | ]).toEqual([ 12 | 'span', 13 | pageNumber 14 | ]) 15 | } 16 | 17 | describe('', () => { 18 | context('for the current page', () => { 19 | it('displays the page number in a span', () => { 20 | const wrapper = shallow( 21 | 22 | ) 23 | 24 | verifyPageNumber(wrapper.node, 1) 25 | }) 26 | }) 27 | 28 | context('for any other page', () => { 29 | const pageActions = { goTo: expect.createSpy() } 30 | const wrapper = shallow( 31 | 32 | ) 33 | 34 | it('displays a link', () => { 35 | expect(wrapper.node.type).toEqual('a') 36 | }) 37 | 38 | it('displays the page number within the link', () => { 39 | const span = wrapper.node.props.children 40 | verifyPageNumber(span, 2) 41 | }) 42 | 43 | context('when the link is clicked', () => { 44 | it('calls the goTo action', () => { 45 | wrapper.find('a').simulate('click') 46 | expect(pageActions.goTo).toHaveBeenCalledWith(2) 47 | }) 48 | }) 49 | }) 50 | }) 51 | 52 | -------------------------------------------------------------------------------- /spec/Prev.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import expect from 'expect' 3 | import { shallow } from 'enzyme' 4 | import FontAwesome from 'react-fontawesome' 5 | import { Prev } from '../src/Prev' 6 | 7 | function verifyIcon(node) { 8 | const { 9 | type, 10 | props: { name } 11 | } = node 12 | 13 | expect([ 14 | type, 15 | name 16 | ]).toEqual([ 17 | FontAwesome, 18 | 'chevron-left' 19 | ]) 20 | } 21 | 22 | describe('', () => { 23 | context('when no previous page exists', () => { 24 | const wrapper = shallow( 25 | 26 | ) 27 | 28 | it('renders an icon', () => { 29 | verifyIcon(wrapper.node) 30 | }) 31 | }) 32 | 33 | context('when previous page exists', () => { 34 | const pageActions = { 35 | prev: expect.createSpy() 36 | } 37 | 38 | const wrapper = shallow( 39 | 40 | ) 41 | 42 | it('renders an anchor', () => { 43 | expect(wrapper.node.type).toEqual('a') 44 | expect(wrapper.find('a').length).toEqual(1) 45 | }) 46 | 47 | it('renders an icon inside the anchor', () => { 48 | const icon = wrapper.node.props.children 49 | verifyIcon(icon) 50 | }) 51 | 52 | context('when clicking the link', () => { 53 | wrapper.find('a').simulate('click') 54 | it('calls the prev action', () => { 55 | expect(pageActions.prev).toHaveBeenCalled() 56 | }) 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /spec/SortLink.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Map } from 'immutable' 3 | import expect from 'expect' 4 | import { shallow } from 'enzyme' 5 | import FontAwesome from 'react-fontawesome' 6 | import { SortLink } from '../src/SortLink' 7 | 8 | function getProps(props={}) { 9 | return { 10 | field: 'name', 11 | text: 'Name', 12 | sort: 'name', 13 | sortReverse: false, 14 | pageActions: { 15 | sort: expect.createSpy() 16 | }, 17 | ...props 18 | } 19 | } 20 | 21 | describe('', () => { 22 | let props 23 | let wrapper 24 | 25 | context('when sortable', () => { 26 | beforeEach(() => { 27 | props = getProps() 28 | wrapper = shallow( 29 | 30 | ) 31 | }) 32 | 33 | it('displays the text in a link', () => { 34 | const { node: { type, props: { children } } } = wrapper 35 | const [text, space, icon] = children 36 | 37 | expect([ 38 | type, 39 | text, 40 | space, 41 | icon.type 42 | ]).toEqual([ 43 | 'a', 44 | props.text, 45 | ' ', 46 | FontAwesome 47 | ]) 48 | }) 49 | 50 | context('when sorted by given field', () => { 51 | beforeEach(() => { 52 | props = getProps({ paginator: Map({ sort: props.field }) }) 53 | wrapper = shallow( 54 | 55 | ) 56 | }) 57 | 58 | it('indicates sort direction', () => { 59 | const icon = wrapper.find(FontAwesome) 60 | expect(icon.node.props.name).toEqual('angle-down') 61 | }) 62 | 63 | context('when link is clicked', () => { 64 | beforeEach(() => { 65 | wrapper.find('a').simulate('click') 66 | }) 67 | 68 | it('calls the sort action', () => { 69 | expect(props.pageActions.sort).toHaveBeenCalledWith(props.field, true) 70 | }) 71 | }) 72 | }) 73 | 74 | context('when sorted by given field in reverse', () => { 75 | beforeEach(() => { 76 | props = getProps({ sortReverse: true }) 77 | wrapper = shallow( 78 | 79 | ) 80 | }) 81 | 82 | it('indicates sort direction', () => { 83 | const icon = wrapper.find(FontAwesome) 84 | expect(icon.node.props.name).toEqual('angle-up') 85 | }) 86 | 87 | context('when link is clicked', () => { 88 | beforeEach(() => { 89 | wrapper.find('a').simulate('click') 90 | }) 91 | 92 | it('calls the sort action', () => { 93 | expect(props.pageActions.sort).toHaveBeenCalledWith(props.field, false) 94 | }) 95 | }) 96 | }) 97 | }) 98 | 99 | context('when not sortable', () => { 100 | beforeEach(() => { 101 | props = getProps() 102 | wrapper = shallow( 103 | 104 | ) 105 | }) 106 | 107 | it('displays the text in a span', () => { 108 | const { node: { type, props: { children } } } = wrapper 109 | expect([ 110 | type, 111 | children 112 | ]).toEqual([ 113 | 'span', 114 | props.text 115 | ]) 116 | }) 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /spec/actions.spec.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable' 2 | import expect from 'expect' 3 | import PromiseMock from 'promise-mock' 4 | import configureMockStore from 'redux-mock-store' 5 | import thunk from 'redux-thunk' 6 | import composables, { expireAll } from '../src/actions' 7 | import { defaultPaginator } from '../src/reducer' 8 | import actionType, * as actionTypes from '../src/actions/actionTypes' 9 | import expectAsync from './specHelper' 10 | import { registerPaginator } from '../src/lib/stateManagement' 11 | 12 | const listId = 'recipesList' 13 | const resolve = t => actionType(t, listId) 14 | const mockStore = configureMockStore([thunk]) 15 | 16 | const setup = (pass=true, results=[]) => { 17 | const paginator = defaultPaginator 18 | .set('id', listId) 19 | .set('results', Immutable.fromJS(results)) 20 | 21 | const store = mockStore({ recipesList: paginator }) 22 | const data = { 23 | total_count: 1, 24 | results: [{ name: 'Ewe and IPA' }] 25 | } 26 | 27 | const fetch = () => () => 28 | (pass && Promise.resolve({ data })) || Promise.reject(new Error('An error')) 29 | 30 | const pageActions = composables({ listId }) 31 | 32 | registerPaginator({ listId, fetch }) 33 | 34 | return { paginator, store, pageActions } 35 | } 36 | 37 | describe('pageActions', () => { 38 | describe('pageActions.reload', () => { 39 | beforeEach(() => { 40 | PromiseMock.install() 41 | }) 42 | 43 | afterEach(() => { 44 | PromiseMock.uninstall() 45 | }) 46 | 47 | 48 | context('when fetch succeeds', () => { 49 | it('dispatches RESULTS_UPDATED', () => { 50 | const { pageActions, store } = setup() 51 | 52 | expectAsync( 53 | store.dispatch(pageActions.reload()).then(() => { 54 | const actions = store.getActions() 55 | const types = actions.map(a => a.type) 56 | 57 | expect(types).toEqual([ 58 | resolve(actionTypes.FETCH_RECORDS), 59 | resolve(actionTypes.RESULTS_UPDATED) 60 | ]) 61 | }) 62 | ) 63 | }) 64 | 65 | context('when results props are configured', () => { 66 | const paginator = defaultPaginator 67 | const store = mockStore({ recipesList: paginator }) 68 | const data = { 69 | total_entries: 1, 70 | recipes: [{ name: 'Ewe and IPA' }] 71 | } 72 | 73 | const fetch = () => () => Promise.resolve({ data }) 74 | 75 | beforeEach(() => { 76 | registerPaginator({ 77 | listId, 78 | fetch, 79 | pageParams: { 80 | resultsProp: 'recipes', 81 | totalCountProp: 'total_entries' 82 | } 83 | }) 84 | }) 85 | 86 | const pageActions = composables({ listId }) 87 | 88 | it('is able to read the results', () => { 89 | expectAsync( 90 | store.dispatch(pageActions.reload()).then(() => { 91 | const t = resolve(actionTypes.RESULTS_UPDATED) 92 | const action = store.getActions().find(a => a.type === t) 93 | expect(action.results).toEqual(data.recipes) 94 | }) 95 | ) 96 | }) 97 | 98 | it('is able to read the count', () => { 99 | expectAsync( 100 | store.dispatch(pageActions.reload()).then(() => { 101 | const t = resolve(actionTypes.RESULTS_UPDATED) 102 | const action = store.getActions().find(a => a.type === t) 103 | expect(action.totalCount).toEqual(data.total_entries) 104 | }) 105 | ) 106 | }) 107 | }) 108 | }) 109 | 110 | context('when fetch fails', () => { 111 | it('dispatches RESULTS_UPDATED_ERROR', () => { 112 | const { pageActions, store } = setup(false) 113 | 114 | expectAsync( 115 | store.dispatch(pageActions.reload()).then(() => { 116 | const actions = store.getActions() 117 | const types = actions.map(a => a.type) 118 | expect(types).toEqual([ 119 | resolve(actionTypes.FETCH_RECORDS), 120 | resolve(actionTypes.RESULTS_UPDATED_ERROR) 121 | ]) 122 | }) 123 | ) 124 | }) 125 | }) 126 | }) 127 | 128 | describe('pageActions.next', () => { 129 | it('dispatches NEXT_PAGE', () => { 130 | const { pageActions } = setup() 131 | const expectedAction = { 132 | type: resolve(actionTypes.NEXT_PAGE) 133 | } 134 | 135 | expect(pageActions.next()).toEqual(expectedAction) 136 | }) 137 | }) 138 | 139 | describe('pageActions.prev', () => { 140 | it('dispatches PREV_PAGE', () => { 141 | const { pageActions } = setup() 142 | const expectedAction = { 143 | type: resolve(actionTypes.PREVIOUS_PAGE) 144 | } 145 | 146 | expect(pageActions.prev()).toEqual(expectedAction) 147 | }) 148 | }) 149 | 150 | describe('pageActions.goTo', () => { 151 | it('dispatches GO_TO_PAGE', () => { 152 | const { pageActions } = setup() 153 | const page = 2 154 | const expectedAction = { 155 | type: resolve(actionTypes.GO_TO_PAGE), 156 | page 157 | } 158 | 159 | expect(pageActions.goTo(page)).toEqual(expectedAction) 160 | }) 161 | }) 162 | 163 | describe('pageActions.setPageSize', () => { 164 | it('dispatches SET_PAGE_SIZE', () => { 165 | const { pageActions } = setup() 166 | const size = 25 167 | const expectedAction = { 168 | type: resolve(actionTypes.SET_PAGE_SIZE), 169 | size 170 | } 171 | 172 | expect(pageActions.setPageSize(size)).toEqual(expectedAction) 173 | }) 174 | }) 175 | 176 | describe('pageActions.toggleFilterItem', () => { 177 | it('dispatches TOGGLE_FILTER_ITEM', () => { 178 | const { pageActions } = setup() 179 | const field = 'status_types' 180 | const value = 'inactive' 181 | const expectedAction = { 182 | type: resolve(actionTypes.TOGGLE_FILTER_ITEM), 183 | field, 184 | value 185 | } 186 | 187 | expect(pageActions.toggleFilterItem(field, value)).toEqual(expectedAction) 188 | }) 189 | }) 190 | 191 | describe('pageActions.setFilter', () => { 192 | it('dispatches SET_FILTER', () => { 193 | const { pageActions } = setup() 194 | const field = 'name' 195 | const value = { like: 'IPA' } 196 | const expectedAction = { 197 | type: resolve(actionTypes.SET_FILTER), 198 | field, 199 | value 200 | } 201 | 202 | expect(pageActions.setFilter(field, value)).toEqual(expectedAction) 203 | }) 204 | }) 205 | 206 | describe('pageActions.setFilters', () => { 207 | it('dispatches SET_FILTERS', () => { 208 | const { pageActions } = setup() 209 | const filters = { name: { like: 'IPA' } } 210 | const expectedAction = { 211 | type: resolve(actionTypes.SET_FILTERS), 212 | filters 213 | } 214 | 215 | expect(pageActions.setFilters(filters)).toEqual(expectedAction) 216 | }) 217 | }) 218 | 219 | describe('pageActions.resetFilters', () => { 220 | it('dispatches RESET_FILTERS', () => { 221 | const { pageActions } = setup() 222 | const filters = { name: { like: 'IPA' } } 223 | const expectedAction = { 224 | type: resolve(actionTypes.RESET_FILTERS), 225 | filters 226 | } 227 | 228 | expect(pageActions.resetFilters(filters)).toEqual(expectedAction) 229 | }) 230 | }) 231 | 232 | describe('pageActions.sort', () => { 233 | it('dispatches SORT_CHANGED', () => { 234 | const { pageActions } = setup() 235 | const field = 'name' 236 | const reverse = false 237 | const expectedAction = { 238 | type: resolve(actionTypes.SORT_CHANGED), 239 | field, 240 | reverse 241 | } 242 | 243 | expect(pageActions.sort(field, reverse)).toEqual(expectedAction) 244 | }) 245 | }) 246 | 247 | describe('updatingItem', () => { 248 | it('dispatches UPDATING_ITEM', () => { 249 | const { pageActions, store } = setup() 250 | const itemId = 42 251 | const expectedAction = { 252 | type: resolve(actionTypes.UPDATING_ITEM), 253 | itemId 254 | } 255 | 256 | expect(store.dispatch(pageActions.updatingItem(itemId))).toEqual(expectedAction) 257 | }) 258 | }) 259 | 260 | describe('updatingItems', () => { 261 | it('dispatches UPDATING_ITEMS', () => { 262 | const { pageActions, store } = setup() 263 | const itemIds = [42, 43] 264 | const expectedAction = { 265 | type: resolve(actionTypes.UPDATING_ITEMS), 266 | itemIds 267 | } 268 | 269 | expect(store.dispatch(pageActions.updatingItems(itemIds))).toEqual(expectedAction) 270 | }) 271 | }) 272 | 273 | describe('removingItem', () => { 274 | it('dispatches REMOVING_ITEM', () => { 275 | const { pageActions, store } = setup() 276 | const itemId = 42 277 | const expectedAction = { 278 | type: resolve(actionTypes.REMOVING_ITEM), 279 | itemId 280 | } 281 | 282 | expect(store.dispatch(pageActions.removingItem(itemId))).toEqual(expectedAction) 283 | }) 284 | }) 285 | 286 | describe('updateItem', () => { 287 | it('dispatches UPDATE_ITEM', () => { 288 | const { pageActions, store } = setup() 289 | const itemId = 42 290 | const data = { name: 'Ewe and IPA' } 291 | const expectedAction = { 292 | type: resolve(actionTypes.UPDATE_ITEM), 293 | itemId, 294 | data 295 | } 296 | 297 | expect(store.dispatch(pageActions.updateItem(itemId, data))).toEqual(expectedAction) 298 | }) 299 | }) 300 | 301 | describe('updateItems', () => { 302 | it('dispatches UPDATE_ITEMS', () => { 303 | const { pageActions, store } = setup() 304 | const itemIds = [42, 43] 305 | const data = { active: false } 306 | const expectedAction = { 307 | type: resolve(actionTypes.UPDATE_ITEMS), 308 | itemIds, 309 | data 310 | } 311 | 312 | expect(store.dispatch(pageActions.updateItems(itemIds, data))).toEqual(expectedAction) 313 | }) 314 | }) 315 | 316 | describe('removeItem', () => { 317 | it('dispatches REMOVE_ITEM', () => { 318 | const { pageActions, store } = setup() 319 | const itemId = 42 320 | const expectedAction = { 321 | type: resolve(actionTypes.REMOVE_ITEM), 322 | itemId 323 | } 324 | 325 | expect(store.dispatch(pageActions.removeItem(itemId))).toEqual(expectedAction) 326 | }) 327 | }) 328 | 329 | describe('expire', () => { 330 | it('dispatches EXPIRE_PAGINATOR', () => { 331 | const { pageActions, store } = setup() 332 | const expectedAction = { 333 | type: resolve(actionTypes.EXPIRE_PAGINATOR) 334 | } 335 | 336 | expect(store.dispatch(pageActions.expire())).toEqual(expectedAction) 337 | }) 338 | }) 339 | 340 | describe('expireAll', () => { 341 | it('dispatches EXPIRE_ALL', () => { 342 | const { store } = setup() 343 | const expectedAction = { 344 | type: actionTypes.EXPIRE_ALL 345 | } 346 | 347 | expect(store.dispatch(expireAll())).toEqual(expectedAction) 348 | }) 349 | }) 350 | 351 | describe('updateAsync', () => { 352 | const itemId = 'itemId' 353 | 354 | beforeEach(() => { 355 | PromiseMock.install() 356 | }) 357 | 358 | afterEach(() => { 359 | PromiseMock.uninstall() 360 | }) 361 | 362 | context('on update success', () => { 363 | it('updates the item', () => { 364 | const { pageActions, store } = setup() 365 | const updateData = { active: true } 366 | const serverVersion = { active: false } 367 | const update = Promise.resolve(serverVersion) 368 | 369 | const expectedActions = [ 370 | pageActions.updateItem(itemId, updateData), 371 | pageActions.updatingItem(itemId), 372 | pageActions.updateItem(itemId, serverVersion) 373 | ] 374 | 375 | expectAsync( 376 | store.dispatch(pageActions.updateAsync(itemId, updateData, update)).then(() => { 377 | expect(store.getActions()).toEqual(expectedActions) 378 | }) 379 | ) 380 | }) 381 | }) 382 | 383 | context('on update failure', () => { 384 | it('reverts the item', () => { 385 | const record = { 386 | id: itemId, 387 | name: 'Ewe and IPA' 388 | } 389 | const results = [record] 390 | const { pageActions, store } = setup(true, results) 391 | 392 | const updateData = { 393 | name: 'Pouty Stout', 394 | extraProp: 'To be removed' 395 | } 396 | 397 | const error = 'server error' 398 | const update = Promise.reject(error) 399 | 400 | const expectedActions = [ 401 | pageActions.updateItem(itemId, updateData), 402 | pageActions.updatingItem(itemId), 403 | pageActions.resetItem(itemId, record), 404 | pageActions.itemError(itemId, error) 405 | ] 406 | 407 | expectAsync( 408 | store.dispatch(pageActions.updateAsync(itemId, updateData, update)).then(() => { 409 | const actions = store.getActions() 410 | expect(actions).toEqual(expectedActions) 411 | }) 412 | ) 413 | }) 414 | }) 415 | }) 416 | 417 | describe('updateItemsAsync', () => { 418 | beforeEach(() => { 419 | PromiseMock.install() 420 | }) 421 | 422 | afterEach(() => { 423 | PromiseMock.uninstall() 424 | }) 425 | 426 | context('on update success', () => { 427 | const updateData = { active: true } 428 | 429 | context('with default settings', () => { 430 | const results = [{ id: 1, name: 'Ewe and IPA' }] 431 | 432 | it('does an async update on all the items', () => { 433 | const ids = results.map(r => r.id) 434 | const { pageActions, store } = setup(true, results) 435 | const expectedActions = [ 436 | pageActions.updateItems(ids, updateData), 437 | pageActions.updatingItems(ids), 438 | pageActions.updateItems(ids, updateData) 439 | ] 440 | 441 | const update = Promise.resolve(updateData) 442 | 443 | expectAsync( 444 | store.dispatch(pageActions.updateItemsAsync(ids, updateData, update)).then(() => { 445 | expect(store.getActions()).toEqual(expectedActions) 446 | }) 447 | ) 448 | }) 449 | }) 450 | 451 | context('with showUpdating=false', () => { 452 | const results = [{ id: 1, name: 'Ewe and IPA' }] 453 | 454 | it('skips the updating indicators', () => { 455 | const { pageActions, store } = setup(true, results) 456 | const ids = results.map(r => r.id) 457 | const expectedActions = [ 458 | pageActions.updateItems(ids, updateData) 459 | ] 460 | 461 | const update = Promise.resolve(updateData) 462 | const promise = pageActions.updateItemsAsync(ids, updateData, update, false) 463 | 464 | expectAsync( 465 | store.dispatch(promise).then(() => { 466 | expect(store.getActions()).toEqual(expectedActions) 467 | }) 468 | ) 469 | }) 470 | }) 471 | }) 472 | 473 | context('on update failure', () => { 474 | it('reverts the results', () => { 475 | const itemId = 1 476 | 477 | const record = { 478 | id: itemId, 479 | name: 'Ewe and IPA', 480 | active: true 481 | } 482 | 483 | const results = [record] 484 | const ids = [record.id] 485 | 486 | const { pageActions, store } = setup(true, results) 487 | const updateData = { active: false } 488 | const error = 'server error' 489 | const update = Promise.reject(error) 490 | 491 | const expectedActions = [ 492 | pageActions.updateItems(ids, updateData), 493 | pageActions.updatingItems(ids), 494 | pageActions.resetResults(results), 495 | pageActions.markItemsErrored(ids, error) 496 | ] 497 | 498 | expectAsync( 499 | store.dispatch(pageActions.updateItemsAsync([itemId], updateData, update)).then(() => { 500 | const actions = store.getActions() 501 | expect(actions).toEqual(expectedActions) 502 | }) 503 | ) 504 | }) 505 | }) 506 | }) 507 | 508 | describe('removeAsync', () => { 509 | const itemId = 'itemId' 510 | 511 | beforeEach(() => { 512 | PromiseMock.install() 513 | }) 514 | 515 | afterEach(() => { 516 | PromiseMock.uninstall() 517 | }) 518 | 519 | context('on remove success', () => { 520 | it('removes the item', () => { 521 | const { pageActions, store } = setup() 522 | const remove = Promise.resolve() 523 | 524 | const expectedActions = [ 525 | pageActions.removingItem(itemId), 526 | pageActions.removeItem(itemId) 527 | ] 528 | 529 | expectAsync( 530 | store.dispatch(pageActions.removeAsync(itemId, remove)).then(() => { 531 | expect(store.getActions()).toEqual(expectedActions) 532 | }) 533 | ) 534 | }) 535 | }) 536 | 537 | context('on remove failure', () => { 538 | it('reverts the item', () => { 539 | const record = { 540 | id: itemId, 541 | name: 'Ewe and IPA' 542 | } 543 | const results = [record] 544 | const { pageActions, store } = setup(true, results) 545 | 546 | const error = 'server error' 547 | const remove = Promise.reject(error) 548 | 549 | const expectedActions = [ 550 | pageActions.removingItem(itemId), 551 | pageActions.resetItem(itemId, record), 552 | pageActions.itemError(itemId, error) 553 | ] 554 | 555 | expectAsync( 556 | store.dispatch(pageActions.removeAsync(itemId, remove)).then(() => { 557 | const actions = store.getActions() 558 | expect(actions).toEqual(expectedActions) 559 | }) 560 | ) 561 | }) 562 | }) 563 | }) 564 | }) 565 | -------------------------------------------------------------------------------- /spec/containers/PaginationWrapper.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import expect from 'expect' 3 | import { mount } from 'enzyme' 4 | import { PaginationWrapper } from '../../src/containers/PaginationWrapper' 5 | import { defaultPaginator } from '../../src/reducer' 6 | import { Prev } from '../../src/Prev' 7 | 8 | function getProps(props = {}) { 9 | return { 10 | pageActions: { 11 | reload: expect.createSpy(), 12 | initialize: expect.createSpy() 13 | }, 14 | paginator: defaultPaginator.merge(props) 15 | } 16 | } 17 | 18 | describe('', () => { 19 | context('when paginator is uninitialized', () => { 20 | const props = getProps() 21 | mount( 22 | 23 | 24 | 25 | ) 26 | 27 | it('calls initialize', () => { 28 | expect(props.pageActions.initialize).toHaveBeenCalled() 29 | }) 30 | }) 31 | 32 | context('when paginator is initialized', () => { 33 | const props = getProps({ initialized: true }) 34 | mount( 35 | 36 | 37 | 38 | ) 39 | 40 | it('does not initialize', () => { 41 | expect(props.pageActions.initialize).toNotHaveBeenCalled() 42 | }) 43 | }) 44 | 45 | context('when paginator is stale', () => { 46 | context('and there is no load error', () => { 47 | const props = getProps({ stale: true }) 48 | mount( 49 | 50 | 51 | 52 | ) 53 | 54 | it('executes a reload', () => { 55 | expect(props.pageActions.reload).toHaveBeenCalled() 56 | }) 57 | }) 58 | 59 | context('and there is a load error', () => { 60 | const props = getProps({ 61 | stale: true, 62 | loadError: { status: 401 } 63 | }) 64 | 65 | mount( 66 | 67 | 68 | 69 | ) 70 | 71 | it('does not execute a reload', () => { 72 | expect(props.pageActions.reload).toNotHaveBeenCalled() 73 | }) 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /spec/decorators/flip.spec.js: -------------------------------------------------------------------------------- 1 | import flip from '../../src/decorators/flip' 2 | import * as shared from './shared' 3 | 4 | describe('flip()', () => { 5 | shared.decorate(flip) 6 | shared.behavesLikeAFlipper() 7 | }) 8 | -------------------------------------------------------------------------------- /spec/decorators/paginate.spec.js: -------------------------------------------------------------------------------- 1 | import paginate from '../../src/decorators/paginate' 2 | import * as shared from './shared' 3 | 4 | describe('paginate()', () => { 5 | shared.decorate(paginate) 6 | shared.behavesLikeAPaginator() 7 | }) 8 | 9 | -------------------------------------------------------------------------------- /spec/decorators/shared.jsx: -------------------------------------------------------------------------------- 1 | import { List, Set } from 'immutable' 2 | import React from 'react' 3 | import expect from 'expect' 4 | import { mount } from 'enzyme' 5 | import configureMockStore from 'redux-mock-store' 6 | import { defaultPaginator } from '../../src/reducer' 7 | 8 | const mockStore = configureMockStore() 9 | 10 | export function decorate(decorator) { 11 | const Component = () => false 12 | const Decorated = decorator(Component) 13 | const store = mockStore({ recipes: defaultPaginator }) 14 | const wrapper = mount( 15 | 16 | ) 17 | 18 | before(function () { 19 | this.component = wrapper.find(Component) 20 | }) 21 | } 22 | 23 | export function behavesLikeAFlipper() { 24 | it('injects hasPreviousPage', function () { 25 | expect(this.component.props().hasPreviousPage).toBeA('boolean') 26 | }) 27 | 28 | it('injects hasNextPage', function () { 29 | expect(this.component.props().hasNextPage).toBeA('boolean') 30 | }) 31 | } 32 | 33 | export function behavesLikeAPaginator() { 34 | behavesLikeAFlipper() 35 | 36 | it('injects currentPage', function () { 37 | expect(this.component.props().currentPage).toBeA('number') 38 | }) 39 | 40 | it('injects totalPages', function () { 41 | expect(this.component.props().totalPages).toBeA('number') 42 | }) 43 | } 44 | 45 | export function behavesLikeADataGrid() { 46 | it('injects results', function () { 47 | expect(this.component.props().results).toBeA(List) 48 | }) 49 | 50 | it('injects isLoading', function () { 51 | expect(this.component.props().isLoading).toBeA('boolean') 52 | }) 53 | 54 | it('injects updating', function () { 55 | expect(this.component.props().updating).toBeA(Set) 56 | }) 57 | 58 | it('injects removing', function () { 59 | expect(this.component.props().removing).toBeA(Set) 60 | }) 61 | } 62 | 63 | export function behavesLikeAPageSizer() { 64 | it('injects pageSize', function () { 65 | expect(this.component.props().pageSize).toBeA('number') 66 | }) 67 | } 68 | 69 | export function behavesLikeASorter() { 70 | it('injects sort', function () { 71 | expect(this.component.props().sort).toBeA('string') 72 | }) 73 | 74 | it('injects sortReverse', function () { 75 | expect(this.component.props().sortReverse).toBeA('boolean') 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /spec/decorators/sort.spec.js: -------------------------------------------------------------------------------- 1 | import { sort } from '../../src/decorators' 2 | import * as shared from './shared' 3 | 4 | describe('sort()', () => { 5 | shared.decorate(sort) 6 | shared.behavesLikeASorter() 7 | }) 8 | 9 | -------------------------------------------------------------------------------- /spec/decorators/stretch.spec.js: -------------------------------------------------------------------------------- 1 | import { stretch } from '../../src/decorators' 2 | import * as shared from './shared' 3 | 4 | describe('stretch()', () => { 5 | shared.decorate(stretch) 6 | shared.behavesLikeAPageSizer() 7 | }) 8 | 9 | -------------------------------------------------------------------------------- /spec/decorators/tabulate.spec.js: -------------------------------------------------------------------------------- 1 | import { tabulate } from '../../src/decorators' 2 | import * as shared from './shared' 3 | 4 | describe('tabulate()', () => { 5 | shared.decorate(tabulate) 6 | shared.behavesLikeADataGrid() 7 | }) 8 | 9 | -------------------------------------------------------------------------------- /spec/decorators/violetPaginator.spec.js: -------------------------------------------------------------------------------- 1 | import { violetPaginator } from '../../src/decorators' 2 | import * as shared from './shared' 3 | 4 | describe('violetPaginator()', () => { 5 | shared.decorate(violetPaginator) 6 | shared.behavesLikeAPaginator() 7 | shared.behavesLikeADataGrid() 8 | shared.behavesLikeAPageSizer() 9 | shared.behavesLikeASorter() 10 | }) 11 | 12 | -------------------------------------------------------------------------------- /spec/pageInfoTranslator.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import { 3 | translate, 4 | configurePageParams, 5 | responseProps, 6 | recordProps 7 | } from '../src/pageInfoTranslator' 8 | import { defaultPaginator } from '../src/reducer' 9 | 10 | describe('configurePageParams', () => { 11 | beforeEach(() => { 12 | configurePageParams({ 13 | totalCount: 'total_records', 14 | results: 'records', 15 | id: 'uuid', 16 | page: 'page_num', 17 | perPage: 'limit', 18 | sort: 'order', 19 | sortOrder: 'direction' 20 | }) 21 | }) 22 | 23 | afterEach(() => { 24 | configurePageParams({ 25 | totalCount: 'total_count', 26 | results: 'results', 27 | id: 'id', 28 | page: 'page', 29 | perPage: 'pageSize', 30 | sort: 'sort', 31 | sortOrder: 'sortOrder' 32 | }) 33 | }) 34 | 35 | it('can override the totalCountProp', () => { 36 | const [totalCountProp] = responseProps() 37 | expect(totalCountProp).toEqual('total_records') 38 | }) 39 | 40 | it('can override the resultsProp', () => { 41 | const [_, resultsProp] = responseProps() 42 | expect(resultsProp).toEqual('records') 43 | }) 44 | 45 | it('can override the idProp', () => { 46 | const { identifier } = recordProps() 47 | expect(identifier).toEqual('uuid') 48 | }) 49 | 50 | it('can override the page param', () => { 51 | const pageInfo = translate(defaultPaginator) 52 | expect(pageInfo.query.page_num).toExist() 53 | }) 54 | 55 | it('can override the page size param', () => { 56 | const pageInfo = translate(defaultPaginator) 57 | expect(pageInfo.query.limit).toExist() 58 | }) 59 | 60 | it('can override the sort param', () => { 61 | const pageInfo = translate(defaultPaginator.set('sort', 'name')) 62 | expect(pageInfo.query.order).toExist() 63 | }) 64 | 65 | it('can override the sort order param', () => { 66 | const pageInfo = translate(defaultPaginator.set('sort', 'name')) 67 | expect(pageInfo.query.direction).toExist() 68 | }) 69 | 70 | context('when sortReverse is used', () => { 71 | beforeEach(() => { 72 | configurePageParams({ sortReverse: true }) 73 | }) 74 | 75 | afterEach(() => { 76 | configurePageParams({ sortReverse: false }) 77 | }) 78 | 79 | it('uses a boolean to indicate the sort order', () => { 80 | const pageInfo = translate(defaultPaginator.set('sort', 'name')) 81 | expect(pageInfo.query.direction).toBe(false) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /spec/reducer.spec.js: -------------------------------------------------------------------------------- 1 | import Immutable, { Map, Set } from 'immutable' 2 | import expect from 'expect' 3 | import createPaginator, { defaultPaginator } from '../src/reducer' 4 | import actionType, * as actionTypes from '../src/actions/actionTypes' 5 | import composables from '../src/actions' 6 | 7 | const id = 'test-list' 8 | const reducer = createPaginator({ listId: id }) 9 | const pageActions = composables({ listId: id }) 10 | 11 | function setup(testPaginator=Map()) { 12 | const state = defaultPaginator.merge(testPaginator) 13 | 14 | return { state } 15 | } 16 | 17 | describe('pagination reducer', () => { 18 | describe('createPaginator', () => { 19 | context('with initial settings', () => { 20 | const config = { 21 | listId: 'customized', 22 | initialSettings: { 23 | pageSize: 50, 24 | sort: 'name' 25 | } 26 | } 27 | 28 | it('merges the initial settings', () => { 29 | expect(createPaginator(config)()).toEqual(defaultPaginator.merge(config.initialSettings)) 30 | }) 31 | }) 32 | }) 33 | 34 | describe('INITIALIZE_PAGINATOR', () => { 35 | context('with preloaded data', () => { 36 | const { state: initialState } = setup() 37 | const preloaded = { 38 | results: [{ name: 'Ewe and IPA' }], 39 | totalCount: 1 40 | } 41 | 42 | const action = { 43 | type: actionType(actionTypes.INITIALIZE_PAGINATOR, id), 44 | preloaded 45 | } 46 | 47 | const state = reducer(initialState, action) 48 | 49 | it('merges the preloaded results', () => { 50 | expect(state.get('results').toJS()).toEqual(preloaded.results) 51 | }) 52 | 53 | it('merges the preloaded totalCount', () => { 54 | expect(state.get('totalCount')).toEqual(preloaded.totalCount) 55 | }) 56 | }) 57 | 58 | context('without preloaded data', () => { 59 | it('marks the list as stale', () => { 60 | }) 61 | }) 62 | }) 63 | 64 | context('when handling SET_FILTER', () => { 65 | const { state: initialState } = setup() 66 | const field = 'foo' 67 | const value = { eq: 'bar' } 68 | const action = { 69 | type: actionType(actionTypes.SET_FILTER, id), 70 | field, 71 | value 72 | } 73 | 74 | const state = reducer(initialState, action) 75 | 76 | it('sets the specified filter', () => { 77 | expect(state.getIn(['filters', field]).toJS()).toEqual(value) 78 | }) 79 | 80 | it('returns to the first page', () => { 81 | expect(state.get('page')).toEqual(1) 82 | }) 83 | }) 84 | 85 | context('when handling SET_FILTERS', () => { 86 | const initialFilters = { 87 | name: { like: 'IPA' }, 88 | show_inactive: { eq: true }, 89 | containers: ['can', 'bottle'] 90 | } 91 | 92 | const paginator = defaultPaginator.merge({ 93 | filters: Immutable.fromJS(initialFilters) 94 | }) 95 | 96 | const { state: initialState } = setup(paginator) 97 | const updatedFilters = { 98 | show_inactive: { eq: false }, 99 | fermentation_temperature: { lt: 60 }, 100 | containers: ['growler'] 101 | } 102 | 103 | const action = { 104 | type: actionType(actionTypes.SET_FILTERS, id), 105 | filters: updatedFilters 106 | } 107 | 108 | const expectedFilters = { 109 | name: initialFilters.name, 110 | show_inactive: updatedFilters.show_inactive, 111 | fermentation_temperature: updatedFilters.fermentation_temperature, 112 | containers: ['growler'] 113 | } 114 | 115 | const state = reducer(initialState, action) 116 | 117 | it('merges the specified filters', () => { 118 | expect(state.get('filters').toJS()).toEqual(expectedFilters) 119 | }) 120 | 121 | it('returns to the first page', () => { 122 | expect(state.get('page')).toEqual(1) 123 | }) 124 | }) 125 | 126 | context('when handling RESET_FILTERS', () => { 127 | const initialFilters = { 128 | name: { like: 'IPA' }, 129 | show_inactive: { eq: true } 130 | } 131 | 132 | const paginator = defaultPaginator.merge({ 133 | filters: Immutable.fromJS(initialFilters) 134 | }) 135 | 136 | const { state: initialState } = setup(paginator) 137 | const updatedFilters = { 138 | show_inactive: { eq: false }, 139 | fermentation_temperature: { lt: 60 } 140 | } 141 | 142 | const action = { 143 | type: actionType(actionTypes.RESET_FILTERS, id), 144 | filters: updatedFilters 145 | } 146 | 147 | const state = reducer(initialState, action) 148 | 149 | it('resets the filters', () => { 150 | expect(state.get('filters').toJS()).toEqual(updatedFilters) 151 | }) 152 | 153 | it('returns to the first page', () => { 154 | expect(state.get('page')).toEqual(1) 155 | }) 156 | }) 157 | 158 | it('handles EXPIRE_PAGINATOR', () => { 159 | const { state: initialState } = setup() 160 | const action = { 161 | type: actionType(actionTypes.EXPIRE_PAGINATOR, id) 162 | } 163 | 164 | const state = reducer(initialState, action) 165 | expect(state.get('stale')).toEqual(true) 166 | }) 167 | 168 | it('handles EXPIRE_ALL', () => { 169 | const { state: initialState } = setup() 170 | const action = { 171 | type: actionTypes.EXPIRE_ALL 172 | } 173 | 174 | const state = reducer(initialState, action) 175 | expect(state.get('stale')).toBe(true) 176 | }) 177 | 178 | it('handles PREVIOUS_PAGE', () => { 179 | const paginator = defaultPaginator.set('page', 2) 180 | const { state: initialState } = setup(paginator) 181 | const action = { type: actionType(actionTypes.PREVIOUS_PAGE, id) } 182 | const state = reducer(initialState, action) 183 | 184 | expect(state.get('page')).toEqual(1) 185 | }) 186 | 187 | it('handles NEXT_PAGE', () => { 188 | const { state: initialState } = setup() 189 | const action = { type: actionType(actionTypes.NEXT_PAGE, id) } 190 | const state = reducer(initialState, action) 191 | 192 | expect(state.get('page')).toEqual(2) 193 | }) 194 | 195 | it('handles GO_TO_PAGE', () => { 196 | const { state: initialState } = setup() 197 | const action = { type: actionType(actionTypes.GO_TO_PAGE, id), size: 100 } 198 | const state = reducer(initialState, action) 199 | 200 | expect(state.get('page')).toEqual(action.page) 201 | }) 202 | 203 | it('handles SET_PAGE_SIZE', () => { 204 | const { state: initialState } = setup() 205 | const action = { type: actionType(actionTypes.SET_PAGE_SIZE, id), page: 2 } 206 | const state = reducer(initialState, action) 207 | 208 | const expectedState = defaultPaginator.merge({ 209 | page: 1, 210 | pageSize: action.size, 211 | stale: true 212 | }) 213 | 214 | expect(expectedState).toEqual(state) 215 | }) 216 | 217 | it('handles FETCH_RECORDS', () => { 218 | const { state: initialState } = setup() 219 | const action = { type: actionType(actionTypes.FETCH_RECORDS, id) } 220 | const state = reducer(initialState, action) 221 | 222 | expect(state.get('isLoading')).toBe(true) 223 | }) 224 | 225 | it('handles RESULTS_UPDATED', () => { 226 | const requestId = 'someId' 227 | const paginator = defaultPaginator.merge({ requestId, isLoading: true }) 228 | const { state: initialState } = setup(paginator) 229 | const records = [{ name: 'Pouty Stout' }, { name: 'Ewe and IPA' }] 230 | const action = { 231 | type: actionType(actionTypes.RESULTS_UPDATED, id), 232 | results: records, 233 | totalCount: 2, 234 | requestId 235 | } 236 | 237 | const state = reducer(initialState, action) 238 | const expectedState = defaultPaginator.merge({ 239 | results: Immutable.fromJS(records), 240 | isLoading: false, 241 | totalCount: action.totalCount, 242 | requestId 243 | }) 244 | 245 | expect(expectedState).toEqual(state) 246 | }) 247 | 248 | it('handles RESULTS_UPDATED_ERROR', () => { 249 | const paginator = defaultPaginator.merge({ isLoading: true }) 250 | const { state: initialState } = setup(paginator) 251 | const error = 'error' 252 | const action = { 253 | type: actionType(actionTypes.RESULTS_UPDATED_ERROR, id), 254 | error 255 | } 256 | 257 | const state = reducer(initialState, action) 258 | expect(state.toJS()).toEqual(defaultPaginator.merge({ 259 | isLoading: false, 260 | loadError: error 261 | }).toJS()) 262 | }) 263 | 264 | it('handles SET_FILTER', () => { 265 | const { state: initialState } = setup() 266 | const action = { 267 | type: actionType(actionTypes.SET_FILTER, id), 268 | field: 'base_salary', 269 | value: { lt: 2000 } 270 | } 271 | 272 | const state = reducer(initialState, action) 273 | expect(state.getIn(['filters', action.field])).toEqual(Immutable.fromJS(action.value)) 274 | }) 275 | 276 | it('handles SORT_CHANGED', () => { 277 | const paginator = defaultPaginator.merge({ sort: 'name', page: 2 }) 278 | const { state: initialState } = setup(paginator) 279 | const action = { 280 | type: actionType(actionTypes.SORT_CHANGED, id), 281 | field: 'fermentation_temperature', 282 | reverse: true 283 | } 284 | 285 | const state = reducer(initialState, action) 286 | const expectedState = defaultPaginator.merge({ 287 | sort: action.field, 288 | sortReverse: action.reverse, 289 | stale: true 290 | }) 291 | 292 | expect(expectedState).toEqual(state) 293 | }) 294 | 295 | it('handles RESET_RESULTS', () => { 296 | const { state: initialState } = setup() 297 | const results = [1, 2, 3] 298 | const action = { 299 | type: actionType(actionTypes.RESET_RESULTS, id), 300 | results 301 | } 302 | 303 | const state = reducer(initialState, action) 304 | expect(state.get('results').toJS()).toEqual(results) 305 | }) 306 | 307 | describe('UPDATE_ITEMS', () => { 308 | const itemId = 'someId' 309 | const results = [{ id: itemId, name: 'Pouty Stout' }] 310 | const paginator = defaultPaginator.merge({ 311 | results: Immutable.fromJS(results), 312 | updating: Set(itemId) 313 | }) 314 | 315 | const { state: initialState } = setup(paginator) 316 | const action = pageActions.updateItems([itemId], { active: true }) 317 | 318 | const state = reducer(initialState, action) 319 | 320 | it('updates all items', () => { 321 | const items = state.get('results') 322 | expect(items.every(i => i.get('active'))).toBe(true) 323 | }) 324 | 325 | it('removes the item from the updating list', () => { 326 | expect(state.get('updating').toArray()).toNotInclude(itemId) 327 | }) 328 | }) 329 | 330 | describe('RESET_ITEM', () => { 331 | const itemId = 'someId' 332 | const results = [{ id: itemId, name: 'Pouty Stout' }] 333 | const paginator = defaultPaginator.merge({ 334 | results: Immutable.fromJS(results), 335 | updating: Set(itemId) 336 | }) 337 | 338 | const { state: initialState } = setup(paginator) 339 | const reset = { id: itemId, name: 'Ewe and IPA' } 340 | const action = pageActions.resetItem(itemId, reset) 341 | 342 | const state = reducer(initialState, action) 343 | 344 | it('updates all items', () => { 345 | const item = state.get('results').find(r => r.get('id') === itemId) 346 | expect(item).toEqual(Map(reset)) 347 | }) 348 | 349 | it('removes the item from the updating list', () => { 350 | expect(state.get('updating').toArray()).toNotInclude(itemId) 351 | }) 352 | }) 353 | 354 | describe('MARK_ITEMS_ERRORED', () => { 355 | const itemId = 'someId' 356 | const results = [{ id: itemId, name: 'Pouty Stout' }] 357 | const paginator = defaultPaginator.merge({ 358 | results: Immutable.fromJS(results), 359 | updating: Set(itemId) 360 | }) 361 | 362 | const { state: initialState } = setup(paginator) 363 | const error = { name: ['taken'] } 364 | const action = pageActions.markItemsErrored([itemId], error) 365 | 366 | const state = reducer(initialState, action) 367 | 368 | it('marks the items as errored', () => { 369 | const item = state.get('results').find(r => r.get('id') === itemId) 370 | expect(item.get('error')).toEqual(Immutable.fromJS(error)) 371 | }) 372 | 373 | it('removes the item from the updating list', () => { 374 | expect(state.get('updating').toArray()).toNotInclude(itemId) 375 | }) 376 | }) 377 | 378 | it('handles UPDATING_ITEM', () => { 379 | const { state: initialState } = setup() 380 | const action = { 381 | type: actionType(actionTypes.UPDATING_ITEM, id), 382 | itemId: 'someId' 383 | } 384 | 385 | const state = reducer(initialState, action) 386 | expect(state.get('updating').toJS()).toInclude(action.itemId) 387 | }) 388 | 389 | describe('UPDATING_ITEMS', () => { 390 | const { state: initialState } = setup() 391 | const ids = [1, 2, 3] 392 | const action = pageActions.updatingItems(ids) 393 | 394 | const state = reducer(initialState, action) 395 | 396 | it('marks the items as updating', () => { 397 | expect(state.get('updating')).toEqual(Set(ids)) 398 | }) 399 | }) 400 | 401 | context('when handling UPDATE_ITEM', () => { 402 | const itemId = 'someId' 403 | const results = [{ id: itemId, name: 'Pouty Stout', error: 'Error updating recipe' }] 404 | const updating = Set.of('someId') 405 | const paginator = defaultPaginator.merge({ results: Immutable.fromJS(results), updating }) 406 | const { state: initialState } = setup(paginator) 407 | const action = { 408 | type: actionType(actionTypes.UPDATE_ITEM, id), 409 | data: { name: 'Ewe and IPA' }, 410 | itemId 411 | } 412 | 413 | const state = reducer(initialState, action) 414 | 415 | it('updates the item', () => { 416 | const item = state.get('results').toJS()[0] 417 | expect(item.name).toEqual(action.data.name) 418 | }) 419 | 420 | it('removes the item from the updating list', () => { 421 | expect(state.get('updating').toJS()).toNotInclude(itemId) 422 | }) 423 | 424 | it('removes the error from the item', () => { 425 | expect(state.get('error')).toNotExist() 426 | }) 427 | }) 428 | 429 | it('handles REMOVING_ITEM', () => { 430 | const { state: initialState } = setup() 431 | const action = { 432 | type: actionType(actionTypes.REMOVING_ITEM, id), 433 | itemId: 'someId' 434 | } 435 | 436 | const state = reducer(initialState, action) 437 | expect(state.get('removing').toJS()).toInclude(action.itemId) 438 | }) 439 | 440 | describe('REMOVE_ITEM', () => { 441 | const itemId = 'someId' 442 | const results = [{ id: itemId, name: 'Pouty Stout' }] 443 | const removing = Set.of(itemId) 444 | const paginator = defaultPaginator.merge({ results: Immutable.fromJS(results), removing }) 445 | const { state: initialState } = setup(paginator) 446 | const action = { 447 | type: actionType(actionTypes.REMOVE_ITEM, id), 448 | itemId 449 | } 450 | 451 | const state = reducer(initialState, action) 452 | 453 | it('removes the item', () => { 454 | expect(state.get('results').count()).toEqual(0) 455 | }) 456 | 457 | it('removes the item from the removing list', () => { 458 | expect(state.get('removing').toJS()).toNotInclude(itemId) 459 | }) 460 | }) 461 | 462 | describe('ITEM_ERROR', () => { 463 | const itemId = 'someId' 464 | const results = [{ id: itemId, name: 'Pouty Stout' }] 465 | const updating = Set.of(itemId) 466 | const paginator = defaultPaginator.merge({ results: Immutable.fromJS(results), updating }) 467 | const { state: initialState } = setup(paginator) 468 | const action = { 469 | type: actionType(actionTypes.ITEM_ERROR, id), 470 | itemId, 471 | error: 'Error updating item' 472 | } 473 | 474 | const state = reducer(initialState, action) 475 | const item = state.get('results').find(r => r.get('id') === itemId) 476 | 477 | it('attaches the error to the item', () => { 478 | expect(item.get('error')).toEqual(action.error) 479 | }) 480 | 481 | it('removes the item from the updating list', () => { 482 | expect(state.get('updating').toJS()).toNotInclude(itemId) 483 | }) 484 | }) 485 | 486 | context('and a filter item exists', () => { 487 | const paginator = defaultPaginator.setIn(['filters', 'myArray'], Set.of('myItem')) 488 | const { state: initialState } = setup(paginator) 489 | 490 | context('when the filter item is toggled', () => { 491 | it('is deleted', () => { 492 | const action = { 493 | type: actionType(actionTypes.TOGGLE_FILTER_ITEM, id), 494 | field: 'myArray', 495 | value: 'myItem' 496 | } 497 | 498 | const state = reducer(initialState, action) 499 | expect(state.getIn(['filters', 'myArray']).toArray()).toExclude('myItem') 500 | }) 501 | }) 502 | }) 503 | 504 | context('and a filter item does not exist', () => { 505 | const { state: initialState } = setup() 506 | 507 | context('when the filter item is toggled', () => { 508 | it('is added', () => { 509 | const action = { 510 | type: actionType(actionTypes.TOGGLE_FILTER_ITEM, id), 511 | field: 'myArray', 512 | value: 'myItem' 513 | } 514 | 515 | const state = reducer(initialState, action) 516 | expect(state.getIn(['filters', 'myArray']).toArray()).toInclude('myItem') 517 | }) 518 | }) 519 | }) 520 | }) 521 | -------------------------------------------------------------------------------- /spec/specHelper.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import jsdom from 'jsdom' 3 | 4 | const document = global.document = jsdom.jsdom('') 5 | global.window = document.defaultView 6 | Object.keys(document.defaultView).forEach((property) => { 7 | if (typeof global[property] === 'undefined') { 8 | global[property] = document.defaultView[property] 9 | } 10 | }) 11 | 12 | global.navigator = { 13 | userAgent: 'node.js' 14 | } 15 | 16 | export default function expectAsync(promise) { 17 | let rejected = false 18 | promise.catch(() => { 19 | rejected = true 20 | }) 21 | 22 | Promise.runAll() 23 | expect(rejected).toBe(false) 24 | } 25 | -------------------------------------------------------------------------------- /spec/stateManagement.spec.js: -------------------------------------------------------------------------------- 1 | import { Set } from 'immutable' 2 | import expect from 'expect' 3 | import { 4 | isUpdating, 5 | isRemoving, 6 | preloadedPaginator, 7 | currentQuery, 8 | registerPaginator, 9 | getPaginator 10 | } from '../src/lib/stateManagement' 11 | import createReducer, { defaultPaginator } from '../src/reducer' 12 | import actionType, * as actionTypes from '../src/actions/actionTypes' 13 | import { translate, responseProps } from '../src/pageInfoTranslator' 14 | 15 | const [id, itemId] = ['recipes', 1] 16 | const reducer = createReducer({ listId: id }) 17 | const resolve = t => actionType(t, id) 18 | const [totalCountProp, resultsProp] = responseProps() 19 | 20 | describe('State management utilities', () => { 21 | describe('preloadedPaginator', () => { 22 | const state = { [id]: reducer(undefined) } 23 | 24 | context('when there is no preloaded data', () => { 25 | const paginator = preloadedPaginator(state, id) 26 | 27 | it('returns the defaultPaginator', () => { 28 | expect(paginator).toEqual(defaultPaginator) 29 | }) 30 | }) 31 | 32 | context('when preloaded data is supplied', () => { 33 | const preloaded = { 34 | results: [{ name: 'Ewe and IPA' }], 35 | totalCount: 1 36 | } 37 | 38 | const paginator = preloadedPaginator(state, 'someId', preloaded) 39 | 40 | it('merges the preloaded data', () => { 41 | expect(paginator).toEqual(defaultPaginator.merge(preloaded)) 42 | }) 43 | }) 44 | }) 45 | 46 | describe('registerPaginator', () => { 47 | it('uses the default param names', () => { 48 | const { params } = registerPaginator({ listId: 'defaultParams' }) 49 | 50 | const expectedParams = { 51 | totalCountProp, 52 | resultsProp 53 | } 54 | 55 | expect(params).toEqual(expectedParams) 56 | }) 57 | 58 | it('allows overriding of resultsProp', () => { 59 | const config = { 60 | listId: 'customResultsProp', 61 | pageParams: { 62 | resultsProp: 'records' 63 | } 64 | } 65 | 66 | const { params } = registerPaginator(config) 67 | 68 | const expectedParams = { 69 | totalCountProp, 70 | resultsProp: config.pageParams.resultsProp 71 | } 72 | 73 | expect(params).toEqual(expectedParams) 74 | }) 75 | 76 | it('allows overriding of totalCountProp', () => { 77 | const config = { 78 | listId: 'customtotalCountProp', 79 | pageParams: { 80 | totalCountProp: 'total_records' 81 | } 82 | } 83 | 84 | const { params } = registerPaginator(config) 85 | 86 | const expectedParams = { 87 | totalCountProp: config.pageParams.totalCountProp, 88 | resultsProp 89 | } 90 | 91 | expect(params).toEqual(expectedParams) 92 | }) 93 | 94 | context('when provided a locator', () => { 95 | const locator = () => defaultPaginator 96 | const { locator: registeredLocator } = registerPaginator({ 97 | listId: 'customLocator', 98 | locator 99 | }) 100 | 101 | it('returns the locator', () => { 102 | expect(registeredLocator).toEqual(locator) 103 | }) 104 | }) 105 | 106 | context('when not provided a locator', () => { 107 | const { locator } = registerPaginator({ listId: 'noLocator' }) 108 | 109 | it('returns a locator that retrieves state by listId', () => { 110 | const state = { noLocator: defaultPaginator } 111 | expect(locator(state)).toEqual(defaultPaginator) 112 | }) 113 | }) 114 | }) 115 | 116 | describe('currentQuery', () => { 117 | const initialize = { 118 | type: actionTypes.INITIALIZE_PAGINATOR, 119 | id 120 | } 121 | 122 | const state = { pagination: reducer(undefined, initialize) } 123 | const expectedQuery = translate(getPaginator(state, id)) 124 | 125 | it('returns the same query that gets passed to config.fetch', () => { 126 | expect(currentQuery(state, id)).toEqual(expectedQuery) 127 | }) 128 | }) 129 | 130 | describe('getPaginator', () => { 131 | const paginator = defaultPaginator.set('pageSize', 50) 132 | 133 | context('when locator is registered', () => { 134 | beforeEach(() => { 135 | const locator = state => state.users.grid 136 | registerPaginator({ listId: 'deeplyNested', locator }) 137 | }) 138 | 139 | const state = { users: { grid: paginator } } 140 | 141 | it('uses the locator to lookup the state', () => { 142 | expect(getPaginator('deeplyNested', state)).toEqual(paginator) 143 | }) 144 | }) 145 | 146 | context('when locator is not registered', () => { 147 | const userGridId = 'users' 148 | beforeEach(() => { 149 | registerPaginator({ listId: userGridId }) 150 | }) 151 | 152 | const state = { [userGridId]: paginator } 153 | 154 | it('looks up the state by listId', () => { 155 | expect(getPaginator(userGridId, state)).toEqual(paginator) 156 | }) 157 | }) 158 | 159 | context('when there is no matching reducer', () => { 160 | it('returns the defaultPaginator', () => { 161 | expect(getPaginator('unregisteredId', {})).toEqual(defaultPaginator) 162 | }) 163 | }) 164 | }) 165 | 166 | describe('isUpdating', () => { 167 | context('when an item is updating', () => { 168 | const configuredReducer = createReducer({ 169 | listId: 'configuredReducer', 170 | initialSettings: { 171 | updating: Set.of(itemId) 172 | } 173 | }) 174 | 175 | const state = { recipes: configuredReducer() } 176 | 177 | it('returns true', () => { 178 | expect(isUpdating(state, id, itemId)).toBe(true) 179 | }) 180 | }) 181 | 182 | context('when an item is not updating', () => { 183 | const initialize = { 184 | type: resolve(actionTypes.INITIALIZE_PAGINATOR) 185 | } 186 | 187 | const state = reducer(undefined, initialize) 188 | 189 | it('returns false', () => { 190 | expect(isUpdating(state, itemId)).toBe(false) 191 | }) 192 | }) 193 | }) 194 | 195 | describe('isRemoving', () => { 196 | context('when an item is being removed', () => { 197 | const configuredReducer = createReducer({ 198 | listId: 'configuredReducer', 199 | initialSettings: { 200 | removing: Set.of(itemId) 201 | } 202 | }) 203 | 204 | const state = configuredReducer() 205 | 206 | it('returns true', () => { 207 | expect(isRemoving(state, itemId)).toBe(true) 208 | }) 209 | }) 210 | 211 | context('when an item is not being removed', () => { 212 | const initialize = { 213 | type: resolve(actionTypes.INITIALIZE_PAGINATOR) 214 | } 215 | 216 | const state = reducer(undefined, initialize) 217 | 218 | it('returns false', () => { 219 | expect(isRemoving(state, itemId)).toBe(false) 220 | }) 221 | }) 222 | }) 223 | }) 224 | -------------------------------------------------------------------------------- /src/DataTable.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import FontAwesome from 'react-fontawesome' 3 | import classNames from 'classnames' 4 | import SortLink from './SortLink' 5 | import { tabulate } from './decorators' 6 | import { recordProps } from './pageInfoTranslator' 7 | 8 | export function DataTable(props) { 9 | const { results, headers, isLoading, updating, removing, className = 'border' } = props 10 | 11 | if (isLoading) { 12 | return ( 13 |
14 | 19 |
20 | ) 21 | } 22 | 23 | const headerRow = headers.map(h => 24 | 25 | 29 | 30 | ) 31 | 32 | const rows = results.map((r, i) => { 33 | const columns = headers.map(h => { 34 | const { field, format } = h 35 | const data = r.get(field) 36 | const displayData = (format && format(r, i)) || data 37 | 38 | return ( 39 | 40 | {displayData} 41 | 42 | ) 43 | }) 44 | 45 | const classes = classNames({ 46 | updating: updating.includes(r.get(recordProps().identifier)), 47 | removing: removing.includes(r.get(recordProps().identifier)) 48 | }) 49 | 50 | return ( 51 | 52 | {columns} 53 | 54 | ) 55 | }) 56 | 57 | return ( 58 | 59 | 60 | 61 | {headerRow} 62 | 63 | 64 | 65 | {rows} 66 | 67 |
68 | ) 69 | } 70 | 71 | DataTable.propTypes = { 72 | headers: PropTypes.array.isRequired, 73 | isLoading: PropTypes.bool, 74 | results: PropTypes.object, 75 | updating: PropTypes.object, 76 | removing: PropTypes.object, 77 | className: PropTypes.string 78 | } 79 | 80 | export default tabulate(DataTable) 81 | -------------------------------------------------------------------------------- /src/Flipper.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import classNames from 'classnames' 3 | import { flip } from './decorators' 4 | import { Prev } from './Prev' 5 | import { Next } from './Next' 6 | 7 | export function Flipper(props) { 8 | const prevClasses = classNames({ disabled: !props.hasPreviousPage }) 9 | const nextClasses = classNames({ disabled: !props.hasNextPage }) 10 | 11 | return ( 12 |
    13 |
  • 14 | 15 |
  • 16 |
  • 17 | 18 |
  • 19 |
20 | ) 21 | } 22 | 23 | Flipper.propTypes = { 24 | hasPreviousPage: PropTypes.bool, 25 | hasNextPage: PropTypes.bool 26 | } 27 | 28 | export default flip(Flipper) 29 | -------------------------------------------------------------------------------- /src/Next.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FontAwesome from 'react-fontawesome' 3 | import { flip } from './decorators' 4 | 5 | export function Next({ pageActions, hasNextPage }) { 6 | const next = 7 | const link = hasNextPage ? ( 8 | {next} 9 | ) : next 10 | 11 | return link 12 | } 13 | 14 | export default flip(Next) 15 | -------------------------------------------------------------------------------- /src/PageLink.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { paginate } from './decorators' 3 | 4 | export function PageLink({ pageActions, page, currentPage }) { 5 | const navigate = () => 6 | pageActions.goTo(page) 7 | 8 | const pageNumber = {page} 9 | const link = page === currentPage ? pageNumber : ( 10 | {pageNumber} 11 | ) 12 | 13 | return link 14 | } 15 | 16 | export default paginate(PageLink) 17 | 18 | -------------------------------------------------------------------------------- /src/PageSizeDropdown.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { stretch } from './decorators' 3 | 4 | const defaultOptions = [ 5 | 15, 6 | 25, 7 | 50, 8 | 100 9 | ] 10 | 11 | export function PageSizeDropdown({ pageSize, pageActions, options=defaultOptions }) { 12 | const optionTags = options.map(n => 13 | 14 | ) 15 | 16 | const setPageSize = e => 17 | pageActions.setPageSize(parseInt(e.target.value, 10)) 18 | 19 | return ( 20 | 23 | ) 24 | } 25 | 26 | PageSizeDropdown.propTypes = { 27 | pageSize: PropTypes.number, 28 | pageActions: PropTypes.object, 29 | options: PropTypes.array 30 | } 31 | 32 | export default stretch(PageSizeDropdown) 33 | -------------------------------------------------------------------------------- /src/Paginator.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import FontAwesome from 'react-fontawesome' 3 | import classNames from 'classnames' 4 | 5 | import paginate from './decorators/paginate' 6 | import range from './lib/range' 7 | import { PageLink } from './PageLink' 8 | import { Prev } from './Prev' 9 | import { Next } from './Next' 10 | 11 | export function Paginator(props) { 12 | const { currentPage, totalPages, hasPreviousPage, hasNextPage } = props 13 | 14 | const upperOffset = Math.max(0, (currentPage - totalPages) + 3) 15 | const minPage = Math.max(props.currentPage - 3 - upperOffset, 1) 16 | const maxPage = Math.min(minPage + 6, totalPages) 17 | const prevClasses = classNames({ disabled: !hasPreviousPage }) 18 | const nextClasses = classNames({ disabled: !hasNextPage }) 19 | 20 | const pageLinks = [...range(minPage, maxPage)].map(page => { 21 | const pageLinkClass = classNames({ current: page === currentPage }) 22 | 23 | return ( 24 |
  • 25 | 26 |
  • 27 | ) 28 | }) 29 | 30 | const separator = totalPages > 7 ? ( 31 |
  • 32 | 33 |
  • 34 | ) : false 35 | 36 | const begin = separator && minPage > 1 ? ( 37 |
  • 38 | 39 |
  • 40 | ) : false 41 | 42 | const end = separator && maxPage < totalPages ? ( 43 |
  • 44 | 45 |
  • 46 | ) : false 47 | 48 | return ( 49 |
      50 |
    • 51 | 52 |
    • 53 | {begin} 54 | {begin && separator} 55 | {pageLinks} 56 | {end && separator} 57 | {end} 58 |
    • 59 | 60 |
    • 61 |
    62 | ) 63 | } 64 | 65 | Paginator.propTypes = { 66 | currentPage: PropTypes.number, 67 | totalPages: PropTypes.number, 68 | hasPreviousPage: PropTypes.bool, 69 | hasNextPage: PropTypes.bool 70 | } 71 | 72 | export default paginate(Paginator) 73 | -------------------------------------------------------------------------------- /src/Prev.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FontAwesome from 'react-fontawesome' 3 | import { flip } from './decorators' 4 | 5 | export function Prev({ pageActions, hasPreviousPage }) { 6 | const prev = 7 | const link = hasPreviousPage ? ( 8 | {prev} 9 | ) : prev 10 | 11 | return link 12 | } 13 | 14 | export default flip(Prev) 15 | -------------------------------------------------------------------------------- /src/SortLink.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import FontAwesome from 'react-fontawesome' 3 | import { sort as decorate } from './decorators' 4 | 5 | export function SortLink({ pageActions, field, text, sort, sortReverse, sortable=true }) { 6 | if (!sortable) { 7 | return {text} 8 | } 9 | 10 | const sortByField = () => 11 | pageActions.sort(field, !sortReverse) 12 | 13 | const arrow = sort === field && ( 14 | sortReverse ? 'angle-up' : 'angle-down' 15 | ) 16 | 17 | return ( 18 | 19 | {text} 20 | 21 | ) 22 | } 23 | 24 | SortLink.propTypes = { 25 | sort: PropTypes.string, 26 | sortReverse: PropTypes.bool, 27 | pageActions: PropTypes.object, 28 | field: PropTypes.string.isRequired, 29 | text: PropTypes.string.isRequired, 30 | sortable: PropTypes.bool 31 | } 32 | 33 | export default decorate(SortLink) 34 | 35 | -------------------------------------------------------------------------------- /src/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const INITIALIZE_PAGINATOR = '@@violet-paginator/INITIALIZE_PAGINATOR' 2 | export const EXPIRE_PAGINATOR = '@@violet-paginator/EXPIRE_PAGINATOR' 3 | export const EXPIRE_ALL = '@@violet-paginator/EXPIRE_ALL' 4 | export const FOUND_PAGINATOR = '@@violet-paginator/FOUND_PAGINATOR' 5 | export const PREVIOUS_PAGE = '@@violet-paginator/PREVIOUS_PAGE' 6 | export const NEXT_PAGE = '@@violet-paginator/NEXT_PAGE' 7 | export const GO_TO_PAGE = '@@violet-paginator/GO_TO_PAGE' 8 | export const SET_PAGE_SIZE = '@@violet-paginator/SET_PAGE_SIZE' 9 | export const FETCH_RECORDS = '@@violet-paginator/FETCH_RECORDS' 10 | export const RESULTS_UPDATED = '@@violet-paginator/RESULTS_UPDATED' 11 | export const RESULTS_UPDATED_ERROR = '@@violet-paginator/RESULTS_UPDATED_ERROR' 12 | export const TOGGLE_FILTER_ITEM = '@@violet-paginator/TOGGLE_FILTER_ITEM' 13 | export const SET_FILTER = '@@violet-paginator/SET_FILTER' 14 | export const SET_FILTERS = '@@violet-paginator/SET_FILTERS' 15 | export const RESET_FILTERS = '@@violet-paginator/RESET_FILTERS' 16 | export const SORT_CHANGED = '@@violet-paginator/SORT_CHANGED' 17 | export const UPDATING_ITEM = '@@violet-paginator/UPDATING_ITEM' 18 | export const UPDATE_ITEMS = '@@violet-paginator/UPDATE_ITEMS' 19 | export const UPDATE_ITEM = '@@violet-paginator/UPDATE_ITEM' 20 | export const UPDATING_ITEMS = '@@violet-paginator/UPDATING_ITEMS' 21 | export const RESET_ITEM = '@@violet-paginator/RESET_ITEM' 22 | export const MARK_ITEMS_ERRORED = '@@violet-paginator/MARK_ITEMS_ERRORED' 23 | export const RESET_RESULTS = '@@violet-paginator/RESET_RESULTS' 24 | export const REMOVING_ITEM = '@@violet-paginator/REMOVING_ITEM' 25 | export const REMOVE_ITEM = '@@violet-paginator/REMOVE_ITEM' 26 | export const ITEM_ERROR = '@@violet-paginator/ITEM_ERROR' 27 | 28 | export default function actionType(t, id) { 29 | return `${t}_${id}` 30 | } 31 | -------------------------------------------------------------------------------- /src/actions/fetchingComposables.js: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid' 2 | import actionType, * as actionTypes from './actionTypes' 3 | import { translate } from '../pageInfoTranslator' 4 | import { getPaginator, listConfig } from '../lib/stateManagement' 5 | 6 | const fetcher = id => 7 | (dispatch, getState) => { 8 | const { fetch, params } = listConfig(id) 9 | const pageInfo = getPaginator(id, getState()) 10 | const requestId = uuid.v1() 11 | 12 | dispatch({ type: actionType(actionTypes.FETCH_RECORDS, id), requestId }) 13 | 14 | const promise = dispatch(fetch(translate(pageInfo))) 15 | 16 | return promise.then(resp => 17 | dispatch({ 18 | type: actionType(actionTypes.RESULTS_UPDATED, id), 19 | results: resp.data[params.resultsProp], 20 | totalCount: resp.data[params.totalCountProp], 21 | requestId 22 | }) 23 | ).catch(error => 24 | dispatch({ 25 | type: actionType(actionTypes.RESULTS_UPDATED_ERROR, id), 26 | error 27 | }) 28 | ) 29 | } 30 | 31 | export default function fetchingComposables(config) { 32 | const id = config.listId 33 | const resolve = t => actionType(t, id) 34 | 35 | return { 36 | initialize: () => ({ 37 | type: resolve(actionTypes.INITIALIZE_PAGINATOR), 38 | preloaded: config.preloaded 39 | }), 40 | reload: () => fetcher(id), 41 | next: () => ({ 42 | type: resolve(actionTypes.NEXT_PAGE) 43 | }), 44 | prev: () => ({ 45 | type: resolve(actionTypes.PREVIOUS_PAGE) 46 | }), 47 | goTo: (page) => ({ 48 | type: resolve(actionTypes.GO_TO_PAGE), 49 | page 50 | }), 51 | setPageSize: (size) => ({ 52 | type: resolve(actionTypes.SET_PAGE_SIZE), 53 | size 54 | }), 55 | toggleFilterItem: (field, value) => ({ 56 | type: resolve(actionTypes.TOGGLE_FILTER_ITEM), 57 | field, 58 | value 59 | }), 60 | setFilter: (field, value) => ({ 61 | type: resolve(actionTypes.SET_FILTER), 62 | field, 63 | value 64 | }), 65 | setFilters: (filters) => ({ 66 | type: resolve(actionTypes.SET_FILTERS), 67 | filters 68 | }), 69 | resetFilters: (filters) => ({ 70 | type: resolve(actionTypes.RESET_FILTERS), 71 | filters 72 | }), 73 | sort: (field, reverse) => ({ 74 | type: resolve(actionTypes.SORT_CHANGED), 75 | field, 76 | reverse 77 | }) 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './actionTypes' 2 | import simpleComposables from './simpleComposables' 3 | import fetchingComposables from './fetchingComposables' 4 | 5 | export function expireAll() { 6 | return { 7 | type: actionTypes.EXPIRE_ALL 8 | } 9 | } 10 | 11 | export default function composables(config) { 12 | return { 13 | ...fetchingComposables(config), 14 | ...simpleComposables(config.listId) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/actions/simpleComposables.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable' 2 | import { recordProps } from '../pageInfoTranslator' 3 | import actionType, * as actionTypes from './actionTypes' 4 | import { getPaginator } from '../lib/stateManagement' 5 | 6 | const { identifier } = recordProps() 7 | 8 | export default function simpleComposables(id) { 9 | const basic = { 10 | expire: () => ({ 11 | type: actionType(actionTypes.EXPIRE_PAGINATOR, id) 12 | }), 13 | updatingItem: (itemId) => ({ 14 | type: actionType(actionTypes.UPDATING_ITEM, id), 15 | itemId 16 | }), 17 | updateItem: (itemId, data) => ({ 18 | type: actionType(actionTypes.UPDATE_ITEM, id), 19 | itemId, 20 | data 21 | }), 22 | updatingItems: (itemIds) => ({ 23 | type: actionType(actionTypes.UPDATING_ITEMS, id), 24 | itemIds 25 | }), 26 | updateItems: (itemIds, data) => ({ 27 | type: actionType(actionTypes.UPDATE_ITEMS, id), 28 | itemIds, 29 | data 30 | }), 31 | resetItem: (itemId, data) => ({ 32 | type: actionType(actionTypes.RESET_ITEM, id), 33 | itemId, 34 | data 35 | }), 36 | updatingAll: () => ({ 37 | type: actionType(actionTypes.UPDATING_ALL, id) 38 | }), 39 | updateAll: (data) => ({ 40 | type: actionType(actionTypes.UPDATE_ALL, id), 41 | data 42 | }), 43 | markItemsErrored: (itemIds, error) => ({ 44 | type: actionType(actionTypes.MARK_ITEMS_ERRORED, id), 45 | itemIds, 46 | error 47 | }), 48 | resetResults: (results) => ({ 49 | type: actionType(actionTypes.RESET_RESULTS, id), 50 | results 51 | }), 52 | removingItem: (itemId) => ({ 53 | type: actionType(actionTypes.REMOVING_ITEM, id), 54 | itemId 55 | }), 56 | removeItem: (itemId) => ({ 57 | type: actionType(actionTypes.REMOVE_ITEM, id), 58 | itemId 59 | }), 60 | itemError: (itemId, error) => ({ 61 | type: actionType(actionTypes.ITEM_ERROR, id), 62 | itemId, 63 | error 64 | }) 65 | } 66 | 67 | const updateAsync = (itemId, data, update) => 68 | (dispatch, getState) => { 69 | const item = getPaginator(id, getState()).get('results') 70 | .find(r => r.get(identifier) === itemId) || Map() 71 | 72 | dispatch(basic.updateItem(itemId, data)) 73 | dispatch(basic.updatingItem(itemId)) 74 | return update.then(serverUpdate => 75 | dispatch(basic.updateItem(itemId, serverUpdate)) 76 | ).catch(err => { 77 | dispatch(basic.resetItem(itemId, item.toJS())) 78 | return dispatch(basic.itemError(itemId, err)) 79 | }) 80 | } 81 | 82 | const updateItemsAsync = (itemIds, data, update, showUpdating = true) => 83 | (dispatch, getState) => { 84 | const results = getPaginator(id, getState()).get('results') 85 | 86 | dispatch(basic.updateItems(itemIds, data)) 87 | if (showUpdating) { 88 | dispatch(basic.updatingItems(itemIds)) 89 | } 90 | 91 | return update.then(resp => { 92 | if (showUpdating) { 93 | dispatch(basic.updateItems(itemIds, data)) 94 | } 95 | 96 | return resp 97 | }).catch(err => { 98 | dispatch(basic.resetResults(results.toJS())) 99 | return dispatch(basic.markItemsErrored(itemIds, err)) 100 | }) 101 | } 102 | 103 | const removeAsync = (itemId, remove) => 104 | (dispatch, getState) => { 105 | const item = getPaginator(id, getState()).get('results') 106 | .find(r => r.get(identifier) === itemId) || Map() 107 | 108 | dispatch(basic.removingItem(itemId)) 109 | return remove.then(() => 110 | dispatch(basic.removeItem(itemId)) 111 | ).catch(err => { 112 | dispatch(basic.resetItem(itemId, item.toJS())) 113 | return dispatch(basic.itemError(itemId, err)) 114 | }) 115 | } 116 | 117 | return { 118 | ...basic, 119 | updateAsync, 120 | updateItemsAsync, 121 | removeAsync 122 | } 123 | } 124 | 125 | -------------------------------------------------------------------------------- /src/containers/PaginationWrapper.jsx: -------------------------------------------------------------------------------- 1 | import { PropTypes, Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | import composables from '../actions' 5 | import { defaultPaginator } from '../reducer' 6 | import { preloadedPaginator } from '../lib/stateManagement' 7 | 8 | export const connector = connect( 9 | (state, ownProps) => ({ 10 | paginator: preloadedPaginator(state, ownProps.listId, ownProps.preloaded) 11 | }), 12 | (dispatch, ownProps) => ({ 13 | pageActions: bindActionCreators(composables(ownProps), dispatch) 14 | }) 15 | ) 16 | 17 | export class PaginationWrapper extends Component { 18 | static propTypes = { 19 | pageActions: PropTypes.object.isRequired, 20 | paginator: PropTypes.object, 21 | children: PropTypes.element.isRequired 22 | } 23 | 24 | static defaultProps = { 25 | paginator: defaultPaginator 26 | } 27 | 28 | componentDidMount() { 29 | const { paginator, pageActions } = this.props 30 | 31 | if (!paginator.get('initialized')) { 32 | pageActions.initialize() 33 | } 34 | 35 | this.reloadIfStale(this.props) 36 | } 37 | 38 | componentWillReceiveProps(nextProps) { 39 | this.reloadIfStale(nextProps) 40 | } 41 | 42 | reloadIfStale(props) { 43 | const { paginator, pageActions } = props 44 | if (paginator.get('stale') && !paginator.get('isLoading') && !paginator.get('loadError')) { 45 | pageActions.reload() 46 | } 47 | } 48 | 49 | render() { 50 | return this.props.children 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/decorators/decorate.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PaginationWrapper, connector } from '../containers/PaginationWrapper' 3 | 4 | export default function decorate(Component, decorator) { 5 | return connector(props => ( 6 | 7 | 11 | 12 | )) 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/decorators/flip.js: -------------------------------------------------------------------------------- 1 | import decorate from './decorate' 2 | import select from './selectors' 3 | 4 | export default function flip(Component) { 5 | return decorate(Component, props => select(props.paginator).flip()) 6 | } 7 | -------------------------------------------------------------------------------- /src/decorators/index.js: -------------------------------------------------------------------------------- 1 | export decorate from './decorate' 2 | export flip from './flip' 3 | export paginate from './paginate' 4 | export stretch from './stretch' 5 | export sort from './sort' 6 | export tabulate from './tabulate' 7 | export violetPaginator from './violetPaginator' 8 | -------------------------------------------------------------------------------- /src/decorators/paginate.js: -------------------------------------------------------------------------------- 1 | import decorate from './decorate' 2 | import select from './selectors' 3 | 4 | export default function paginate(Component) { 5 | return decorate(Component, props => select(props.paginator).paginate()) 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/decorators/selectors.js: -------------------------------------------------------------------------------- 1 | export default function select(paginator) { 2 | const totalPages = 3 | Math.ceil(paginator.get('totalCount') / paginator.get('pageSize')) 4 | 5 | const page = paginator.get('page') 6 | 7 | const flip = () => ({ 8 | hasPreviousPage: page > 1, 9 | hasNextPage: page < totalPages 10 | }) 11 | 12 | const paginate = () => ({ 13 | currentPage: page, 14 | totalPages, 15 | ...flip() 16 | }) 17 | 18 | const tabulate = () => ({ 19 | results: paginator.get('results'), 20 | isLoading: paginator.get('isLoading'), 21 | updating: paginator.get('updating'), 22 | removing: paginator.get('removing') 23 | }) 24 | 25 | const stretch = () => ({ 26 | pageSize: paginator.get('pageSize') 27 | }) 28 | 29 | const sort = () => ({ 30 | sort: paginator.get('sort'), 31 | sortReverse: paginator.get('sortReverse') 32 | }) 33 | 34 | const violetPaginator = () => ({ 35 | ...paginate(), 36 | ...tabulate(), 37 | ...stretch(), 38 | ...sort() 39 | }) 40 | 41 | return { 42 | flip, 43 | paginate, 44 | tabulate, 45 | stretch, 46 | sort, 47 | violetPaginator 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/decorators/sort.js: -------------------------------------------------------------------------------- 1 | import decorate from './decorate' 2 | import select from './selectors' 3 | 4 | export default function sort(Component) { 5 | return decorate(Component, props => select(props.paginator).sort()) 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/decorators/stretch.js: -------------------------------------------------------------------------------- 1 | import decorate from './decorate' 2 | import select from './selectors' 3 | 4 | export default function stretch(Component) { 5 | return decorate(Component, props => select(props.paginator).stretch()) 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/decorators/tabulate.js: -------------------------------------------------------------------------------- 1 | import decorate from './decorate' 2 | import select from './selectors' 3 | 4 | export default function tabulate(Component) { 5 | return decorate(Component, props => select(props.paginator).tabulate()) 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/decorators/violetPaginator.js: -------------------------------------------------------------------------------- 1 | import decorate from './decorate' 2 | import select from './selectors' 3 | 4 | export default function violetPaginator(Component) { 5 | return decorate(Component, props => select(props.paginator).violetPaginator()) 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export composables, { expireAll } from './actions' 2 | export VioletDataTable from './DataTable' 3 | export VioletFlipper from './Flipper' 4 | export VioletPaginator from './Paginator' 5 | export VioletSortLink from './SortLink' 6 | export VioletPrev from './Prev' 7 | export VioletNext from './Next' 8 | export VioletPageSizeDropdown from './PageSizeDropdown' 9 | export createPaginator from './reducer' 10 | export * as decorators from './decorators' 11 | export { configurePageParams } from './pageInfoTranslator' 12 | export { isUpdating, isRemoving, currentQuery } from './lib/stateManagement' 13 | -------------------------------------------------------------------------------- /src/lib/range.js: -------------------------------------------------------------------------------- 1 | import 'babel-regenerator-runtime' 2 | 3 | export default function* range(low, high) { 4 | for (let i = low; i <= high; i++) { 5 | yield i 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/reduxResolver.js: -------------------------------------------------------------------------------- 1 | export function updateListItem(list, id, update, identifier = 'id') { 2 | return list.map(i => { 3 | if (i.get(identifier) === id) { 4 | return update(i) 5 | } 6 | 7 | return i 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/stateManagement.js: -------------------------------------------------------------------------------- 1 | import { defaultPaginator } from '../reducer' 2 | import { translate, responseProps } from '../pageInfoTranslator' 3 | 4 | const stateMap = {} 5 | const defaultLocator = listId => state => state[listId] 6 | const preload = { results: [] } 7 | 8 | const defaultPageParams = () => { 9 | const [totalCountProp, resultsProp] = responseProps() 10 | 11 | return { 12 | totalCountProp, 13 | resultsProp 14 | } 15 | } 16 | 17 | export function registerPaginator({ 18 | listId, 19 | fetch, 20 | initialSettings = {}, 21 | pageParams = {}, 22 | locator = defaultLocator(listId) 23 | }) { 24 | stateMap[listId] = { 25 | locator, 26 | fetch, 27 | initialSettings, 28 | params: { 29 | ...defaultPageParams(), 30 | ...pageParams 31 | } 32 | } 33 | 34 | return stateMap[listId] 35 | } 36 | 37 | export function getPaginator(listId, state) { 38 | const config = stateMap[listId] || { 39 | locator: defaultLocator(listId) 40 | } 41 | 42 | return config.locator(state) || defaultPaginator 43 | } 44 | 45 | export function listConfig(listId) { 46 | return stateMap[listId] 47 | } 48 | 49 | export function preloadedPaginator(state, listId, preloaded = preload) { 50 | const paginator = getPaginator(listId, state) 51 | return paginator.equals(defaultPaginator) ? paginator.merge(preloaded) : paginator 52 | } 53 | 54 | export function isUpdating(state, listId, itemId) { 55 | const paginator = getPaginator(listId, state) 56 | return paginator.get('updating').includes(itemId) 57 | } 58 | 59 | export function isRemoving(state, itemId) { 60 | return state.get('removing').includes(itemId) 61 | } 62 | 63 | export function currentQuery(state, listId) { 64 | return translate(getPaginator(listId, state)) 65 | } 66 | -------------------------------------------------------------------------------- /src/pageInfoTranslator.js: -------------------------------------------------------------------------------- 1 | let [ 2 | pageParam, 3 | pageSizeParam, 4 | sortParam, 5 | sortOrderParam, 6 | useBooleanOrdering, 7 | totalCountProp, 8 | resultsProp, 9 | idProp 10 | ] = [ 11 | 'page', 12 | 'pageSize', 13 | 'sort', 14 | 'sortOrder', 15 | false, 16 | 'total_count', 17 | 'results', 18 | 'id' 19 | ] 20 | 21 | export function responseProps() { 22 | return [totalCountProp, resultsProp] 23 | } 24 | 25 | export function recordProps() { 26 | return { identifier: idProp } 27 | } 28 | 29 | export function configurePageParams({ 30 | page, 31 | perPage, 32 | sort, 33 | sortOrder, 34 | sortReverse, 35 | totalCount, 36 | results, 37 | id 38 | }) { 39 | if (page) { 40 | pageParam = page 41 | } 42 | 43 | if (perPage) { 44 | pageSizeParam = perPage 45 | } 46 | 47 | if (sort) { 48 | sortParam = sort 49 | } 50 | 51 | if (sortOrder) { 52 | sortOrderParam = sortOrder 53 | } 54 | 55 | if (totalCount) { 56 | totalCountProp = totalCount 57 | } 58 | 59 | if (results) { 60 | resultsProp = results 61 | } 62 | 63 | if (id) { 64 | idProp = id 65 | } 66 | 67 | useBooleanOrdering = !!sortReverse 68 | } 69 | 70 | function sortDirection(value) { 71 | if (useBooleanOrdering) { 72 | return value 73 | } 74 | 75 | return value ? 'desc' : 'asc' 76 | } 77 | 78 | function sortParams(paginator) { 79 | if (paginator.get('sort')) { 80 | return { 81 | [sortParam]: paginator.get('sort'), 82 | [sortOrderParam]: sortDirection(paginator.get('sortReverse')) 83 | } 84 | } 85 | 86 | return {} 87 | } 88 | 89 | export function translate(paginator) { 90 | const { 91 | id, 92 | page, 93 | pageSize, 94 | filters 95 | } = paginator.toJS() 96 | 97 | return { 98 | id, 99 | query: { 100 | [pageParam]: page, 101 | [pageSizeParam]: pageSize, 102 | ...sortParams(paginator), 103 | ...filters 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | import Immutable, { Map, List, Set } from 'immutable' 2 | import { resolveEach } from 'redux-resolver' 3 | import { updateListItem } from './lib/reduxResolver' 4 | import actionType, * as actionTypes from './actions/actionTypes' 5 | import { recordProps } from './pageInfoTranslator' 6 | import { registerPaginator } from './lib/stateManagement' 7 | 8 | export const defaultPaginator = Map({ 9 | initialized: false, 10 | page: 1, 11 | pageSize: 15, 12 | totalCount: 0, 13 | sort: '', 14 | sortReverse: false, 15 | isLoading: false, 16 | stale: false, 17 | results: List(), 18 | updating: Set(), 19 | removing: Set(), 20 | requestId: null, 21 | loadError: null, 22 | filters: Map() 23 | }) 24 | 25 | function initialize(state, action) { 26 | return state.merge({ 27 | initialized: true, 28 | stale: !action.preloaded, 29 | ...(action.preloaded || {}) 30 | }) 31 | } 32 | 33 | function expire(state) { 34 | return state.merge({ stale: true, loadError: null }) 35 | } 36 | 37 | function next(state) { 38 | return expire(state.set('page', state.get('page') + 1)) 39 | } 40 | 41 | function prev(state) { 42 | return expire(state.set('page', state.get('page') - 1)) 43 | } 44 | 45 | function goToPage(state, action) { 46 | return expire(state.set('page', action.page)) 47 | } 48 | 49 | function setPageSize(state, action) { 50 | return expire( 51 | state.merge({ 52 | pageSize: action.size, 53 | page: 1 54 | }) 55 | ) 56 | } 57 | 58 | function toggleFilterItem(state, action) { 59 | const items = state.getIn(['filters', action.field], Set()).toSet() 60 | 61 | return expire( 62 | state.set('page', 1).setIn( 63 | ['filters', action.field], 64 | items.includes(action.value) ? items.delete(action.value) :items.add(action.value) 65 | ) 66 | ) 67 | } 68 | 69 | function setFilter(state, action) { 70 | return expire( 71 | state.setIn( 72 | ['filters', action.field], 73 | Immutable.fromJS(action.value) 74 | ).set('page', 1) 75 | ) 76 | } 77 | 78 | function setFilters(state, action) { 79 | return expire( 80 | state.set( 81 | 'filters', 82 | state.get('filters').merge(action.filters) 83 | ).set('page', 1) 84 | ) 85 | } 86 | 87 | function resetFilters(state, action) { 88 | return expire( 89 | state.set( 90 | 'filters', 91 | Immutable.fromJS(action.filters || {}) 92 | ).set('page', 1) 93 | ) 94 | } 95 | 96 | function sortChanged(state, action) { 97 | return expire( 98 | state.merge({ 99 | sort: action.field, 100 | sortReverse: action.reverse, 101 | page: 1 102 | }) 103 | ) 104 | } 105 | 106 | function fetching(state, action) { 107 | return state.merge({ 108 | isLoading: true, 109 | requestId: action.requestId 110 | }) 111 | } 112 | 113 | function updateResults(state, action) { 114 | if (action.requestId !== state.get('requestId')) { 115 | return state 116 | } 117 | 118 | return state.merge({ 119 | results: Immutable.fromJS(action.results), 120 | totalCount: action.totalCount, 121 | isLoading: false, 122 | stale: false 123 | }) 124 | } 125 | 126 | function resetResults(state, action) { 127 | return state.set('results', Immutable.fromJS(action.results)) 128 | } 129 | 130 | function error(state, action) { 131 | return state.merge({ 132 | isLoading: false, 133 | loadError: action.error 134 | }) 135 | } 136 | 137 | function updatingItem(state, action) { 138 | return state.set('updating', state.get('updating').add(action.itemId)) 139 | } 140 | 141 | function updateItem(state, action) { 142 | return state.merge({ 143 | updating: state.get('updating').toSet().delete(action.itemId), 144 | results: updateListItem( 145 | state.get('results'), action.itemId, 146 | item => item.merge(action.data).set('error', null), 147 | recordProps().identifier 148 | ) 149 | }) 150 | } 151 | 152 | function updateItems(state, action) { 153 | const { itemIds } = action 154 | 155 | return state.merge({ 156 | updating: state.get('updating').toSet().subtract(itemIds), 157 | results: state.get('results').map(r => { 158 | if (itemIds.includes(r.get(recordProps().identifier))) { 159 | return r.merge(action.data).set('error', null) 160 | } 161 | 162 | return r 163 | }) 164 | }) 165 | } 166 | 167 | function updatingItems(state, action) { 168 | const { itemIds } = action 169 | 170 | return state.set('updating', state.get('updating').toSet().union(itemIds)) 171 | } 172 | 173 | function resetItem(state, action) { 174 | return state.merge({ 175 | updating: state.get('updating').toSet().delete(action.itemId), 176 | results: updateListItem( 177 | state.get('results'), action.itemId, 178 | () => Immutable.fromJS(action.data), 179 | recordProps().identifier 180 | ) 181 | }) 182 | } 183 | 184 | function removingItem(state, action) { 185 | return state.set('removing', state.get('removing').add(action.itemId)) 186 | } 187 | 188 | function removeItem(state, action) { 189 | return state.merge({ 190 | totalCount: state.get('totalCount') - 1, 191 | removing: state.get('removing').toSet().delete(action.itemId), 192 | results: state.get('results').filter( 193 | item => item.get(recordProps().identifier) !== action.itemId 194 | ) 195 | }) 196 | } 197 | 198 | function itemError(state, action) { 199 | return state.merge({ 200 | updating: state.get('updating').toSet().delete(action.itemId), 201 | removing: state.get('removing').toSet().delete(action.itemId), 202 | results: updateListItem( 203 | state.get('results'), 204 | action.itemId, 205 | item => item.set('error', Immutable.fromJS(action.error)), 206 | recordProps().identifier 207 | ) 208 | }) 209 | } 210 | 211 | function markItemsErrored(state, action) { 212 | const { itemIds } = action 213 | 214 | return state.merge({ 215 | updating: state.get('updating').toSet().subtract(itemIds), 216 | removing: state.get('removing').toSet().subtract(itemIds), 217 | results: state.get('results').map(r => { 218 | if (itemIds.includes(r.get(recordProps().identifier))) { 219 | return r.set('error', Immutable.fromJS(action.error)) 220 | } 221 | 222 | return r 223 | }) 224 | }) 225 | } 226 | 227 | export default function createPaginator(config) { 228 | const { initialSettings } = registerPaginator(config) 229 | const resolve = t => actionType(t, config.listId) 230 | 231 | return resolveEach(defaultPaginator.merge(initialSettings), { 232 | [actionTypes.EXPIRE_ALL]: expire, 233 | [resolve(actionTypes.INITIALIZE_PAGINATOR)]: initialize, 234 | [resolve(actionTypes.EXPIRE_PAGINATOR)]: expire, 235 | [resolve(actionTypes.PREVIOUS_PAGE)]: prev, 236 | [resolve(actionTypes.NEXT_PAGE)]: next, 237 | [resolve(actionTypes.GO_TO_PAGE)]: goToPage, 238 | [resolve(actionTypes.SET_PAGE_SIZE)]: setPageSize, 239 | [resolve(actionTypes.FETCH_RECORDS)]: fetching, 240 | [resolve(actionTypes.RESULTS_UPDATED)]: updateResults, 241 | [resolve(actionTypes.RESULTS_UPDATED_ERROR)]: error, 242 | [resolve(actionTypes.TOGGLE_FILTER_ITEM)]: toggleFilterItem, 243 | [resolve(actionTypes.SET_FILTER)]: setFilter, 244 | [resolve(actionTypes.SET_FILTERS)]: setFilters, 245 | [resolve(actionTypes.RESET_FILTERS)]: resetFilters, 246 | [resolve(actionTypes.SORT_CHANGED)]: sortChanged, 247 | [resolve(actionTypes.UPDATING_ITEM)]: updatingItem, 248 | [resolve(actionTypes.UPDATE_ITEM)]: updateItem, 249 | [resolve(actionTypes.UPDATING_ITEMS)]: updatingItems, 250 | [resolve(actionTypes.UPDATE_ITEMS)]: updateItems, 251 | [resolve(actionTypes.RESET_ITEM)]: resetItem, 252 | [resolve(actionTypes.MARK_ITEMS_ERRORED)]: markItemsErrored, 253 | [resolve(actionTypes.RESET_RESULTS)]: resetResults, 254 | [resolve(actionTypes.REMOVING_ITEM)]: removingItem, 255 | [resolve(actionTypes.REMOVE_ITEM)]: removeItem, 256 | [resolve(actionTypes.ITEM_ERROR)]: itemError 257 | }) 258 | } 259 | --------------------------------------------------------------------------------