├── .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 | [](https://github.com/sslotsky/violet-paginator)
2 | [](https://www.npmjs.com/package/violet-paginator)
3 | [](https://www.npmjs.com/package/violet-paginator)
4 | [](https://travis-ci.org/sslotsky/violet-paginator)
5 | [](https://github.com/sslotsky/violet-paginator)
6 | [](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 |
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 |
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 | {n}
14 | )
15 |
16 | const setPageSize = e =>
17 | pageActions.setPageSize(parseInt(e.target.value, 10))
18 |
19 | return (
20 |
21 | {optionTags}
22 |
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 |
--------------------------------------------------------------------------------