Repository: viniciusdacal/redux-arc Branch: master Commit: afcb9d93acd7 Files: 50 Total size: 157.0 KB Directory structure: gitextract_qamzj16o/ ├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CNAME ├── LICENSE ├── README.html ├── README.md ├── book.json ├── build/ │ └── gitbook.css ├── docs/ │ ├── README.md │ ├── advanced/ │ │ ├── README.md │ │ └── RequestMiddlewares.md │ └── basics/ │ ├── ActionCreators.md │ ├── ActionTypes.md │ ├── CreatingAsyncMiddleware.md │ ├── README.md │ ├── RequestAndResponseValues.md │ └── RequestsDefinition.md ├── package.json ├── redux-arc.sublime-project ├── redux-arc.sublime-workspace ├── rollup.config.js ├── src/ │ ├── apiActionCreatorFactory.js │ ├── createActions.js │ ├── createAsyncMiddleware.js │ ├── createCreators.js │ ├── createReducers.js │ ├── createTypes.js │ ├── fsaActionCreatorFactory.js │ ├── index.js │ ├── parseUrl.js │ ├── requestMiddlewares.js │ ├── toApiExternalType.js │ ├── toExternalTypes.js │ ├── utils.js │ └── validateConfig.js └── test/ ├── .eslintrc ├── constants.js ├── createActions.spec.js ├── createAsyncMiddleware.spec.js ├── createCreators.spec.js ├── createReducers.spec.js ├── createTypes.spec.js ├── middlewareWithRedux.spec.js ├── parseUrl.spec.js ├── publicApi.spec.js ├── requestMiddlewares.spec.js ├── toExternalTypes.spec.js └── validateConfig.spec.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ [ "env", { "targets": { "browsers": [ "ie >= 11" ] }, "exclude": ["transform-async-to-generator", "transform-regenerator"], "modules": false, "loose": true } ] ], "plugins": [ "transform-object-rest-spread" ], "env": { "commonjs": { "presets": [ [ "env", { "loose": true } ] ] } } } ================================================ FILE: .eslintrc ================================================ { "extends": "react-app" } ================================================ FILE: .gitignore ================================================ node_modules .idea coverage .DS_Store .nvmrc npm-debug.log yarn-error.log eslint.json jasmine.json jasmine.*.json bin/* #*git* #.git* *.tar* *.gz* *dump* .~* logs/*.log .coverdata .coverrun results/ jsconfig.json .tern-project .vscode dist lib es coverage _book ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "node" script: - npm run lint - npm run test:cov after_success: - npm run coveralls cache: yarn ================================================ FILE: CNAME ================================================ redux-arc.js.org ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-2016 Reselect Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.html ================================================ README

Create scalable, no-boilerplate redux Apps!

Arc is an abstraction layer to help you reduce boilerplate on redux-apps and also, organize better your code. Additionally, it has utilities to handle async requests.

build status npm version
Coverage Status

Why

Redux is awesome! But people often complain about how much boilerplate they have to write when using it. Part of this problem, is because they feel unproductive defining constants, action creators and big reducers, but also because they don’t have a clear idea on how to organize their project, or even how to proper handle async requests. This project, intends to help on all that aspects!

We don’t intend to recreate the wheel, instead, we tried to use what the community are used with, and build up some approaches together in order to clarify the things about the project architecture, code splitting and the things around actions.

Creators and types are generated by a config

  // actions.js
  import { createActions } from 'redux-arc';

  export { types, creators } = createActions('jedi', {
    add: null,
  });

Create reducers using createReducers and types:

  import { createReducers } from 'redux-arc';
  import { types } from './actions';

  const INITIAL_STATE = [];
  const HANDLERS = {
    [types.ADD]: (state, action) => [...state, action.payload]
  };

  export default createReducers(INITIAL_STATE, HANDLERS);

Call the creators providing payload and meta

  import { creators } from './actions';

  const payload = {
    name: 'Luke',
    master: 'Yoda',
  };
  const meta = { foo: 'bar' };

  dispatch(creators.add(payload, meta))

Create Async Actions

  import { createActions } from 'redux-arc';

  export { types, creators } = createActions('jedi', {
    add: { url: '/api/jedi' method: 'post'},
  });

  dispatch(creators.add(payload, meta));

  types.ADD.REQUEST // JEDI_ADD_REQUEST
  types.ADD.RESPONSE // JEDI_ADD_RESPONSE

createActions creates both, regular and async actions. Async types has REQUEST and RESPONSE type, respectively to when a request starts and when it finishes.

Getting started

yarn add redux-arc

or

npm i --save redux-arc

To understand this docs, you should have a good understand of redux: what are Action creators, what are reducers, middlewares and also, what is a Flux Standard Action. So please, if you need a recap on those concepts, read the links bellow:

Redux Docs
Flux Standard Action

Action creators and Types

When you have to create a new action on redux, the first 2 steps you usually do is defining a const to you action type and then defining an action creator.

const NEW_JEDI = 'NEW_JEDI';

const newJedi = (payload) => ({
  type: NEW_JEDI,
  payload,
});

The above code is fine, but the problem is, you have dozens or hundreds of action in an application, and you are always writing the same code. Also, if you are not strict on code review, you end up having situations where your action type has a name different from your action creator:

const NEW_JEDI = 'NEW_JEDI';

const addJedi = (payload) => ({
  type: NEW_JEDI,
  payload,
});

Thinking in the above issues, Arc has a createAction function, that you use to define your actions and it generates the types and action creators automatically for you. Take a look:

  import { createActions } from 'redux-arc';

  const { types, creators } = createActions('yourNamespace', {
    newJedi: null,
  });

  types.newJedi // YOUR_NAMESPACE_NEW_JEDI

  const payload = {
    master: 'Yoda',
    name: 'Luke',
  };

  creators.newJedi(payload);
  /*
  {
    type: 'YOUR_NAMESPACE_NEW_JEDI',
    payload: {
      master: 'Yoda',
      name: 'Luke',
    }
  }
  */

The createActions method, expects a namespace as its first argument, this will be uppercased and will serve as a prefix for the actions. As the second parameter, we need to provide a config object, which the key is the action name and the value is an object with default values for payload and meta, or null if you don’t want to provide defaults. Then, it will return creators and types.

Both creators and types are objects, the first one contains the action creators for the actions you defined. As in the case above we defined an action newJedi, then we have a creator at creators.newJedi.

Creators accepts until three arguments:
- payload: could be of any type. Will become the action.payload
- meta: could be of any type. Will become the action.meta
- error: boolean. Indicates if the action has an error or not. You can omit it if the action has no error.

The api was designed following the flux-standard-action concepts. It can be strict about how your action should look like, but this will help you creating better actions. Also, now you can be sure that the first parameter you provide to the action creator, will become the action.payload.

The types is an object that contains strings. Its keys are the action names, but different from the creators, here they are uppercased:

  types.NEW_JEDI

Also, when you do a console.log to see its content, you can see that we prefix the actions with the namespace you provided:

  const { types } = createActions('yourNamespace', {
    newJedi: null,
  });

  types.NEW_JEDI // -> YOUR_NAMESPACE_NEW_JEDI

We decided to have the namespace, to not stop you from having actions with the same name in different modules. Don’t worry, you will be able to differ them easily when using redux-dev-tools, just remember to provide unique namespaces.

Async Actions

Originally, Arc was created to be an abstraction layer to handle async request in Redux. So, it has all you need about that. The api to generate async actions and types is the same, you only need to provide some additional params in the action config object. Take a look below:

  import { createApiActions } from 'redux-arc';

  const { creators, types } = createActions('todo', {
    list:   { url: 'api/todo',     method: 'get'  },
    read:   { url: 'api/todo/:id', method: 'get'  },
    create: { url: 'api/todo',     method: 'post' },
    update: { url: 'api/todo/:id', method: 'put'  },
  });

Above are defined three actions: list, read, create.

Two params are required for a async action config, url and method.
- url: You can define any url you want and it also accepts dynamic params, as you can see in the read action. We defined a dynamic param id, inserting :id
- method: Generally speaking, any http method your request lib supports. This will be used only by you in the asyncMiddleware that you will configure.

Async Creators and Async Types

An async creator is very similar to a simple creator. It accepts payload and meta as arguments. They will further become the action.payload and action.meta, as in a regular creator. The difference here, is that the meta, should be an object, and its values will be used to parse dynamic urls. Considering that, the creator read should be used like that:

  const payload = null;
  const meta = { id: '123' };
  creators.read(payload, meta);

With the above code, the final url to your read request, would be api/todo/123

The async types differ a little bit from the regular ones as well. First, as a api call has two moments (request and response), we need two different types to use inside our reducers. Considering that, Arc returns an object for each type, containing a REQUEST and a RESPONSE key with the respective types:

  types.READ.REQUEST == 'TODO_READ_REQUEST';
  types.READ.RESPONSE == 'TODO_READ_RESPONSE';

Async Middleware

Arc doesn’t intend to be a request lib, so, you need to tell it how you want to make your requests, and you do that by configuring the asyncMiddleware.

It’s quite simple, take a look below in an example using axios:

import { createAsyncMiddleware } from 'redux-arc';
import axios from 'axios';

const asyncTask = store => done => (options) => {
  const { method, url, payload } = options;
  const params = method === 'get' ? { params: payload } : payload;

  axios[method](url, params).then((error, response) => done(error, response));
};

// create the async middleware
const asyncMiddleware = createAsyncMiddleware(asyncTask);

// set it to the Store
const store = createStore(
  reducer,
  applyMiddleware(asyncMiddleware),
);

In the above example, we are using axios, but you can use whatever you want to perform the request, just make sure you call done, passing error and response when the request ends.

For more info about asyncTask and createAsyncMiddleware, read Connecting Arc Into Redux

Reducers

Beyond types and action creators, we also have reducers. There are a few ways to deal with them, some approaches use switch case, some others use multiple IFs. With most of them, you end up having a lot of code inside the same function, which makes maintenance and focusing hard. I know some approaches mention that you can split your code into small functions when it gets bigger, but why do not start from something that is easy to scale and also allow you to focus on each action separately?

Thinking about that, we created the function createReducers

createReducers

This factory was created to work standalone, it doesn’t require you to use any other feature from Arc. If you like, you can continue creating your actions and types as you always did. Take a look at the example below:

  // vanillaActions.js
  export const ADD_NEW_JEDI = 'ADD_NEW_JEDI';

  export const addNewJedi = (name, master) => ({
    type: ADD_NEW_JEDI,
    name,
    master,
  });

Above we defined our types and action creator using pure JavaScript . Below, you can see how we could use it with createReducers:

  import { createReducers } from 'redux-arc';
  import { ADD_NEW_JEDI } from './vanillaActions';

  const INITIAL_STATE = [];

  const onAddNewJedi = (state, action) => [
    ...state,
    { name: action.name, master: action.master },
  ];

  const HANDLERS = {
    [ADD_NEW_JEDI]: onAddNewJedi
  };

  export default createReducers(INITIAL_STATE, HANDLERS);

You must provide a INITIAL_STATE and a HANDLERS object, which the keys should be the action types and the values should be reducers.

Using createActions with arc’s types object
As the types object is a simple JavaScript object, with strings, it fits perfectly with createReducers. Take a look below in an example using both, a regular and a async action with createReducers:

actions.js:

  const { creators, types } = createActions('todo', {
    list: { url: 'api/todo', method: 'get'  }, // async action
    reset: null, // regular action,
  });

reducers.js

import { createReducers } from 'redux-arc'
import { types } from './actions';

const INITIAL_STATE = {
  listResult: [],
  listIsLoading: false,
  listError: null,
};

const onListRequest = (state, action) => ({
  ...state,
  listIsLoading: true,
  listError: INITIAL_STATE.listError,
});

const onListResponse = (state, action) => {
  if (action.error) {
    return {
      ...state,
      listIsLoading: INITIAL_STATE.listIsLoading,
      listError: action.payload,
    }
  }

  return {
    ...state,
    listIsLoading: INITIAL_STATE.listIsLoading,
    listResult: action.payload
  }
};

const onReset = state => INITIAL_STATE;

const HANDLERS = {
  [types.LIST.REQUEST]: onListRequest,
  [types.LIST.RESPONSE]: onListResponse,
  [types.RESET]: onReset,
};

export default createReducers(INITIAL_STATE, HANDLERS);

As we mentioned, we are using two kinds of action types, regular and async.

For the regular, there’s no much secret, types.RESET contains a string TODO_RESET generated by createActions, using the namespace and the action name we defined earlier.

The async action, contains an object, with REQUEST and RESPONSE keys, each one containing a string, which is the actual type.

Arc will dispatch the respective request and response action using the same types we have in types object and the same applies for regular actions, so Arc doesn’t open breaches for you to commit a typo.

Async Actions

Response action:

When the request is done, an action with the response will be dispatched. Considering the list example, the response action would look like this:

{
  type: 'JEDI_LIST_RESPONSE', // types.LIST.RESPONSE,
  meta: {
    url: 'api/todo',
    method: 'get',
  },
  payload: [
    // resource list
  ],
}

Error handling

The above example is a a response with success. Accordingly to FSA, errors should be treated as a First class concept. In that case, when you got some error in an async request, the response action will come with the error property as true and the payload will be the actual error. Just like the example below:

{
  type: 'JEDI_LIST_RESPONSE', // types.LIST.RESPONSE,
  meta: {
    url: 'api/todo',
    method: 'get',
  },
  payload: new Error('the request error'),
  error: true,
}
================================================ FILE: README.md ================================================ Create scalable, no-boilerplate redux Apps! Arc is an abstraction layer to help you reduce boilerplate on redux-apps and also, organize better your code. Additionally, it has utilities to handle async requests. [![build status](https://img.shields.io/travis/viniciusdacal/redux-arc/master.svg?style=flat-square)](https://travis-ci.org/viniciusdacal/redux-arc) [![npm version](https://img.shields.io/npm/v/redux-arc.svg?style=flat-square)](https://www.npmjs.com/package/redux-arc) [![Coverage Status](https://coveralls.io/repos/github/viniciusdacal/redux-arc/badge.svg?branch=master)](https://coveralls.io/github/viniciusdacal/redux-arc?branch=master) ## Why Redux is awesome! But people often complain about how much boilerplate they have to write when using it. Part of this problem, is because they feel unproductive defining constants, action creators and big reducers, but also because they don't have a clear idea on how to organize their project, or even how to proper handle async requests. This project, intends to help on all that aspects! We don't intend to recreate the wheel, instead, we tried to use what the community are used with, and build up some approaches together in order to clarify the things about the project architecture, code splitting and the things around actions. ## Action creators and types generated by a config ```js // actions.js import { createActions } from 'redux-arc'; export { types, creators } = createActions('jedi', { add: null, reset: null }); ``` ## Create reducers reusing types and without switch cases: ```js import { createReducers } from 'redux-arc'; import { types } from './actions'; const INITIAL_STATE = []; const onAdd = (state, action) => [ ...state, action.payload, ]; const onReset = () => INITIAL_STATE; const HANDLERS = { [types.ADD]: onAdd, [types.RESET]: onReset, }; export default createReducers(INITIAL_STATE, HANDLERS); ``` ## Call the creators providing payload and meta ```js import { creators } from './actions'; const payload = { name: 'Luke' }; const meta = { foo: 'bar' }; dispatch(creators.add(payload, meta)); /* { type: 'JEDI_ADD', payload: { name: 'luke' }, meta: { foo: 'bar' }, } */ dispatch(creators.reset()); // { type: 'JEDI_RESET' } ``` ## Create Async Actions ```js import { createActions } from 'redux-arc'; export { types, creators } = createActions('jedi', { add: { url: '/api/jedi' method: 'post'}, }); dispatch(creators.add(payload, meta)); types.ADD.REQUEST // JEDI_ADD_REQUEST types.ADD.RESPONSE // JEDI_ADD_RESPONSE ``` > createActions creates both, regular and **Async Actions**. Async types has **REQUEST** and **RESPONSE** type, respectively to when a request starts and when it finishes. ## Demo Project Take a look at the demo project using Arc to build a Contacts CRUD: [github.com/redux-arc/redux-arc-demo](https://github.com/redux-arc/redux-arc-demo) # Getting started ```bash yarn add redux-arc ``` or ```bash npm i --save redux-arc ``` To understand this docs, you should have a good understand of redux: what are Action creators, what are reducers, middlewares and also, what is a Flux Standard Action. So please, if you need a recap on those concepts, read the links bellow: [Redux Docs](https://redux.js.org/) [Flux Standard Action](https://github.com/acdlite/flux-standard-action) # Action creators and Types When you have to create a new action on redux, the first 2 steps you usually do is defining a **const** to you *action type* and then defining an *action creator*. ```js const ADD_JEDI = 'ADD_JEDI'; const addJedi = (payload) => ({ type: ADD_JEDI, payload, }); ``` The above code is fine, but the problem is, you have dozens or hundreds of action in an application, and you are always writing the same code. Also, if you are not strict on code review, you end up having situations where your action type has a name different from your action creator: ```js const ADD_JEDI = 'ADD_JEDI'; const addJedi = (payload) => ({ type: ADD_JEDI, payload, }); ``` Thinking in the above issues, Arc has a `createAction` function, that you use to define your actions and it generates the types and action creators automatically for you. Take a look: ```js import { createActions } from 'redux-arc'; const { types, creators } = createActions('yourNamespace', { addJedi: null, }); types.addJedi // YOUR_NAMESPACE_ADD_JEDI const payload = { master: 'Yoda', name: 'Luke', }; creators.addJedi(payload); /* { type: 'YOUR_NAMESPACE_ADD_JEDI', payload: { master: 'Yoda', name: 'Luke', } } */ ``` The `createActions` method, expects a namespace as its first argument, this will be uppercased and will serve as a prefix for the actions. As the second parameter, we need to provide a config object, which the key is the action name and the value is an object with default values for **payload** and **meta**, or `null` if you don't want to provide defaults. Then, it will return **creators** and **types**. Both creators and types are objects, the first one contains the action creators for the actions you defined. As in the case above we defined an action addJedi, then we have a creator at `creators.addJedi`. Creators accepts until three arguments: - payload: could be of any type. Will become the `action.payload` - meta: could be of any type. Will become the `action.meta` - error: boolean. Indicates if the action has an error or not. You can omit it if the action has no error. > The api was designed following the flux-standard-action concepts. It can be strict about how your action should look like, but this will help you creating better actions. Also, now you can be sure that the first parameter you provide to the action creator, will become the action.payload. The types is an object that contains strings. Its keys are the action names, but different from the creators, here they are uppercased: ```js types.ADD_JEDI ``` Also, when you do a console.log to see its content, you can see that we prefix the actions with the namespace you provided: ```js const { types } = createActions('yourNamespace', { addJedi: null, }); types.ADD_JEDI // -> YOUR_NAMESPACE_ADD_JEDI ``` > We decided to have the namespace, to not stop you from having actions with the same name in different modules. Don't worry, you will be able to differ them easily when using redux-dev-tools, just remember to provide unique namespaces. # Reducers Beyond types and action creators, we also have reducers. There are a few ways to deal with them, some approaches use `switch case`, some others use multiple `IFs`. With most of them, you end up having a lot of code inside the same function, which makes maintenance and focusing hard. I know some approaches mention that you can split your code into small functions when it gets bigger, but why do not start from something that is easy to scale and also allow you to focus on each action separately? Thinking about that, we created the function `createReducers` ## createReducers This factory was created to work standalone, it doesn't require you to use any other feature from Arc. If you like, you can continue creating your actions and types as you always did. Take a look at the example below: ```js // vanillaActions.js export const ADD_TODO = 'ADD_TODO'; export const RESET_TODOS = 'RESET_TODOS'; export const addTodo = (title, completed) => ({ type: ADD_TODO, payload: { title, completed, } }); export const resetTodos = () => ({ type: RESET_TODOS, }); ``` Above we defined our type and action creator using pure JavaScript. Below, you can see how we could use it with **createReducers**: ```js import { createReducers } from 'redux-arc'; import { ADD_TODO, RESET_TODOS } from './vanillaActions'; const INITIAL_STATE = []; const onAddTodo = (state, action) => [ ...state, { name: action.name, master: action.master }, ]; const onResetTodos = (state, action) => INITIAL_STATE; const HANDLERS = { [ADD_TODO]: onAddTodo [RESET_TODOS]: onResetTodos }; export default createReducers(INITIAL_STATE, HANDLERS); ``` You must provide an `INITIAL_STATE` and a `HANDLERS` object, which the keys should be action types and the values should be reducers. **Using createReducers with arc's types object** As types generated from Arc is a simple JavaScript object, with strings, it fits perfectly with **createReducers**. Take a look below: **actions.js**: ```js const { creators, types } = createActions('todo', { addTodo: null, resetTodos: null, }); ``` **reducers.js** ```js import { createReducers } from 'redux-arc' import { types } from './actions'; const INITIAL_STATE = []; const onAddTodo = (state, action) => [ ...state, { name: action.name, master: action.master }, ]; const onResetTodos = (state, action) => INITIAL_STATE; const HANDLERS = { [types.ADD_TODO]: onAddTodo [types.RESET_TODOS]: onResetTodos }; export default createReducers(INITIAL_STATE, HANDLERS); ``` As you can see, you can use **createReducers** either standalone or with types generated by Arc. It helps you organize your logic and focus on how each action will affect the state; Also, it runs some validations over the config you provided. For example: If you commit a typo when providing the action type, it will throw a friendly descriptive error for you. # Async Actions Originally, Arc was created to be an abstraction layer to handle async request in Redux. So, it has all you need about that. The api to generate async action creators and types is the same we use for regular actions, you only need to provide some additional params in the action config object. Take a look below: ```js import { createActions } from 'redux-arc'; const { creators, types } = createActions('todo', { list: { url: 'api/todo', method: 'get' }, read: { url: 'api/todo/:id', method: 'get' }, create: { url: 'api/todo', method: 'post' }, update: { url: 'api/todo/:id', method: 'put' }, }); ``` Above are defined four actions: `list`, `read`, `create` and `update`. Two params are required in an async action config, `url` and `method`. - **url**: You can define any url you want and it also accepts dynamic params, as you can see in the `read` action. We defined a dynamic param **id**, inserting `:id`. - **method**: Generally speaking, any http method your request lib supports. This will be used only by you in the **asyncMiddleware** that you will configure. ### Async Creators and Async Types An async creator is very similar to a simple creator. It accepts **payload** and **meta** as arguments. They will further become the `action.payload` and `action.meta`, as in a regular creator. The difference here, is that the meta, should be an object, and its values will be also used to parse dynamic urls. Considering that, the creator **read** should be used like that: ```js const payload = null; const meta = { id: '123' }; creators.read(payload, meta); ``` With the above code, the final url to our read request, would be `api/todo/123`. The async types differ a little bit from the regular ones as well. First, as a api call has two moments (request and response), we need two different types to use inside our reducers. Considering that, Arc returns an object for each type, containing a `REQUEST` and a `RESPONSE` key with the respective types: ```js types.READ.REQUEST == 'TODO_READ_REQUEST'; types.READ.RESPONSE == 'TODO_READ_RESPONSE'; ``` ### Async Middleware Arc doesn't intend to be a request lib, so, you need to tell it how you want to make your requests, and you do that by configuring the asyncMiddleware. It's quite simple, take a look below in an example using axios: ```js import { createAsyncMiddleware } from 'redux-arc'; import axios from 'axios'; const asyncTask = store => done => (options) => { const { method, url, payload } = options; const params = method === 'get' ? { params: payload } : payload; return axios[method](url, params).then( response => done(null, response.data), error => done(error, null), ); }; // create the async middleware const asyncMiddleware = createAsyncMiddleware(asyncTask); // set it to the Store const store = createStore( reducer, applyMiddleware(asyncMiddleware), ); ``` >In the above example, we are using axios, but you can use whatever you want to perform the request, just make sure you call done, passing error and response when the request has finished. For more info about **asyncTask** and **createAsyncMiddleware**, read [Connecting Arc Into Redux](http://redux-arc.js.org/docs/basics/ConnectingArcIntoRedux.html) ## Async actions in Reducers In reducers, you have to define two different handlers for each request definition, one to handle the state change when the request starts an another when it finishes. Considering the following config: ```js import { createActions } from 'redux-arc'; export const { creators, types } = createActions('todo', { list: { url: 'api/todo', method: 'get' }, }); ``` We would have a reducers like this: ```js import { createReducers } from 'redux-arc' import { types } from './actions'; const INITIAL_STATE = { listResult: [], listIsLoading: false, listError: null, }; const onListRequest = (state, action) => ({ ...state, listIsLoading: true, listError: INITIAL_STATE.listError, }); const onListResponse = (state, action) => { if (action.error) { return { ...state, listIsLoading: INITIAL_STATE.listIsLoading, listError: action.payload, } } return { ...state, listIsLoading: INITIAL_STATE.listIsLoading, listResult: action.payload } }; const onReset = state => INITIAL_STATE; const HANDLERS = { [types.LIST.REQUEST]: onListRequest, [types.LIST.RESPONSE]: onListResponse, [types.RESET]: onReset, }; export default createReducers(INITIAL_STATE, HANDLERS); ``` ### Response action: When the request is done, an action with the response will be dispatched. Considering the list example, the response action would look like this: ```js { type: 'JEDI_LIST_RESPONSE', // types.LIST.RESPONSE, meta: { url: 'api/todo', method: 'get', }, payload: [ // resource list ], } ``` ### Error handling The above example is a response with success. Accordingly to **FSA**, errors should be treated as a [**First class concept**](https://github.com/redux-utilities/flux-standard-action#errors-as-a-first-class-concept). In that case, when you got some error in an async request, the response action will come with the error property as `true` and the payload will be the actual error. Just like the example below: ```js { type: 'JEDI_LIST_RESPONSE', // types.LIST.RESPONSE, meta: { url: 'api/todo', method: 'get', }, payload: new Error('the request error'), error: true, } ``` ================================================ FILE: book.json ================================================ { "gitbook": "3.2.2", "title": "Redux Arc", "plugins": ["edit-link", "prism", "-highlight", "github", "anchorjs"], "pluginsConfig": { "edit-link": { "base": "https://github.com/viniciusdacal/redux-arc/tree/master", "label": "Redux Arc" }, "github": { "url": "https://github.com/viniciusdacal/redux-arc/" }, "theme-default": { "styles": { "website": "build/gitbook.css" } } } } ================================================ FILE: build/gitbook.css ================================================ .book-summary ul.summary li span { cursor: not-allowed; opacity: 0.3; } .book-summary ul.summary li a:hover { color: #008cff; text-decoration: none; } ================================================ FILE: docs/README.md ================================================ Table of Contents * [Read Me](/README.md) * [Basics](/docs/basics/README.md) * [Action Creators](/docs/basics/ActionCreators.md) * [Action Types](/docs/basics/ActionTypes.md) * [Creating Async Middleware](/docs/basics/CreatingAsyncMiddleware.md) * [Requests Definition](/docs/basics/RequestsDefinition.md) * [Advanced](/docs/advanced/README.md) * [Request Middlewares](/docs/advanced/RequestMiddlewares.md) ================================================ FILE: docs/advanced/README.md ================================================ ## Advanced * [Request Middlewares](RequestMiddlewares.md) ================================================ FILE: docs/advanced/RequestMiddlewares.md ================================================ # Request Middlewares Request middlewares, as the name says, are middlewares that you can apply to specific requests. Lets say you want to format your payload before the request, but only in a few cases. Or you would like to process some response, before it goes to the reducers. For all these edge cases, you can use a request middleware. # Saving a session token into localStorage Let's imagine you have a login request, that returns a session token in case of success. On the response, you need to get that token and save it to the browser's localStorage. That could be fairly easy to handle with a middleware. Let's take a look into the whole code for that and then go through it, step by step. Below you can see the middleware's code: ```js // saveUserSession.js function saveUserSession() { return done => (action, error, response) => { if (response && response.token) { localStorage.setItem('$token', response.token); } return done(action, error, response); }; } saveUserSession.applyPoint = 'onResponse'; export default saveUserSession; ``` As we can observe in the above code, request middlewares are very similar to redux's middlewares. The big difference is that redux middlewares watches all application actions, while request middlewares you apply as you need, for specific requests. A middleware should also contain a property `applyPoint`, which should be either `'onRequest'` or `'onResponse'`. The applyPoint tells in which moment of the request the middleware will be executed. Considering we need the token that comes in the response, we use the applyPoint `'onResponse'`. Going to the code inside the middleware, we call the function **done**, passing the same params we received. We do that because we don't want to intercept the action or modify the response, we only intend to use the response value. But we still do a **return** of the result of **done**, to not break the promise chain. After that, we check if the response is valid and then we save the token in the localStorage. ## Applying middlewares to requests To use a middleware, is fairly simple, you only need to *import* and include it in an array under a property **middlewares** in the action config. Take a look at the following example: ```js import { createActions } from 'redux-arc'; import saveUserSession from './saveUserSession'; const { types, creators } = createActions('user', { login: { url: 'user/login', method: 'post', middlewares: [saveUserSession], }, }); dispatch(creators.login({ email: 'user@test.com', password: '123', })); ``` We are defining a login request, and we are applying the middleware we created to it. Then, we use the creator to dispatch the action that will start the request, passing the email and password in the payload. providing the middleware in the config, makes the middleware run every time you call that request. But let's say you would like to apply the middleware in a single call to that request, then you could do the following: ```js import saveUserSession from './saveUserSession'; const { types, creators } = createActions('user', { login: { url: 'user/login', method: 'post', }, }); dispatch(creators.login({ email: 'user@test.com', password: '123', middlewares: [saveUserSession], })); ``` In the code above, instead of apply the middleware to all login requests, we apply to the single call we are doing. ## onRequest middlewares In the previous example we used the applyPoint `'onResponse'`, because we wanted to access the response value. In the following example, we are going to use the applyPoint `'onRequest'`, because we will process and change the payload before it goes to the request. ## Saving an user So, let's imagine you would like to create and update a user, but you wouldn't like to configure two different requests to do that. You would rather to config a single request named **save** and when you call it passing a user without an id, it would understand that you were intending to do a creation request, otherwise, it would assume you were trying to update the user, and would do the necessary changes on the request data to ensure the update would work. You could create a middleware to handle that scenario, and name it **createOrUpdate**. Let's do it. ```js function createOrUpdate() { return done => (action, error, response) => { const { payload, meta } = action; if (!payload.id) { return done(action, error, response); } const { id, ...user } = payload; const updateAction = { ...action, payload: user, meta: { url: `${meta.url}/${id}`, method: 'put', id }, }; return done(updateAction, error, response); } } createOrUpdate.applyPoint = 'onRequest'; export default createOrUpdate; ``` > In the cases your middleware has an applyPoint `onRequest`, you would have access only to the `action` object, unless another middleware created an `error` or `response` in the `onRequest` chain. The first thing we do in the middleware, is check for the `payload.id`. If none is present, we assume this request intends to create an user and we assume the original request config is prepared to do that, having the method as `post`, and having the proper url for the creation. That said, we only need to let the request happens normally, calling the function **done**, passing the params we received. If there's an id present, we create another action, modifying the **url** to include the **id** and also changing the method, to `put`. Once we do that, we only need to call **done**, providing modified action. And that's it, our **createOrUpdate** middleware is done. Now, let's take a look on how to use it. ```js import { createActions } from 'redux-arc'; import createOrUpdate from './createOrUpdate'; const { types, creators } = createActions('user', { save: { url: 'user', method: 'post', middlewares: [createOrUpdate] }, }); dispatch(creators.save({ name: 'My New user', email: 'user@redux-arc.js.org', })); dispatch(creators.save({ id: '123' name: 'My Edited user', email: 'user@redux-arc.js.org', })); ``` First, we define the request as it would be for the creation, passing the url as `user` and the method as `post`. Then, we add the middleware to it. In the first dispatching, the payload does not contain the id, so it will create an user. The second dispatch contains the id and will be an update. > Usually, for `onRequest` you would change only the action value, and for `onResponse`, you would change only the response. But feel free to change the action inside `onResponse` cycle if that makes sense. Remember that when you create a middleware, you can use it in different request. This last middleware for example, you could use to create and save todo items, or into any crud your application has. Request middlewares can be used in many different ways, adding flexibility to the requests. ================================================ FILE: docs/basics/ActionCreators.md ================================================ # Action Creators An action creator, is a function that returns an action. Simple action creators should look just like this: ```js const add = payload => ({ type: 'TODO_ADD', payload, }); const reset = () => ({ type: 'TODO_RESET', }); ``` The problem is, when you are in a real project, you will have far more actions than only two, and you always have to define these creators. 99% of times, they are just like the code above. You always have to define a name for your **creator**, then you have to define a **type** that has (or at least should) the same name as the **creator**, but uppercased. Then you receive some arguments in the function, inject them in the action and return the action. This process is too repetitive. To solve that issue, Arc has the `createActions` method, which generates types and action creator for you, based on a config. Take a look at the example below: ```js import { createActions } from 'redux-arc'; const { creators } = createActions('todo', { add: null, reset: null, }) ``` The above config would generate a **creators**, similar to the object below: ```js { add: function (payload, meta, error) {}, reset: function (payload, meta, error) {}, } ``` Those action creators can be used the same way as regular action creators are. So, having access to `dispatch` function, you can just do the following: ```js const payload = { // any payload }; const meta { // any meta }; dispatch(creators.add(payload, meta)); /* { type: 'TODO_ADD', payload: {}, meta: {}, } */ ``` The arguments `payload` and `meta` are not required. To dispatch a *reset action*, for example, as we are not going to use either **payload** neither **meta** inside our reducers, we can call the creator straight away, without passing any argument: ```js dispatch(creators.reset()); /* { type: 'TODO_RESET', } */ ``` ## Providing default payload and meta for the action You may be asking yourself why we always have to provide a `null` in the action config. One of the reasons, is because we accept some extra options to generate the actions. You could provide a default `payload`, a default `meta` or a default `error`, for example: ```js const anyDefaultPayload = {}; const anyDefaultMeta = {}; const { creators } = createActions('todo', { add: { payload: anyDefaultPayload, meta: anyDefaultMeta, error: false, // always a boolean; }, reset: null, }); ``` Besides a default values, Arc also accepts a param `url`, which will create a different kind of action, a **async action**. # Async Action By Arc's point of view, a async action that contains the necessary information to trigger a async request. It has some additional info, as `url` and `method` and it also has two types instead of just one. Through the `createActions` factory, Arc allows you to create **async creators** and **async types** too, in order to make your life easier when you need to deal with this subject. The way you generate them is very similar to regular actions. Take a look below: ```js const { creators } = createActions('todo', { list: { url: 'api/todo', method: 'get' }, update: { url: 'api/todo/:id', method: 'put' }, reset: null, }) ``` As you can see, the api is nearly the same, you just need to provide the params `url` and `method` to make it work. - **url**: an endpoint to make the requests. It also accepts dynamic params, as you can see in the config to **update** request. We defined a `:id` which will be parsed when we call the creator providing the actual param. - **method**: Any http method your request lib supports. ('get', 'post', etc...) - **middlewares**: Arc also accepts middlewares in the action config, to give you flexibility to handle edge cases. You can check them at: [Request Middlewares](http://redux-arc.js.org/docs/advanced/RequestMiddlewares.html) Given the above config, to start a list request, you could just do the following: ```js dispatch(creators.list()); ``` First, `creators.list` will return the following action: ```js { type: ['MY_NAMESPACE_LIST_REQUEST', 'MY_NAMESPACE_LIST_RESPONSE'], meta: { url: 'api/todo', method: 'get', }, } ``` And then, the function dispatch, will dispatch it for redux. The way we use `creators.update` is very similar, the only two differences are: - We need to provide a **payload**, that will be the request's payload itself. - We need to provide a param **id**, that will be used to parse the dynamic url. Take a look in the example below: ```js const payload = { description: 'Implement Arc in the project', date: '2017-10-03', }; const meta = { id: '123' }; dispatch(creators.update(payload, meta)); ``` The above creator, generates the following action: ```js { type: ['MY_NAMESPACE_UPDATE_REQUEST', 'MY_NAMESPACE_UPDATE_RESPONSE'], payload: { description: 'Implement Arc in the project', date: '2017-10-03', }, meta: { url: 'todo/123', method: 'put', id: '123', }, } ``` > When the above action is dispatched, Arc's middleware intercepts it, parses the values and passes the options to **asyncTask** perform the request. Notice that, those actions generated by Arc's creators, have an array containing two strings instead of being only a simple string. That's how Arc's middleware identifies them as Async Actions. Also, the first type is used in the action dispatched right before the request starts and the second is used to dispatch an action right after we get a response. ## Providing url as function Arc also supports url as a function that expects params as its first argument and returns a string, as the following example: ```js const { creators } = createActions('todo', { read: { url: params => `todo/${params.id}`, method: 'get', } }) ``` ================================================ FILE: docs/basics/ActionTypes.md ================================================ # Action Types Beyond creators, you also receive an object named **types** when you call `createActions`. This object contains all the respective types for your actions. Given the following config: ```js const { types } = createActions('myNamespace', { list: { url: 'todo', method: 'get' }, update: { url: 'todo/:id', method: 'put' }, reset: null, }); ``` Your action types would look exactly like this: ```js { LIST: { REQUEST: 'MY_NAMESPACE_LIST_REQUEST', RESPONSE: 'MY_NAMESPACE_LIST_RESPONSE', }, UPDATE: { REQUEST: 'MY_NAMESPACE_UPDATE_REQUEST', RESPONSE: 'MY_NAMESPACE_UPDATE_RESPONSE', }, RESET: 'MY_NAMESPACE_RESET', }; ``` Considering the code above, to get the type for your list response action, you could do the following: ```js types.LIST.RESPONSE // MY_NAMESPACE_LIST_RESPONSE ``` **The logic we use to define the types, is basically:** Convert the action name to Upper Case, splitting words by underscore `_` and use it as the root keys of our action types object: ```js // list -> LIST { LIST, UPDATE, RESET, } ``` For async types, we add the keys `REQUEST` and `RESPONSE` to all types objects: ```js // list -> { LIST: { REQUEST, RESPONSE, }, UPDATE: { REQUEST, RESPONSE, }, RESET, } ``` The final value for the action type, begins with the namespace uppercased, followed by the action name uppercased. For **Async Actions**, we append respectively, the words `REQUEST` and `RESPONSE`). ``` myNamespace -> MY_NAMESPACE list -> LIST reset -> RESET MY_NAMESPACE_LIST_REQUEST MY_NAMESPACE_LIST_RESPONSE MY_NAMESPACE_RESET ``` ================================================ FILE: docs/basics/CreatingAsyncMiddleware.md ================================================ # Creating Async Middleware The first step to start using Arc's async actions, is to create the AsyncMiddleware and set it to the store. ```js import { createStore, applyMiddleware } from 'redux'; import { createAsyncMiddleware } from 'redux-arc'; const asyncTask = store => done => (options) => { const { url, method, payload, ...meta } = options; /* do your request and call done with the respective error and response when the request is finished */ done(err, response)) }; const asyncMiddleware = createAsyncMiddleware(asyncTask); // set it to the Store const store = createStore( reducer, applyMiddleware(asyncMiddleware), ); ``` ### asyncTask Notice we defined a function named `asyncTask` on the first lines of the above example, that's because we leave the request call for you. Everytime you dispatch an async action, the middleware will call `asyncTask` passing the respective params. `asyncTask` should do the request and should call `done` when the request is finished, providing an error and a response. What you are going to use to perform your requests (fetch, promises, libs etc..), is up to you! Only make sure you call `done` in the proper time. Pay attention to the `options` argument. The `asyncTask` should rely on this to perform the request. The `options` will be an object that contains `url`, `payload` and `method`. If we remove all the promise related code, the `asyncTask` would look like this: ```js const asyncTask = store => done => options => { const { url, method, payload, ...meta } = options; done(error, response); }; ``` **Parameter** The parameter `store`, is the actual redux store, with the methods `getState` and `dispatch`, use them with parsimony. The parameter `done` is a function and should be called once the request is finished, with the **error** and **response**. The parameter `options` will contain some fields extracted from the async action, which are the `url`, `method` the `payload` you provided when you call the action creator and all `action.meta` fields. ### createAsyncMiddleware This function waits as its first argument, the function `asyncTask`, and will return the actual middleware for redux. Then, you get the middleware and apply it to the store. Rather to set `asyncMiddleware` as the first middleware in the config, it will avoid traffic unnecessary actions through the whole redux flow. And that's it, your middleware is configured in the store. Let's jump strait forward to the request definitions. ================================================ FILE: docs/basics/README.md ================================================ ## Basics * [Action Creators](ActionCreators.md) * [Action Types](ActionTypes.md) * [Requests Definition](RequestsDefinition.md) * [Creating Async Middleware](CreatingAsyncMiddleware.md) * [Request and Response values](RequestAndResponseValues.md) ================================================ FILE: docs/basics/RequestAndResponseValues.md ================================================ # Request and response values When you start a request, dispatching an async action, There are two different actions that are dispatched by Arc. One action when the request starts, and another when it ends. Let's observe the following example: ```js const { creators, types } = createActions('todo', { update: { url: 'todo/:id', method: 'put' } }) ``` We can start a request, by dispatching the async action: ```js dispatch(creators.update({ id: '123', payload: { description: 'Implement Arc in the project', date: '2017-10-03', } })); ``` ## Request action Then, Arc will dispatch an action notifying that the request has been started. The action would be like the following: ```js { type: 'TODO_UPDATE_REQUEST', meta: { id: '123', url: 'todo/123', method: 'put', }, payload: { description: 'Implement Arc in the project', date: '2017-10-03', }, } ``` Using the **types** object, you can have access to that action type, using the below code: ```js types.UPDATE.REQUEST ``` In that case, you could start creating your reducer such as below: ```js const INITIAL_STATE = { updateInProcess: false, updateResult: null, updateError: null, }; function toDoReducer(state = INITIAL_STATE, action) { if (action.type === types.UPDATE.REQUEST) { return { ...state, updateError: INITIAL_STATE.updateError, updateInProcess: true, } } return state; } ``` In the example above, every time a update request starts, we reset the value **updateError** for its initial state, which is `null`, we also set **updateInProcess** as true. If we need, we could also use the **id**, **url** or **method** in our reducers, accessing it from **action.meta** ## Response Action When the request ends, Arc will dispatch the response action, which would look like the following example: ```js { type: 'TODO_UPDATE_RESPONSE', meta: { id: '123', url: 'todo/123', method: 'put', }, payload: THE PROPER RESPONSE FROM YOUR ENDPOINT, } ``` The above example is considering a successful request. In case of error, the action would come with a key error and the value for it would be true. The payload would be the error itself. The error action would be like this: ```js { type: 'TODO_UPDATE_RESPONSE', meta: { id: '123', url: 'todo/123', method: 'put', }, error: true, payload: THE PROPER ERROR, } ``` Let's update our reducer based on the above examples: ```js const INITIAL_STATE = { updateInProcess: false, updateResult: null, updateError: null, }; function toDoReducer(state = INITIAL_STATE, action) { if (action.type === types.UPDATE.REQUEST) { return { ...state, updateError: INITIAL_STATE.updateError, updateInProcess: true, } } if (action.type === types.UPDATE.RESPONSE) { if (action.error) { return { ...state, updateInProcess: INITIAL_STATE.updateInProcess, updateError: action.payload, }; } return { ...state, updateInProcess: INITIAL_STATE.updateInProcess, updateResult: action.payload, }; } return state; } ``` In the example above, first we check if the **action.error** is **true**, and if it's, we set the **action.payload** to **updateError** and we reset **updateInProcess** to its initial state. If no error is present, we set the action.payload to updateResult but we also reset **updateInProcess**. ================================================ FILE: docs/basics/RequestsDefinition.md ================================================ # Async actions Definition Arc provides a declarative interface to define async actions, that allows you to do this in a few lines of code. If you are used with any route system, you will feel like home, if you don't, it will take minutes for you to understand how it works. First of all, let's see how would look a todo list crud definition, considering a rest api: Let's consider our base url as `/api`: ```js import { createActions } from 'redux-arc'; const { types, actions } = createActions('todo', { create: { url: '/api/todo', method: 'post' }, read: { url: '/api/todo/:id', method: 'get' }, update: { url: '/api/todo/:id', method: 'put' }, list: { url: '/api/todo', method: 'get' }, list: { url: '/api/todo', method: 'get' }, }); const payload = null; const meta = { id: '123' }; actions.read(payload, meta); //dispatch read action types.READ.REQUEST // TODO_READ_REQUEST types.READ.RESPONSE // TODO_READ_RESPONSE ``` > In the above example, we have our baseUrl as `/api`. If you are using a lib like axios, you can set the **baseUrl** in its config, so, the url definition of `read`, for example, could look like this: `todo/:id` instead of this:`/api/todo/:id` Let's step back and explore each part of this request definition. #createActions This function is only a factory that returns the action creators and the action types. ```js import { createActions } from 'redux-arc'; const { types, actions } = createActions('myNamespace', { list: { url: 'path/to/list', method: 'get'}, }); ``` `createActions` expects a namespace as its first argument, that will be used to prefix the action types. The second argument is the requests definition object, which should respect the following pattern: ```js { list: { url: 'todo', method: 'get', }, update: { url: 'todo/:id', method: 'put', } } ``` As you can see, it's possible to define multiple requests in the same config object, you only need to give it an action name. Considering the above config: - **action name** - It's the name you give for each async action. It will be used to generate the action creator and also the action type. In the above example, we have `list` and `update`. - **url** - will be passed to `asyncTask` to perform the request. It also accepts dynamic urls, so, you can define an url such as `path/to/resource/:id`. - **method** - It will be passed to `asyncTask` as well, in this case, you can use any method your request lib supports, the most common are `post`,`get`, `put` and `delete`. ================================================ FILE: package.json ================================================ { "name": "redux-arc", "version": "0.7.5", "description": "Abstraction layer to help you reduce boilerplate on redux-apps", "main": "lib/index.js", "module": "es/index.js", "files": [ "dist", "lib", "src" ], "keywords": [ "redux", "redux-arc", "react", "async" ], "scripts": { "clean": "rimraf lib dist es coverage", "lint": "eslint src test build", "test": "cross-env BABEL_ENV=commonjs jest", "test:watch": "yarn test -- --watch", "test:cov": "yarn test -- --coverage", "coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls", "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", "build:umd": "cross-env BABEL_ENV=es NODE_ENV=development rollup -c -i src/index.js -o dist/redux.js", "build:umd:min": "cross-env BABEL_ENV=es NODE_ENV=production rollup -c -i src/index.js -o dist/redux.min.js", "build": "yarn run build:commonjs && yarn run build:es && yarn run build:umd && yarn run build:umd:min", "prepare": "yarn run clean && yarn run lint && yarn test && yarn run build", "examples:lint": "eslint examples", "examples:test": "cross-env CI=true babel-node examples/testAll.js", "docs:clean": "rimraf _book", "docs:prepare": "gitbook install", "docs:build": "yarn run docs:prepare && gitbook build -g viniciusdacal/redux-arc && cp logo/apple-touch-icon.png _book/gitbook/images/apple-touch-icon-precomposed-152.png && cp logo/favicon.ico _book/gitbook/images", "docs:watch": "yarn run docs:prepare && gitbook serve", "docs:publish": "yarn run docs:clean && yarn run docs:build && cp CNAME _book && cd _book && git init && git commit --allow-empty -m 'update book' && git checkout -b gh-pages && touch .nojekyll && git add . && git commit -am 'update book' && git push git@github.com:viniciusdacal/redux-arc gh-pages --force" }, "devDependencies": { "babel-cli": "6.26.0", "babel-core": "6.26.0", "babel-eslint": "8.2.2", "babel-jest": "22.4.1", "babel-plugin-external-helpers": "6.22.0", "babel-plugin-transform-object-rest-spread": "6.26.0", "babel-preset-env": "1.6.1", "babel-register": "6.26.0", "cross-env": "5.1.0", "eslint": "4.9.0", "eslint-config-react-app": "2.0.1", "eslint-plugin-flowtype": "2.39.1", "eslint-plugin-import": "2.2.0", "eslint-plugin-jsx-a11y": "5.1.1", "eslint-plugin-react": "7.4.0", "gitbook-cli": "2.3.2", "glob": "7.1.1", "jest": "22.4.2", "prettier": "1.8.2", "redux": "3.7.1", "rimraf": "2.6.2", "rollup": "0.51.8", "rollup-plugin-babel": "3.0.2", "rollup-plugin-node-resolve": "3.0.0", "rollup-plugin-replace": "2.0.0", "rollup-plugin-uglify": "2.0.1" }, "authors": [ "Vinicius Dacal (https://github.com/viniciusdacal)" ], "license": "MIT", "jest": { "coverageDirectory": "./coverage", "testRegex": "(/test/.*\\.spec.js)$", "globals": { "BABEL_ENV": "commonjs" }, "modulePathIgnorePatterns": [ "/_book/" ] }, "dependencies": {}, "peerDependencies": { "redux": ">= 3.7.1" } } ================================================ FILE: redux-arc.sublime-project ================================================ { "folders": [ { "path": "." } ], "build_systems": [ { "name": "Jest", "shell_cmd": "jest --colors $file", "selector": "source.js", "file_regex": ".*.(spec|stories).js(?x)", "target": "ansi_color_build", "syntax": "Packages/ANSIescape/ANSI.tmLanguage" } ] } ================================================ FILE: redux-arc.sublime-workspace ================================================ { "auto_complete": { "selected_items": [ [ "midd", "middleware" ], [ "appl", "applyPoint" ], [ "sele", "selected" ], [ "new", "newState" ], [ "selec", "selectedIsLoading" ], [ "en", "ensureValueKey" ], [ "medi", "mediaFiles" ], [ "total", "totalValues" ], [ "ang", "angulo_hora" ], [ "hora", "hora_base_12" ], [ "filt", "filters" ], [ "input", "inputText" ], [ "cach", "cachedBrands" ], [ "rena", "renameKey" ], [ "meta", "metaCached" ], [ "arra", "arrayOf" ], [ "update", "updateStoreValue" ], [ "clien", "clientId" ], [ "fiel", "fieldId" ], [ "categ", "categoryIds" ], [ "cage", "categoryIds" ], [ "In", "instance" ], [ "list", "listResult" ], [ "job", "jobTypes" ], [ "inf", "influencerClass" ], [ "in", "influencerClass" ], [ "chan", "channelsCPM" ], [ "place", "placementId" ], [ "set", "setInfluencerOverride" ], [ "INfluen", "influencerClasses" ], [ "CPm", "CPMs" ], [ "influ", "influencerOverride" ], [ "infl", "influencer" ], [ "campa", "campaignId" ], [ "media", "mediaPlanId" ], [ "opp", "oppId" ], [ "oppo", "opportunityId" ], [ "ac", "action" ], [ "READ", "READ_INITIAL_STATE" ], [ "peding", "pending" ], [ "branc", "branchKey" ], [ "val", "valueKey" ], [ "disab", "disablePreview" ], [ "default", "defaultProps" ], [ "proP", "propTypes" ], [ "sto", "stories" ], [ "boo", "boolean" ], [ "ge", "generateProps" ], [ "has", "hasId" ], [ "UITa", "UITableHCol" ], [ "read", "readResult" ], [ "assin", "assignedToId" ], [ "date", "dateRange" ], [ "onChang", "onChangeCallback" ], [ "defaul", "defaultProps" ], [ "Bool", "BooleanDisplay" ], [ "fetc", "fetchStudios" ], [ "select", "selectorId" ], [ "pased", "parsedNextUrlParams" ], [ "is", "isequal" ], [ "defau", "defaultValues" ], [ "pagina", "paginationKey" ], [ "defa", "defaultValues" ], [ "sor", "sort" ], [ "par", "parsePageToSkip" ], [ "excel", "excelExport" ], [ "pagin", "paginationKey" ], [ "page", "parsePageToSkip" ], [ "previ", "previousValues" ], [ "com", "component" ], [ "MEdia", "MediaValueCalculator" ], [ "onLI", "onListResponse" ], [ "crea", "creators" ], [ "value", "valueKey" ], [ "valu", "valueKey" ], [ "change", "changeSkip" ], [ "create", "createActions" ], [ "creat", "createActions" ], [ "add", "addCustomSearch" ], [ "ADD", "ADD_CUSTOM_SEARCH" ], [ "hand", "handlers" ], [ "handl", "handlerKey" ], [ "type", "typeof" ], [ "param", "paramType" ], [ "INT", "INVALID_ACTION" ], [ "fina", "finalMeta" ], [ "conf", "configPayload" ], [ "actio", "action" ], [ "RESET", "RESET_WITH_META" ], [ "pu", "publicNames" ], [ "Val", "validateConfig" ], [ "va", "validateConfig" ], [ "action", "actionCreatorFactory" ], [ "parse", "parseUrl" ], [ "_", "_REQUEST" ], [ "pars", "parseOptions" ], [ "BASE", "BASE_TYPES" ], [ "names", "NAMESPACE" ], [ "chil", "child" ], [ "sort", "sortOrder" ], [ "spli", "splitSort" ], [ "spl", "splitSort" ], [ "prop", "propTypes" ], [ "clas", "classnames" ], [ "defua", "defaultReturn" ], [ "user", "userId" ], [ "ad", "additionalContactIds" ], [ "id", "idToString" ], [ "find", "findOne" ], [ "capta", "capitalize" ], [ "cap", "capitalize" ], [ "rol", "roleOptions" ], [ "so", "sortBy" ], [ "UITab", "UITableFilters" ], [ "url", "urlParams" ], [ "onChange", "onChangePaginator" ], [ "User", "UserList" ], [ "las", "lastAccess" ], [ "last", "lastName" ], [ "UITable", "UITableHCol" ], [ "UItable", "UITableHeader" ], [ "paed", "parsedValue" ], [ "vel", "velcroHandlers" ], [ "sche", "schemaSelector" ], [ "get", "getUrlParams" ], [ "res", "restFilters" ], [ "se", "schemaSelector" ], [ "debo", "debounce" ] ] }, "buffers": [ ], "build_system": "", "build_system_choices": [ [ [ [ "Jest", "" ], [ "Packages/ESLint/ESLint.sublime-build", "" ] ], [ "Jest", "" ] ], [ [ [ "Packages/C++/C++ Single File.sublime-build", "" ], [ "Packages/C++/C++ Single File.sublime-build", "Run" ] ], [ "Packages/C++/C++ Single File.sublime-build", "" ] ] ], "build_varint": "", "command_palette": { "height": 392.0, "last_filter": "close", "selected_items": [ [ "close", "File: Close All" ], [ "babel", "Set Syntax: JavaScript (Babel)" ], [ "bl", "Git Blame" ], [ "cop", "File: Copy Path From Project" ], [ "Gutter", "Preferences: GitGutter Settings" ], [ "instal", "Package Control: Install Package" ], [ "du", "File: Duplicate" ], [ "rub", "Set Syntax: Ruby" ], [ "rena", "File: Rename" ], [ "esli", "ESLint" ], [ "java", "Set Syntax: Java" ], [ "low", "Convert Case: Lower Case" ], [ "eslin", "ESLint" ], [ "eslint", "ESLint" ], [ "b", "Git Blame" ], [ "lowe", "Convert Case: Lower Case" ], [ "install", "Package Control: Install Package" ], [ "cloe", "Convert Case: Lower Case" ], [ "up", "Convert Case: Upper Case" ], [ "upp", "Convert Case: Upper Case" ], [ "dif", "Diffy Compare" ], [ "insta", "Package Control: Install Package" ], [ "sor", "Sort Lines" ], [ "m", "Set Syntax: Markdown" ], [ "move", "File: Move" ], [ "mo", "File: Move" ], [ "dele", "File: Delete" ], [ "jes", "Build With: Jest" ], [ "remove", "Package Control: Remove Package" ], [ "build", "Build With: Jest" ], [ "isnta", "Package Control: Install Package" ], [ "c++", "Build With: C++ Single File" ], [ "run", "Build With: C++ Single File - Run" ], [ "termina", "Terminal View: Open Bash Terminal" ], [ "increment", "Emmet: Increment Number by 1" ], [ "case", "Convert Case: Swap Case" ], [ "reve", "File: Reveal" ], [ "new", "File: New File Relative to Current View" ], [ "copy", "File: Copy Path" ], [ "dupl", "File: Duplicate" ], [ "html", "HTML: Encode Special Characters" ], [ "new file", "File: New File Relative to Current View" ], [ "jsx", "Pretty JSON: JSON 2 XML" ], [ "bla", "Git Blame" ], [ "json", "Pretty JSON: Format (Pretty Print) JSON" ], [ "jspon", "Pretty JSON: Format (Pretty Print) JSON" ], [ "mv", "File: Move" ], [ "cp", "File: Copy Path" ], [ "sort", "Pretty JSON: Format and Sort JSON" ], [ "dupli", "File: Duplicate" ], [ "dus", "Plugin Development: Convert Syntax to .sublime-syntax" ], [ "ren", "File: Rename" ], [ "save", "File: Save All" ], [ "clos", "File: Close All" ], [ "mark", "Set Syntax: Markdown" ], [ "babe", "Set Syntax: JavaScript (Babel)" ], [ "de", "File: Delete" ], [ "sa", "File: Save All" ], [ "renam", "File: Rename" ], [ "BL", "Git Blame" ], [ "cl", "File: Close All" ], [ "bab", "Set Syntax: JavaScript (Babel)" ], [ "diff", "Set Syntax: Diff" ], [ "theme", "UI: Select Theme" ], [ "disa", "SublimeLinter: Disable Linter" ], [ "bba", "Set Syntax: JavaScript (Babel)" ], [ "babl", "Babel Transform" ], [ "valida", "Pretty JSON: Validate" ], [ "j", "Pretty JSON: JSON query with ./jq" ], [ "jq", "Pretty JSON: JSON query with ./jq" ], [ "es", "Set Syntax: Perl" ], [ "", "ESLint" ], [ "mar", "Set Syntax: Markdown" ], [ "clo", "File: Close All" ], [ "blam", "Git Blame" ], [ "rnea", "File: Rename" ], [ "esl", "ESLint" ], [ "re", "File: Rename" ], [ "upper", "Convert Case: Upper Case" ], [ "prettier", "JsPrettier: Format JavaScript" ], [ "remove p", "Package Control: Remove Package" ], [ "dee", "File: Delete" ], [ "mvoe", "File: Move" ], [ "co", "File: Copy Path" ], [ "del", "File: Delete" ], [ "rea", "File: Rename" ], [ "duplic", "File: Duplicate" ], [ "import", "ImportJS: goto module" ], [ "title", "Convert Case: Title Case" ], [ "Convert", "Convert Case: Title Case" ], [ "ja", "Pretty JSON: Format and Sort JSON" ], [ "pretti", "JsPrettier: Format JavaScript" ], [ "jsn", "Pretty JSON: Format and Sort JSON" ], [ "copo", "File: Copy Path From Project" ], [ "path", "File: Copy Path" ], [ "less", "Set Syntax: LESS" ], [ "update", "Package Control: Upgrade/Overwrite All Packages" ], [ "Snippet", "Snippet: Function" ], [ "react cm", "Snippet: React: wrap in a component" ], [ "lorem", "LoremIpsum: (15) some" ], [ "inst", "Package Control: Install Package" ], [ "post", "Set Syntax: PostCSS" ] ], "width": 436.0 }, "console": { "height": 290.0, "history": [ "cd ~/ben" ] }, "distraction_free": { "menu_visible": true, "show_minimap": false, "show_open_files": false, "show_tabs": false, "side_bar_visible": false, "status_bar_visible": false }, "expanded_folders": [ "/Users/viniciusdacal/github/redux-arc" ], "file_history": [ "/Users/viniciusdacal/github/redux-arc/test/requestMiddlewares.spec.js", "/Users/viniciusdacal/github/redux-arc/test/middlewareWithRedux.spec.js", "/Users/viniciusdacal/github/redux-arc/redux-arc.sublime-project", "/Users/viniciusdacal/github/redux-arc/docs/advanced/RequestMiddlewares.md", "/Users/viniciusdacal/github/redux-arc/src/requestMiddlewares.js", "/Users/viniciusdacal/github/redux-arc/README.md", "/Users/viniciusdacal/ben/app/web/src/components/Tools/MediaValueCalculator/AdditionalFields/AdditionalFields.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Tools/MediaValueCalculator/MediaValueCalculator.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Tools/MediaValueCalculator/connector.js", "/Users/viniciusdacal/ben/app/web/src/components/Placement/reducers.js", "/Users/viniciusdacal/ben/app/web/src/components/Form/Input/Input.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Form/Input/Number/Number.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Form/Input/Currency/Currency.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Tools/MediaValueCalculator/MediaValueCalculator.scss", "/Users/viniciusdacal/ben/app/web/src/components/Tools/MediaValueCalculator/Calculations/Calculations.jsx", "/Users/viniciusdacal/ben/app/web/js/components/content/opportunities/form/form.js", "/Users/viniciusdacal/ben/app/web/js/components/content/opportunities/form/form.jade", "/Users/viniciusdacal/ben/app/web/src/store/api/errorHandlers.js", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/Form/Form.jsx", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/Form/connector.js", "/Users/viniciusdacal/ben/app/web/src/store/middlewares/redirectOnResponse.js", "/Users/viniciusdacal/ben/app/api/packages/media-plans/lib/permissions.js", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/reducers.js", "/Users/viniciusdacal/ben/app/api/packages/users/lib/handlers.js", "/Users/viniciusdacal/ben/app/web/src/components/Opportunity/Filter/Channel/Channel.stories.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Opportunity/Filter/Channel/Channel.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Opportunity/Filter/Channel/utils/helpers.js", "/Users/viniciusdacal/ben/app/web/src/components/Form/Selector/Selector.jsx", "/Users/viniciusdacal/ben/app/web/src/utils/reduxSelector/reduxSelector.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Form/reduxFormField/reduxFormField.jsx", "/Users/viniciusdacal/ben/app/web/src/utils/reduxSelector/actions.js", "/Users/viniciusdacal/ben/app/web/scss/components/_close.scss", "/Users/viniciusdacal/ben/app/web/src/components/Brand/Selector/Selector.jsx", "/Users/viniciusdacal/ben/app/web/src/utils/reduxSelector/reducers.js", "/Users/viniciusdacal/ben/app/web/src/utils/reduxSelector/selectors.js", "/Users/viniciusdacal/ben/app/web/src/utils/reduxSelector/createSelectors.js", "/Users/viniciusdacal/ben/app/web/src/components/Brand/Form/Form.jsx", "/Users/viniciusdacal/ben/app/web/src/utils/function.js", "/Users/viniciusdacal/ben/app/web/js/app/common/directives/react-file-uploader.js", "/Users/viniciusdacal/ben/app/web/src/components/Form/Uploader/connector.js", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/View/Totals/Totals.jsx", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/View/View.jsx", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/View/Summary/Summary.jsx", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/View/connector.js", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/View/Channels/Channels.jsx", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/Card/Card.jsx", "/Users/viniciusdacal/ben/app/web/src/components/UI/ActionIconList/Item/Item.jsx", "/Users/viniciusdacal/ben/app/web/.eslintrc", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/selectors.js", "/Users/viniciusdacal/ben/app/web/src/components/Opportunity/Search/Filters/connector.js", "/Users/viniciusdacal/ben/app/web/src/components/Opportunity/Content/Filters/connector.js", "/Users/viniciusdacal/ben/app/web/src/components/Opportunity/Content/Filters/Filters.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Placement/View/connector.js", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/Report/connector.js", "/Users/viniciusdacal/ben/app/web/e2e-tests/support/api.js", "/Users/viniciusdacal/ben/app/web/js/app/opportunity/directives/inline-view.js", "/Users/viniciusdacal/ben/app/web/src/components/Opportunity/Search/connector.js", "/Users/viniciusdacal/ben/app/web/src/components/Persona/Form/Form.stories.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Form/Uploader/UploadedFile/UploadedFile.stories.jsx", "/Users/viniciusdacal/ben/app/web/src/components/User/Form/AccountInfo/AccountInfo.stories.jsx", "/Users/viniciusdacal/ben/app/web/src/__mocks__/redux-form.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Form/Select/ButtonGroup/ButtonGroup.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Form/Selector/Selector.stories.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Opportunity/ChannelsAlertsAndRatings/ChannelsAlertsAndRatings.stories.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Opportunity/ChannelsAlertsAndRatings/ChannelsAlertsAndRatings.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Opportunity/Filter/Ratings/Ratings.jsx", "/Users/viniciusdacal/ben/app/web/src/components/User/Form/AccountInfo/AccountInfo.jsx", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/FilteredList/Filters/Filters.stories.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Form/Search/Search.jsx", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/FilteredList/Filters/Filters.jsx", "/Users/viniciusdacal/ben/app/web/.storybook/config.js", "/Users/viniciusdacal/ben/app/web/src/components/Form/Selector/Filters/Filters.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Form/Selector/Filters/Filters.stories.jsx", "/Users/viniciusdacal/ben/app/web/.storybook/webpack.config.js", "/Users/viniciusdacal/ben/app/web/config/jest/setup.js", "/Users/viniciusdacal/ben/app/package.json", "/Users/viniciusdacal/ben/app/web/package.json", "/Users/viniciusdacal/ben/app/web/src/components/Form/CountrySelector/CountrySelector.stories.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Form/Uploader/Dropzone/Dropzone.stories.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Form/Uploader/UploadedFile/UploadedFile.spec.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Opportunity/StatusAndCoordinators/StatusAndCoordinators.stories.jsx", "/Users/viniciusdacal/ben/app/vagrant/config.rb", "/Users/viniciusdacal/ben/app/web/src/components/QueuedJobs/reducers.spec.js", "/Users/viniciusdacal/ben/app/web/src/components/QueuedJobs/List/List.stories.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Form/ButtonGroup/ButtonGroup.jsx", "/Users/viniciusdacal/ben/app/web/src/components/QueuedJobs/utils/mockedData.js", "/Users/viniciusdacal/ben/app/web/src/components/Form/DatePicker/DatePicker.stories.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Form/DatePicker/DatePicker.jsx", "/Users/viniciusdacal/ben/app/web/src/components/QueuedJobs/reducers.js", "/Users/viniciusdacal/ben/app/vagrant/config.rb.dist", "/Users/viniciusdacal/ben/app/vagrant/README.md", "/Users/viniciusdacal/ben/app/web/.babelrc", "/Users/viniciusdacal/Library/Application Support/Sublime Text 3/Packages/User/ESLint.sublime-settings", "/Users/viniciusdacal/ben/app/web/src/components/Route/Private/Private.jsx", "/Users/viniciusdacal/ben/app/web/src/components/QueuedJobs/List/List.jsx", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/AllocatedUsages/QualitySelector/QualitySelector.stories.jsx", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/AllocatedUsages/QualitySelector/QualitySelector.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Client/Selector/Selector.jsx", "/Users/viniciusdacal/ben/app/web/src/components/User/Form/ClientAndBrands.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Brand/Form/connector.js", "/Users/viniciusdacal/ben/ben.sublime-project", "/Users/viniciusdacal/ben/app/web/src/store/settings/actions.js", "/Users/viniciusdacal/ben/app/web/src/store/reduxForm.jsx", "/Users/viniciusdacal/ben/app/web/config/webpack.base.js", "/Users/viniciusdacal/ben/app/web/src/store/history/history.js", "/Users/viniciusdacal/ben/app/web/public/index.html", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/Form/utils.js", "/Users/viniciusdacal/ben/app/web/config/webpack.prod.js", "/Users/viniciusdacal/ben/app/web/config/webpack.dll.js", "/Users/viniciusdacal/ben/app/web/src/components/Form/TimePicker/connector.spec.js", "/Users/viniciusdacal/ben/app/web/src/components/Form/TimePicker/connector.js", "/Users/viniciusdacal/ben/app/web/src/components/Form/DateRangePicker/DateRangePicker.stories.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Form/DateRangePicker/DateRangePicker.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Placement/View/View.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Placement/View/chunks/Header.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Placement/View/Header/Header.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Opportunity/Search/Filters/Filters.jsx", "/Users/viniciusdacal/ben/app/web/src/components/Opportunity/utils/withMediaPlanFilters.js", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/Top/Top.jsx", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/FilteredList/FilteredList.jsx", "/Users/viniciusdacal/ben/app/web/src/components/MediaPlan/Form/defaultValuesBuilder.js", "/Users/viniciusdacal/ben/app/web/src/utils/fieldsSchema/fieldsSchema.js", "/Users/viniciusdacal/ben/app/web/src/components/Placement/View/Header/Header.scss", "/Users/viniciusdacal/ben/app/web/src/components/Form/DatePicker/scss/datepicker-styles.scss", "/Users/viniciusdacal/ben/app/web/src/components/QueuedJobs/List/schema.js", "/Users/viniciusdacal/ben/app/web/src/components/User/List/connector.jsx", "/Users/viniciusdacal/ben/app/web/src/utils/filtersWithUrl/selectors.js", "/Users/viniciusdacal/ben/app/web/src/utils/filtersWithUrl/filtersWithUrl.js" ], "find": { "height": 157.0 }, "find_in_files": { "height": 111.0, "where_history": [ ] }, "find_state": { "case_sensitive": true, "find_history": [ "payload:", "payload", "jest", "middlewares", "police", "middlewares", "exists", "item" ], "highlight": true, "in_selection": false, "preserve_case": false, "regex": false, "replace_history": [ ], "reverse": false, "show_context": true, "use_buffer2": true, "whole_word": false, "wrap": true }, "groups": [ { "sheets": [ ] } ], "incremental_find": { "height": 25.0 }, "input": { "height": 84.0 }, "layout": { "cells": [ [ 0, 0, 1, 1 ] ], "cols": [ 0.0, 1.0 ], "rows": [ 0.0, 1.0 ] }, "menu_visible": true, "output.SublimeLinter": { "height": 0.0 }, "output.exec": { "height": 176.0 }, "output.find_results": { "height": 0.0 }, "output.mdpopups": { "height": 0.0 }, "output.unsaved_changes": { "height": 112.0 }, "pinned_build_system": "", "project": "redux-arc.sublime-project", "replace": { "height": 46.0 }, "save_all_on_build": true, "select_file": { "height": 0.0, "last_filter": "", "selected_items": [ [ "request", "docs/advanced/RequestMiddlewares.md" ], [ "placement/redu", "web/src/components/Placement/reducers.js" ], [ "mediavaluecalcula", "web/src/components/Tools/MediaValueCalculator/connector.js" ], [ "additionalfields", "web/src/components/Tools/MediaValueCalculator/AdditionalFields/AdditionalFields.jsx" ], [ "inputcurrency", "web/src/components/Form/Input/Currency/Currency.jsx" ], [ "inputnumber", "web/src/components/Form/Input/Number/Number.jsx" ], [ "forminput", "web/src/components/Form/Input/Input.jsx" ], [ "calculations", "web/src/components/Tools/MediaValueCalculator/Calculations/Calculations.jsx" ], [ "mediavalue", "web/src/components/Tools/MediaValueCalculator/MediaValueCalculator.jsx" ], [ "redire", "web/src/store/middlewares/redirectOnResponse.js" ], [ "mp/form/conn", "web/src/components/MediaPlan/Form/connector.js" ], [ "mediaplanform", "web/src/components/MediaPlan/Form/Form.jsx" ], [ "errorhand", "web/src/store/api/errorHandlers.js" ], [ "mp/redu", "web/src/components/MediaPlan/reducers.js" ], [ "users/han", "api/packages/users/lib/handlers.js" ], [ "opportunityfilterchannel", "web/src/components/Opportunity/Filter/Channel/Channel.jsx" ], [ "opportunityfilterchanne", "web/src/components/Opportunity/Filter/Channel/Channel.stories.jsx" ], [ "reduxsele", "web/src/utils/reduxSelector/reduxSelector.jsx" ], [ "formselector.jsx", "web/src/components/Form/Selector/Selector.jsx" ], [ "reduxformfi", "web/src/components/Form/reduxFormField/reduxFormField.jsx" ], [ "opp/filterchann", "web/src/components/Opportunity/Filter/Channel/Channel.jsx" ], [ "close", "web/scss/components/_close.scss" ], [ "brandselector", "web/src/components/Brand/Selector/Selector.jsx" ], [ "utils/reduxselector/createselectors", "web/src/utils/reduxSelector/createSelectors.js" ], [ "reduxselector/sele", "web/src/utils/reduxSelector/selectors.js" ], [ "reduxselectorac", "web/src/utils/reduxSelector/actions.js" ], [ "mpform/conne", "web/src/components/MediaPlan/Form/connector.js" ], [ "reduxselector/reduc", "web/src/utils/reduxSelector/reducers.js" ], [ "mediaplanform/con", "web/src/components/MediaPlan/Form/connector.js" ], [ "brandform", "web/src/components/Brand/Form/Form.jsx" ], [ "components/form/uploader/connector", "web/src/components/Form/Uploader/connector.js" ], [ "react-uplo", "web/js/app/common/directives/react-file-uploader.js" ], [ "actioniconlistitem", "web/src/components/UI/ActionIconList/Item/Item.jsx" ], [ "mediaplancard", "web/src/components/MediaPlan/Card/Card.jsx" ], [ "mediaplanviewchannels", "web/src/components/MediaPlan/View/Channels/Channels.jsx" ], [ "summary.jsx", "web/src/components/MediaPlan/View/Summary/Summary.jsx" ], [ "components/mediaplan/selectors", "web/src/components/MediaPlan/selectors.js" ], [ "mediaplanview/con", "web/src/components/MediaPlan/View/connector.js" ], [ "mediaplanview", "web/src/components/MediaPlan/View/View.jsx" ], [ "mediaplanviewtot", "web/src/components/MediaPlan/View/Totals/Totals.jsx" ], [ "mp/reduc", "web/src/components/MediaPlan/reducers.js" ], [ "api.j", "web/e2e-tests/support/api.js" ], [ "mediaplanview/", "web/src/components/MediaPlan/View/connector.js" ], [ "channels", "web/src/components/MediaPlan/View/Channels/Channels.jsx" ], [ "src/__mocks__/redux-form.jsx", "web/src/__mocks__/redux-form.jsx" ], [ "user/form/accountinfo/accountinfo.stories.jsx", "web/src/components/User/Form/AccountInfo/AccountInfo.stories.jsx" ], [ "uploader/uploadedfile/uploadedfile.stories.jsx", "web/src/components/Form/Uploader/UploadedFile/UploadedFile.stories.jsx" ], [ "src/components/form/select/buttongroup/buttongroup.jsx", "web/src/components/Form/Select/ButtonGroup/ButtonGroup.jsx" ], [ "components/persona/form/form.stories.jsx", "web/src/components/Persona/Form/Form.stories.jsx" ], [ "mediaplanlistfilters", "web/src/components/MediaPlan/FilteredList/Filters/Filters.jsx" ], [ "filteredlist/filters/filters.stories.jsx", "web/src/components/MediaPlan/FilteredList/Filters/Filters.stories.jsx" ], [ "form/accountinfo/accountinfo", "web/src/components/User/Form/AccountInfo/AccountInfo.jsx" ], [ "components/opportunity/filter/ratings/ratings.jsx", "web/src/components/Opportunity/Filter/Ratings/Ratings.jsx" ], [ "oppchannelsalertsandratings", "web/src/components/Opportunity/ChannelsAlertsAndRatings/ChannelsAlertsAndRatings.jsx" ], [ "opportunity/channelsalertsandratings/channelsalertsandratings.stories.jsx", "web/src/components/Opportunity/ChannelsAlertsAndRatings/ChannelsAlertsAndRatings.stories.jsx" ], [ "components/form/selector/selector.stories.jsx", "web/src/components/Form/Selector/Selector.stories.jsx" ], [ "formselectorfilters", "web/src/components/Form/Selector/Filters/Filters.stories.jsx" ], [ "src/components/opportunity/statusandcoordinators/statusandcoordinators.stories.jsx", "web/src/components/Opportunity/StatusAndCoordinators/StatusAndCoordinators.stories.jsx" ], [ "uploadedfile", "web/src/components/Form/Uploader/UploadedFile/UploadedFile.spec.jsx" ], [ "src/components/form/uploader/uploadedfile/uploadedfile.stories.j", "web/src/components/Form/Uploader/UploadedFile/UploadedFile.stories.jsx" ], [ "src/components/form/uploader/dropzone/dropzone.stories.jsx", "web/src/components/Form/Uploader/Dropzone/Dropzone.stories.jsx" ], [ "src/components/form/selector/selector.stories.jsx", "web/src/components/Form/Selector/Selector.stories.jsx" ], [ "src/components/form/countryselector/countryselector.stories.jsx", "web/src/components/Form/CountrySelector/CountrySelector.stories.jsx" ], [ "package.json", "web/package.json" ], [ "config/jest/setup.js", "web/config/jest/setup.js" ], [ "storybook/webpack.config.js", "web/.storybook/webpack.config.js" ], [ ".storybook/config.js", "web/.storybook/config.js" ], [ "src/components/queuedjobs/list/list.stories.jsx", "web/src/components/QueuedJobs/List/List.stories.jsx" ], [ "web/src/components/form/buttongroup/buttongroup.jsx", "web/src/components/Form/ButtonGroup/ButtonGroup.jsx" ], [ "queuedjobs/utils/mockeddata.js", "web/src/components/QueuedJobs/utils/mockedData.js" ], [ "components/queuedjobs/list/list.stories.jsx", "web/src/components/QueuedJobs/List/List.stories.jsx" ], [ "datepicker", "web/src/components/Form/DatePicker/DatePicker.jsx" ], [ "datepicker/datepicker.stories.jsx", "web/src/components/Form/DatePicker/DatePicker.stories.jsx" ], [ "components/queuedjobs/reducers.js", "web/src/components/QueuedJobs/reducers.js" ], [ "components/queuedjobs/reducers.spec.js", "web/src/components/QueuedJobs/reducers.spec.js" ], [ "web/src/components/mediaplan/form/form.jsx", "web/src/components/MediaPlan/Form/Form.jsx" ], [ "omponents/mediaplan/form/connector.js", "web/src/components/MediaPlan/Form/connector.js" ], [ "buttongroup", "web/src/components/Form/ButtonGroup/ButtonGroup.jsx" ], [ "qualityselector", "web/src/components/MediaPlan/AllocatedUsages/QualitySelector/QualitySelector.jsx" ], [ "mediaplan/allocatedusages/qualityselector/qualityselector.stories.jsx", "web/src/components/MediaPlan/AllocatedUsages/QualitySelector/QualitySelector.stories.jsx" ], [ "components/queuedjobs/list/list", "web/src/components/QueuedJobs/List/List.jsx" ], [ "queutils/mockeddata", "web/src/components/QueuedJobs/utils/mockedData.js" ], [ "private", "web/src/components/Route/Private/Private.jsx" ], [ "mediaplan/form/connector.j", "web/src/components/MediaPlan/Form/connector.js" ], [ "mediaplan/form/form.jsx", "web/src/components/MediaPlan/Form/Form.jsx" ], [ "web/src/utils/reduxselector/reducers.js", "web/src/utils/reduxSelector/reducers.js" ], [ "src/utils/function.js", "web/src/utils/function.js" ], [ "clientselec", "web/src/components/Client/Selector/Selector.jsx" ], [ "mediaplan/form/co", "web/src/components/MediaPlan/Form/connector.js" ], [ "clientselector", "web/src/components/Client/Selector/Selector.jsx" ], [ "reduselector/", "web/src/utils/reduxSelector/reducers.js" ], [ "user/form", "web/src/components/User/Form/ClientAndBrands.jsx" ], [ "reduxse", "web/src/utils/reduxSelector/reduxSelector.jsx" ], [ "mediaplanform/conne", "web/src/components/MediaPlan/Form/connector.js" ], [ "brandform/con", "web/src/components/Brand/Form/connector.js" ], [ "/components/mediaplan/form/connector.js", "web/src/components/MediaPlan/Form/connector.js" ], [ "redusele", "web/src/utils/reduxSelector/reduxSelector.jsx" ], [ "setting/ac", "web/src/store/settings/actions.js" ], [ "mediavalu/conn", "web/src/components/Tools/MediaValueCalculator/connector.js" ], [ "reduxform", "web/src/store/reduxForm.jsx" ], [ "histo", "web/src/store/history/history.js" ], [ "mp/form", "web/src/components/MediaPlan/Form/Form.jsx" ], [ "timepicker/con", "web/src/components/Form/TimePicker/connector.js" ], [ "daterangepic", "web/src/components/Form/DateRangePicker/DateRangePicker.jsx" ], [ "form/daterangepicker/daterangepicker.stories.jsx", "web/src/components/Form/DateRangePicker/DateRangePicker.stories.jsx" ], [ "form/timepicker/connector", "web/src/components/Form/TimePicker/connector.spec.js" ], [ "store/reduxform", "web/src/store/reduxForm.jsx" ], [ "mediaplan/filt", "web/src/components/MediaPlan/FilteredList/Filters/Filters.jsx" ], [ "withmediaplanfilters", "web/src/components/Opportunity/utils/withMediaPlanFilters.js" ], [ "opportunity/search/fil", "web/src/components/Opportunity/Search/Filters/connector.js" ], [ "opportunity/search/filter", "web/src/components/Opportunity/Search/Filters/Filters.jsx" ], [ "utils/fieldsschema", "web/src/utils/fieldsSchema/fieldsSchema.js" ], [ "pl/view/header", "web/src/components/Placement/View/Header/Header.jsx" ], [ "placementview", "web/src/components/Placement/View/View.jsx" ], [ "components/form/buttongroup/buttongroup.jsx", "web/src/components/Form/ButtonGroup/ButtonGroup.jsx" ], [ "opportunity/search/filters/connector.js", "web/src/components/Opportunity/Search/Filters/connector.js" ], [ "filterswithurls/", "web/src/utils/filtersWithUrl/selectors.js" ], [ "filterswithurl/", "web/src/utils/filtersWithUrl/utils.js" ], [ "userlist/cone", "web/src/components/User/List/connector.jsx" ], [ "withurl", "web/src/utils/filtersWithUrl/filtersWithUrl.js" ], [ "groupbutt", "web/src/components/Form/ButtonGroup/ButtonGroup.jsx" ], [ "withmed", "web/src/components/Opportunity/utils/withMediaPlanFilters.js" ], [ "opp/search/filter", "web/src/components/Opportunity/Search/Filters/connector.js" ], [ "channelsalertsandratings", "web/src/components/Opportunity/ChannelsAlertsAndRatings/ChannelsAlertsAndRatings.jsx" ], [ "reduxformfie", "web/src/components/Form/reduxFormField/reduxFormField.jsx" ], [ "filterswithurl", "web/src/utils/filtersWithUrl/selectors.js" ], [ "filters", "web/src/utils/filtersWithUrl/filtersWithUrl.js" ], [ "buttongrou", "web/src/components/Form/ButtonGroup/ButtonGroup.jsx" ] ], "width": 0.0 }, "select_project": { "height": 0.0, "last_filter": "", "selected_items": [ ], "width": 0.0 }, "select_symbol": { "height": 0.0, "last_filter": "", "selected_items": [ ], "width": 0.0 }, "selected_group": 0, "settings": { }, "show_minimap": true, "show_open_files": false, "show_tabs": true, "side_bar_visible": true, "side_bar_width": 301.0, "status_bar_visible": true, "template_settings": { } } ================================================ FILE: rollup.config.js ================================================ import nodeResolve from 'rollup-plugin-node-resolve'; import babel from 'rollup-plugin-babel'; import replace from 'rollup-plugin-replace'; import uglify from 'rollup-plugin-uglify'; var env = process.env.NODE_ENV const config = { input: 'src/index.js', plugins: [] } if (env === 'es' || env === 'cjs') { config.output = { format: env } config.external = ['redux'] config.plugins.push( babel({ plugins: ['external-helpers'], }) ) } if (env === 'development' || env === 'production') { config.output = { format: 'umd' } config.name = 'ReduxArc' config.plugins.push( nodeResolve({ jsnext: true }), babel({ exclude: [ '**/node_modules/**', 'src/__mocks__/**', ], plugins: ['external-helpers'], }), replace({ 'process.env.NODE_ENV': JSON.stringify(env) }) ) } if (env === 'production') { config.plugins.push( uglify({ compress: { pure_getters: true, unsafe: true, unsafe_comps: true, warnings: false } }) ) } export default config ================================================ FILE: src/apiActionCreatorFactory.js ================================================ import { toAsyncTypes } from './utils'; import parseUrl from './parseUrl'; const normalizeUrl = (url, params) => typeof url === 'function' ? url(params) : url; export default function apiActionCreatorFactory(config, type) { const { payload: configPayload, url: configUrl, meta: configMeta, ...restMeta } = config; const asyncTypes = toAsyncTypes(type); function apiActionCreator(payload, meta) { const url = normalizeUrl(configUrl, meta); const finalMeta = { ...restMeta, ...(configMeta || {}), ...(meta || {}), }; return { type: asyncTypes, payload: payload !== undefined ? payload : configPayload, meta: { ...finalMeta, url: parseUrl(url, finalMeta), }, }; } Object.assign(apiActionCreator, config); return apiActionCreator; } ================================================ FILE: src/createActions.js ================================================ import { parseToUppercase } from './utils'; import createTypes from './createTypes'; import createCreators from './createCreators'; import validateConfig from './validateConfig'; import toExternalTypes from './toExternalTypes'; /* @param {string} namespace - namespace to be uppercased and prefix your action types @param {Array} config - object with options */ export default function createActions(namespace, config) { const NAMESPACE = parseToUppercase(namespace); validateConfig(NAMESPACE, config); const actionKeys = Object.keys(config); const actionTypes = createTypes(actionKeys, NAMESPACE); const creators = createCreators(config, actionTypes); return { creators, types: toExternalTypes(config, actionTypes), }; } ================================================ FILE: src/createAsyncMiddleware.js ================================================ import { compose } from 'redux'; import { checkAction } from './utils'; import { getRequestMiddlewares } from './requestMiddlewares'; /** * This is a standard Redux middleware that listens for async actions * This middleware waits as its first param, a function with the following signature * done => (action, response) => done(action, error, response) * * The actions for this middleware, should look like the following * { * type: ['REQUEST_TYPE', 'RESPONSE_TYPE'], * payload: {}, * meta: { * ...allAdditionalData * }, * } * @param {Object} asyncTask - function that executes the async task */ function execAsyncTask(requestType, asyncTask) { return store => next => (action) => { store.dispatch({ type: requestType, meta: action.meta, payload: action.payload, }); const done = (err, response) => next(action, err, response); const options = { payload: action.payload, ...action.meta }; return asyncTask(store)(done)(options); }; } function handleResponse(responseType) { return store => (action, err, response) => { const responseAction = { type: responseType, meta: action.meta, payload: response, }; if (err) { const actionToDispatch = { ...responseAction, error: true, payload: err }; store.dispatch(actionToDispatch); return err; } store.dispatch(responseAction); return response; }; } export default function createAsyncMiddleware(asyncTask) { if (typeof asyncTask !== 'function') { const warning = 'You must provide a asyncTask function to createAsyncMiddleware, with the following signature: '; const example = 'done => (action, error, response) => done(action, error, response)'; throw new Error(`${warning} \n ${example}`) } return store => next => (action) => { const { type, meta } = action; if (!Array.isArray(type)) { return next(action); } if (!checkAction(type)) { throw new Error('Expected type to be an array of two strings, request and response.'); } if (!meta || typeof meta !== 'object') { throw new Error('Expected meta to be an object'); } const requestMiddlewares = getRequestMiddlewares(action.meta.middlewares); const [requestType, responseType] = action.type; const chain = [ requestMiddlewares('onRequest'), execAsyncTask(requestType, asyncTask), requestMiddlewares('onResponse'), ].map(middleware => middleware(store)); const done = handleResponse(responseType)(store); return compose(...chain)(done)(action); }; } ================================================ FILE: src/createCreators.js ================================================ import fsaActionCreatorFactory from './fsaActionCreatorFactory'; import apiActionCreatorFactory from './apiActionCreatorFactory'; function getFactory(singleConfig) { return !singleConfig || !singleConfig.url ? fsaActionCreatorFactory : apiActionCreatorFactory; } const DEFAULT_CONFIG = {}; /* @param {Object} config - original config object provided to createActions @param {Object} actionTypes - action types object with keys being the original names and the value being the uppercased namespace + uppercased name. */ export default function createCreators(config, actionTypes) { return Object.keys(config).reduce((acc, creatorName) => { const singleConfig = config[creatorName] || DEFAULT_CONFIG; const factory = getFactory(singleConfig) return { ...acc, [creatorName]: factory(singleConfig, actionTypes[creatorName]), }; }, {}); } ================================================ FILE: src/createReducers.js ================================================ function insertEmojis (string) { const searches = [ ' undefined', ' null', ' \'', ' \\[object Object\\]', ' \[0-9\].*', ]; return searches.reduce( (str, search, index) => str.replace(new RegExp(`(${search})`, 'g'), ` 👉$1`), string) } function visualValue(value) { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (typeof value === 'string') return `'${value}'`; if (typeof value === 'function') { return value.toString().replace(/\n/g, '').replace(/{(.*)}/g, '{ ... }'); } return value; } function objToString(obj) { let output = []; Object.keys(obj).forEach((key) => { output.push(` ${key}: ${visualValue(obj[key])},`); }); return `{\n${output.join('\n')}\n}`; } function validateHandlers(handlers) { if (!handlers) { throw new Error(`Invalid handler: ${handlers}`); } const isInvalid = Object.keys(handlers).some((key) => key === '[object Object]' || key === 'undefined' || key === 'null' || typeof handlers[key] !== 'function' ); if (isInvalid) { throw new Error( `All keys must be valid types and all values should be functions:\n${insertEmojis(objToString(handlers))} `); } } export default function createReducers(initialState, handlers) { validateHandlers(handlers); return (state = initialState, action) => { if (!action) { return state; } const handler = handlers[action.type]; return !handler ? state : handler(state, action); } } ================================================ FILE: src/createTypes.js ================================================ import { parseToUppercase } from './utils'; export default function createTypes(actionKeys, NAMESPACE) { return actionKeys.reduce((acc, actionName) => ({ ...acc, [actionName]: `${NAMESPACE}_${parseToUppercase(actionName)}`, }), {}); }; ================================================ FILE: src/fsaActionCreatorFactory.js ================================================ export default function actionCreatorFactory(config, type) { const normalizedConfig = config !== null && typeof config === 'object' ? config : {}; const { payload: configPayload, meta: configMeta, error: configError } = config; function actionCreator(payload, meta, error) { const action = { type, }; const finalMeta = { ...configMeta, ...meta, }; if (Object.keys(finalMeta).length) { action.meta = finalMeta; } const finalPayload = payload !== undefined ? payload : configPayload; if (finalPayload !== undefined) { action.payload = finalPayload; } const finalError = error !== undefined ? error : configError; if (finalError !== undefined) { action.error = finalError; } return action; } Object.assign(actionCreator, normalizedConfig); return actionCreator; } ================================================ FILE: src/index.js ================================================ import createActions from './createActions'; import createAsyncMiddleware from './createAsyncMiddleware'; import createReducers from './createReducers'; import middlewares from './requestMiddlewares'; export { createActions, createAsyncMiddleware, createReducers, middlewares, }; ================================================ FILE: src/parseUrl.js ================================================ export default function parseUrl(url, params) { return url.replace(/(:)([A-Za-z][A-Za-z0-9]*)/g, (match, $1, $2) => { const paramType = typeof params[$2]; if (paramType !== 'string' && paramType !== 'number') { throw new Error(`Param ${$2} from url ${url}, not found in params object`); } return params[$2]; }); } ================================================ FILE: src/requestMiddlewares.js ================================================ import { compose } from 'redux'; /** * This is a middleware manager, which is used to get get and run middlewares * over actions and responses * * A request middleware should follow the signature below * store => done => (action, error, response) => done(action, error, response); * A middleware must have an applyPoint property. The available apply points are: * 'onRequest' and 'onResponse' */ const applyPoints = ['onRequest', 'onResponse']; const notEmpty = item => !!item; export function validateMiddleware(middlewares) { middlewares.forEach(middleware => { if (typeof middleware !== 'function') { throw new Error(`All middlewares should be functions: [${middlewares}]`); } if (applyPoints.indexOf(middleware.applyPoint) < 0) { const invalid = `Invalid applyPoint: ${middleware.applyPoint}, provided with middleware: ${middleware.name}.`; const available = `The apply points available are: ${applyPoints.join(', ')}`; throw new Error(`${invalid} ${available}`); } }); } const withApplyPoint = applyPoint => middleware => middleware.applyPoint === applyPoint; const get = middlewares => (applyPoint) => { const applyMiddlewares = middlewares.filter(withApplyPoint(applyPoint)); return store => done => { const chain = applyMiddlewares.map(middleware => middleware(store)); return compose(...chain)(done); }; }; export function getRequestMiddlewares(middlewares) { if (!Array.isArray(middlewares)) { return get([]); } const actualMiddlewares = middlewares.filter(notEmpty); validateMiddleware(actualMiddlewares); return get(actualMiddlewares); }; export default { getRequestMiddlewares, }; ================================================ FILE: src/toApiExternalType.js ================================================ import { toAsyncTypes } from './utils'; export default function toApiExternalType(actionType) { const asyncTypes = toAsyncTypes(actionType); return { REQUEST: asyncTypes[0], RESPONSE: asyncTypes[1], }; } ================================================ FILE: src/toExternalTypes.js ================================================ import { parseToUppercase } from './utils'; import toApiExternalType from './toApiExternalType'; function getParser(singleConfig) { return !singleConfig || !singleConfig.url ? actionType => actionType : toApiExternalType; } export default function toExternalType(config, actionTypes) { return Object.keys(actionTypes).reduce((acc, key) => { const parse = getParser(config[key]) return { ...acc, [parseToUppercase(key)]: parse(actionTypes[key]) } }, {}); } ================================================ FILE: src/utils.js ================================================ export const isString = str => typeof str === 'string'; export const checkAction = type => type.length === 2 && type.every(isString); export const parseToUppercase = (str) => str.replace(/([A-Z])/g, '_$1').toUpperCase(); export const removeNamespace = (str, NAMESPACE) => str.replace(`${NAMESPACE}_`, ''); export const toAsyncTypes = (type) => [`${type}_REQUEST`, `${type}_RESPONSE`]; ================================================ FILE: src/validateConfig.js ================================================ import { parseToUppercase } from './utils'; export default function validateConfig(namespace, configs) { Object.keys(configs).forEach((creatorName) => { const config = configs[creatorName] || {}; const configName = `${namespace}_${parseToUppercase(creatorName)}`; if (config.url && typeof config.url !== 'string' && typeof config.url !== 'function') { throw new Error( `Invalid url, ${config.url}, provided for ${configName}, it should be a string or a function that returns a string`, ); } if (config.url && (typeof config.method !== 'string' || !config.method.length)) { throw new Error( `Invalid method, ${config.method}, provided for ${configName}, it should be a string`, ); } if (config.modifier && typeof config.modifier !== 'function') { throw new Error( `Invalid modifier handler, ${config.modifier}, provided for ${configName}, it should be a function`, ); } }); if (!namespace || typeof namespace !== 'string') { throw new Error(`Invalid namespace provided: ${namespace}, it should be a string`); } } ================================================ FILE: test/.eslintrc ================================================ { "env": { "jest": true } } ================================================ FILE: test/constants.js ================================================ const urlFunction = (params) => `/${params.test}/`; export const BASE_TYPES = { list: 'MY_LIST', listWithUrlFunction: 'MY_LIST_WITH_URL_FUNCTION', read: 'MY_READ', readWithExtras: 'MY_READ_WITH_EXTRAS', }; export const BASE_CONFIGS = { list: { url: 'endpoint', method: 'get' }, listWithUrlFunction: { url: urlFunction, method: 'get' }, read: { url: 'endpoint/:id', method: 'put' }, readWithExtras: { url: 'endpoint/:id', method: 'put', middlewares: ['myMiddleware'], }, }; ================================================ FILE: test/createActions.spec.js ================================================ import createActions from '../src/createActions'; const baseConfigs = { list: { url: 'endpoint', method: 'get' }, read: { url: 'endpoint/:id', method: 'put' }, readWithExtras: { url: 'endpoint/:id', method: 'put', middlewares: ['middleware'], meta: { extraParam: 'EXTRA_PARAM', } }, reset: null, clear: 1, withDefaults: { payload: '1', meta: { foo: 'bar' }, error: true }, resetWithMeta: { meta: { a: 'TEST', b: 'META_DATA', }, payload: 'TEST_PAYLOAD', } }; describe('createActions', () => { const { types, creators } = createActions('my', baseConfigs); it('should return the proper types object', () => { const expectedTypes = { LIST: { REQUEST: 'MY_LIST_REQUEST', RESPONSE: 'MY_LIST_RESPONSE', }, READ: { REQUEST: 'MY_READ_REQUEST', RESPONSE: 'MY_READ_RESPONSE', }, READ_WITH_EXTRAS: { REQUEST: 'MY_READ_WITH_EXTRAS_REQUEST', RESPONSE: 'MY_READ_WITH_EXTRAS_RESPONSE', }, RESET: 'MY_RESET', WITH_DEFAULTS: 'MY_WITH_DEFAULTS', CLEAR: 'MY_CLEAR', RESET_WITH_META: 'MY_RESET_WITH_META', }; expect(types).toEqual(expectedTypes); }); it('should return the proper action when calling a creator without any value', () => { expect(creators.list()).toEqual({ type: [types.LIST.REQUEST, types.LIST.RESPONSE], meta: { url: 'endpoint', method: 'get', }, }); expect(creators.reset()).toEqual({ type: types.RESET, }); }); it('should parse the url with provided params', () => { expect(creators.read(null, { id: '123' })).toEqual({ type: [types.READ.REQUEST, types.READ.RESPONSE], meta: { url: 'endpoint/123', method: 'put', id: '123', }, payload: null, }); }); it('should return the final action with given payload, meta and error', () => { expect(creators.reset(null, { id: '123' }, false)).toEqual({ type: types.RESET, meta: { id: '123', }, payload: null, error: false, }); }); it('should return the action with defaults values if configured', () => { expect(creators.withDefaults()).toEqual({ type: types.WITH_DEFAULTS, meta: { foo: 'bar', }, payload: '1', error: true, }); }); it('should return middlewares and any extra param inside meta', () => { expect(creators.readWithExtras({ test: 'TEST' }, { id: '123' })).toEqual({ type: [types.READ_WITH_EXTRAS.REQUEST, types.READ_WITH_EXTRAS.RESPONSE], payload: { test: 'TEST' }, meta: { url: 'endpoint/123', method: 'put', id: '123', middlewares: ['middleware'], extraParam: 'EXTRA_PARAM', }, }); }); it('should prefer the meta provided in the call ranther than the one from config', () => { expect(creators.readWithExtras({ test: 'TEST' }, { id: '123', middlewares: ['override'] })).toEqual({ type: [types.READ_WITH_EXTRAS.REQUEST, types.READ_WITH_EXTRAS.RESPONSE], payload: { test: 'TEST' }, meta: { url: 'endpoint/123', method: 'put', id: '123', middlewares: ['override'], extraParam: 'EXTRA_PARAM', }, }); }) }); ================================================ FILE: test/createAsyncMiddleware.spec.js ================================================ /* eslint-disable import/first */ jest.mock('../src/requestMiddlewares', () => ({ globalMiddlewares: {}, onCallApply: jest.fn((applyPoint) => store => done => (action, error, response) => done(action, error, response)) })); const middlewares = require('../src/requestMiddlewares'); const get = policeNames => middlewares.onCallApply; middlewares.getRequestMiddlewares = jest.fn((middlewares) => { if (Array.isArray(middlewares)) { return get(middlewares); } return get([]); }); // eslint-disable-next-line import/first import createAsyncMiddleware from '../src/createAsyncMiddleware'; import middlewaresMock from '../src/requestMiddlewares'; const storeApi = { dispatch: jest.fn(() => {}), getState: jest.fn(() => {}), }; const API_RESPONSE = 'API_RESPONSE'; const API_ERROR = 'API_ERROR'; const asyncTask = store => done => (action) => { done(null, API_RESPONSE); return API_RESPONSE; }; const asyncErrorTask = store => done => (action) => { done(API_ERROR, null); return API_ERROR; }; describe('createAsyncMiddleware', () => { it('should throw if you does not provide an asyncTask function', () => { expect(() => createAsyncMiddleware()).toThrow() expect(() => createAsyncMiddleware('test')).toThrow(); }); it('should not intercept regular actions', () => { const nextMock = jest.fn(); const apiMiddleware = createAsyncMiddleware(asyncTask)(storeApi)(nextMock); apiMiddleware({ type: 'REGULAR_ACTION', meta: {}, }); expect(nextMock.mock.calls.length).toBe(1); }); it('should throw for invalid types', () => { const nextMock = jest.fn(); const apiMiddleware = createAsyncMiddleware(asyncTask)(storeApi)(nextMock); expect(() => apiMiddleware({ type: ['REGULAR_ACTION', 2], meta: {} })).toThrow(); }); it('should throw when has no meta in the action', () => { const nextMock = jest.fn(); const apiMiddleware = createAsyncMiddleware(asyncTask)(storeApi)(nextMock); expect(() => apiMiddleware({ type: ['REGULAR_ACTION', '2'] })).toThrow(); }); it('should get actionPolicies passing the proper argument', () => { const nextMock = jest.fn(); const apiMiddleware = createAsyncMiddleware(asyncTask)(storeApi)(nextMock); const middlewares = ['mypolice']; apiMiddleware({ type: ['REQUEST_ACTION', 'RESPONSE_ACTION'], meta: { middlewares, }, }); expect(middlewaresMock.getRequestMiddlewares.mock.calls.length).toBe(1); expect(middlewaresMock.getRequestMiddlewares.mock.calls[0][0]).toBe(middlewares); middlewaresMock.getRequestMiddlewares.mockClear(); middlewaresMock.onCallApply.mockClear(); storeApi.dispatch.mockClear(); }); it('should execute the middlewares in the sequence', () => { const nextMock = jest.fn(); const apiMiddleware = createAsyncMiddleware(asyncTask)(storeApi)(nextMock); const middlewares = ['mypolice']; const returnValue = apiMiddleware({ type: ['REQUEST_ACTION', 'RESPONSE_ACTION'], meta: { middlewares, }, }); expect(returnValue).toBe(API_RESPONSE); expect(middlewaresMock.onCallApply.mock.calls.length).toBe(2); expect(middlewaresMock.onCallApply.mock.calls[0][0]).toBe('onRequest'); expect(middlewaresMock.onCallApply.mock.calls[1][0]).toBe('onResponse'); storeApi.dispatch.mockClear(); }); it('should dispatch an action with error when get error from the asyncTask', () => { const nextMock = jest.fn(); const apiMiddleware = createAsyncMiddleware(asyncErrorTask)(storeApi)(nextMock); const middlewares = ['mypolice']; const returnValue = apiMiddleware({ type: ['REQUEST_ACTION', 'RESPONSE_ACTION'], meta: { middlewares, }, }); expect(returnValue).toBe(API_ERROR); expect(storeApi.dispatch.mock.calls.length).toBe(2); expect(storeApi.dispatch.mock.calls[0][0].type).toBe('REQUEST_ACTION'); expect(storeApi.dispatch.mock.calls[1][0].type).toBe('RESPONSE_ACTION'); expect(storeApi.dispatch.mock.calls[1][0].error).toBe(true); expect(storeApi.dispatch.mock.calls[1][0].payload).toBe(API_ERROR); }); it('should call asyncTask with the store', () => { const asyncTaskMock = jest.fn(); asyncTaskMock.mockReturnValue(() => () => {}); const apiMiddleware = createAsyncMiddleware(asyncTaskMock)(storeApi)(() => {}); apiMiddleware({ type: ['REQUEST_ACTION', 'RESPONSE_ACTION'], meta: {}, }); expect(asyncTaskMock.mock.calls[0][0]).toBe(storeApi); storeApi.dispatch.mockClear(); }) }); ================================================ FILE: test/createCreators.spec.js ================================================ import createCreators from '../src/createCreators'; import apiActionCreatorFactory from '../src/apiActionCreatorFactory'; const urlFunction = (params) => `/${params.test}/`; const BASE_TYPES = { list: 'MY_LIST', listWithUrlFunction: 'MY_LIST_WITH_URL_FUNCTION', read: 'MY_READ', readWithExtras: 'MY_READ_WITH_EXTRAS', }; const BASE_CONFIGS = { list: { url: 'endpoint', method: 'get' }, listWithUrlFunction: { url: urlFunction, method: 'get' }, read: { url: 'endpoint/:id', method: 'put' }, readWithExtras: { url: 'endpoint/:id', method: 'put', middlewares: ['myMiddleware'], }, }; describe('createCreators', () => { it('should return an action creator', () => { const creators = createCreators(BASE_CONFIGS, BASE_TYPES, apiActionCreatorFactory); expect(creators.list()).toEqual({ type: ['MY_LIST_REQUEST', 'MY_LIST_RESPONSE'], meta: { url: 'endpoint', method: 'get', }, payload: undefined, }); expect(creators.listWithUrlFunction(null, { test: 1232 })).toEqual({ type: [ 'MY_LIST_WITH_URL_FUNCTION_REQUEST', 'MY_LIST_WITH_URL_FUNCTION_RESPONSE' ], meta: { url: urlFunction({ test: 1232 }), method: 'get', test: 1232, }, payload: null, }); expect(creators.read(null, { id: '123' })).toEqual({ type: ['MY_READ_REQUEST', 'MY_READ_RESPONSE'], meta: { url: 'endpoint/123', method: 'put', id: '123', }, payload: null }); expect(creators.readWithExtras(null, { id: '123' })).toEqual({ type: ['MY_READ_WITH_EXTRAS_REQUEST', 'MY_READ_WITH_EXTRAS_RESPONSE'], meta: { url: 'endpoint/123', method: 'put', id: '123', middlewares: ['myMiddleware'], }, payload: null, }); }); }); ================================================ FILE: test/createReducers.spec.js ================================================ import createReducers from '../src/createReducers'; const expectedToThrow = `All keys must be valid types and all values should be functions: { a: 👉 'test', 👉 undefined: function Test(state, action) { ... }, b: function b() { ... }, c: function c() { ... }, d: 👉 null, 👉 [object Object]: 👉 null, e: 👉 1, f: 👉 undefined, }`; describe('createReducers', () => { const INCLUDE_C = 'INCLUDE_C'; const INCLUDE_D = 'INCLUDE_D'; const ACTION_NOT_REGISTERED = 'ACTION_NOT_REGISTERED'; const INITIAL_STATE = { a: 'A', b: 'B', }; const HANDLERS = { [INCLUDE_C]: (state = INITIAL_STATE, action) => ({ ...state, c: 'C', }), [INCLUDE_D]: (state = INITIAL_STATE, action) => ({ ...state, d: 'D', }), }; it('should throw when a key is not defined', () => { expect(() => { const key = undefined; createReducers({}, { [key]: function() {}, }) }).toThrow(); }); it('should throw when a key is invalid', () => { expect(() => { const key = { foo: 'bar' }; createReducers({}, { [key]: function() {}, }) }).toThrow(); }); it('should throw when a reducer is not a function', () => { expect(() => { createReducers({}, { a: '' }); }).toThrow(); }); it('should throw if handlers is empty', () => { expect(() => { createReducers({}, null); }).toThrow('Invalid handler: null'); }); it('should proper format the error message', () => { expect(() => { const key = undefined; createReducers({}, { a: 'test', [key]: function Test(state, action) { return { ...state, foo: 'bar', } }, b: () => {}, c: function() {}, d: null, [{}]: null, e: 1, f: undefined, }) }).toThrow(expectedToThrow); }); const reducer = createReducers(INITIAL_STATE, HANDLERS); it('should return same state when no reducers found for the action', () => { expect(reducer(INITIAL_STATE, { type: ACTION_NOT_REGISTERED })).toEqual(INITIAL_STATE); }) it('should provide the correct state', () => { expect(reducer({ z: 'z', y: 'y'}, { type: ACTION_NOT_REGISTERED })).toEqual({ z: 'z', y: 'y'}); }); it('should return the current state if there is no action', () => { expect(reducer({ z: 'z', y: 'y'})).toEqual({ z: 'z', y: 'y'}); }); it('should return assume INITIAL_STATE when no state is provided', () => { expect(reducer()).toEqual(INITIAL_STATE); }); it('should call only the reducer registered for the action', () => { expect(reducer(INITIAL_STATE, { type: INCLUDE_C })).toEqual({ a: 'A', b: 'B', c: 'C', }); expect(reducer(INITIAL_STATE, { type: INCLUDE_D })).toEqual({ a: 'A', b: 'B', d: 'D', }); }); }); ================================================ FILE: test/createTypes.spec.js ================================================ import createTypes from '../src/createTypes'; describe('createTypes', () => { it('should return an object with the respective action types', () => { const actionTypes = createTypes(['list', 'softDelete'], 'MY'); expect(actionTypes).toEqual({ list: 'MY_LIST', softDelete: 'MY_SOFT_DELETE', }); }); }); ================================================ FILE: test/middlewareWithRedux.spec.js ================================================ import { createStore, applyMiddleware } from 'redux'; import createAsyncMiddleware from '../src/createAsyncMiddleware'; describe('Testing middleware on redux', () => { const SINGULAR_RESPONSE = 'SINGULAR_RESPONSE'; const asyncTask = store => next => (action) => { next(null, SINGULAR_RESPONSE); const promise = new Promise((resolve, reject) => { resolve(SINGULAR_RESPONSE); }); return promise; }; const mockReducer = jest.fn((state, action) => state); const createStoreWithHamal = applyMiddleware(createAsyncMiddleware(asyncTask))(createStore); const store = createStoreWithHamal(mockReducer); it('should dispatch the actions', (done) => { mockReducer.mockClear(); const returnedValue = store.dispatch({ type: ['REQUEST_ACTION', 'RESPONSE_ACTION'], meta: { url: 'test' }, payload: {}, }); expect(mockReducer.mock.calls[0][1]).toEqual({ type: 'REQUEST_ACTION', meta: { url: 'test' }, payload: {}, }); expect(mockReducer.mock.calls[1][1]).toEqual({ type: 'RESPONSE_ACTION', meta: { url: 'test' }, payload: SINGULAR_RESPONSE, }); returnedValue.then((value) => { done(); expect(value).toBe(SINGULAR_RESPONSE); }) }); it('should perform middlewares over the actions', (done) => { mockReducer.mockClear(); const onRequestMiddleware = store => done => (action, error, response) => done({ ...action, meta: { ...action.meta, onRequest: true }, }, error, response); onRequestMiddleware.applyPoint = 'onRequest'; const onResponseMiddleware = store => done => (action, error, response) => done({ ...action, meta: { ...action.meta, onResponse: true }, }, error, response); onResponseMiddleware.applyPoint = 'onResponse'; const returnedValue = store.dispatch({ type: ['REQUEST_ACTION', 'RESPONSE_ACTION'], payload: {}, meta: { url: 'test', middlewares: [onRequestMiddleware, onResponseMiddleware], extras: true }, }); expect(mockReducer.mock.calls[0][1]).toEqual({ type: 'REQUEST_ACTION', payload: {}, meta: { url: 'test', middlewares: [onRequestMiddleware, onResponseMiddleware], extras: true, onRequest: true, }, }); expect(mockReducer.mock.calls[1][1]).toEqual({ type: 'RESPONSE_ACTION', meta: { url: 'test', middlewares: [onRequestMiddleware, onResponseMiddleware], extras: true, onResponse: true, onRequest: true, }, // Passed through both middlewares payload: SINGULAR_RESPONSE, }); returnedValue.then((value) => { done(); expect(value).toBe(SINGULAR_RESPONSE); }); }); }); ================================================ FILE: test/parseUrl.spec.js ================================================ import parseUrl from '../src/parseUrl'; describe('parseUrl', () => { it('should ignore url without params', () => { expect(parseUrl('regular/endpoint')).toBe('regular/endpoint'); }); it('should throw if param isnt present', () => { expect(() => parseUrl('endpoint/:myparam', {})).toThrow( 'Param myparam from url endpoint/:myparam, not found in params object' ); }); it('should parse params', () => { expect(parseUrl('endpoint/:myparam', { myparam: 'hey-ho'})).toBe('endpoint/hey-ho'); }); it('should ignore the begning of a absolute url', () => { expect(() => parseUrl('https://ds.devel.goben.rocks/api:8080', {})).not.toThrow(); expect(parseUrl('https://ds.devel.goben.rocks/api:8080', {})) .toBe('https://ds.devel.goben.rocks/api:8080'); }); }); ================================================ FILE: test/publicApi.spec.js ================================================ import { createActions, createAsyncMiddleware, createReducers, middlewares, } from '../src'; const publicNames = [ 'createActions', 'createAsyncMiddleware', 'createReducers', 'middlewares', ]; describe('publicApi', () => { [ createActions, createAsyncMiddleware, createReducers, middlewares, ].map((value, index) => { it(`should ${publicNames[index]} be present`, () => { expect(!!value).toBe(true); }); }); }); ================================================ FILE: test/requestMiddlewares.spec.js ================================================ import { getRequestMiddlewares } from '../src/requestMiddlewares'; describe('getRequestMiddlewares', () => { const SINGULAR_VALUE = 'SINGULAR_VALUE'; test('should get the police runner for the given police array', (done) => { const myMiddleware = store => next => (action, error, response) => { return next({ ...action, myMiddlewareWasHere: true }, error, response); }; myMiddleware.applyPoint = 'onRequest'; const myAction = { type: 'REQUEST' }; const reqMiddlewares = getRequestMiddlewares([myMiddleware]); const callback = (action) => { expect(action.myMiddlewareWasHere).toBe(true); done(); return SINGULAR_VALUE; }; const result = reqMiddlewares('onRequest')({})(callback)(myAction, null, null); expect(result).toBe(SINGULAR_VALUE); }); test('should run even without middlewares registered', (done) => { const myAction = { type: 'REQUEST' } const reqMiddlewares = getRequestMiddlewares(undefined); const callback = (action) => { expect(action).toBe(myAction); done(); return SINGULAR_VALUE; }; const result = reqMiddlewares('onRequest')({})(callback)(myAction, null, null); expect(result).toBe(SINGULAR_VALUE); }); test('Should throw when trying to use a invalid middleware', () => { const invalidMiddleware = {}; expect( () => getRequestMiddlewares(['invalidMiddlewareString', invalidMiddleware]) ).toThrowError( `All middlewares should be functions: [invalidMiddlewareString,[object Object]]` ); }); test('Should run the middlewares in the given order', (done) => { const appendB = store => next => (action, error, response) => next({ ...action, payload: action.payload + 'B' }, error, response); appendB.applyPoint = 'onRequest'; const appendC = store => next => (action, error, response) => next({ ...action, payload: action.payload + 'C' }, error, response); appendC.applyPoint = 'onRequest'; const appendD = store => next => (action, error, response) => next({ ...action, payload: action.payload + 'D' }, error, response); appendD.applyPoint = 'onRequest'; const myAction = { type: 'REQUEST', payload: 'A' }; const runner = getRequestMiddlewares([appendB, appendC, appendD]); const callback = (action) => { expect(action.payload).toBe('ABCD'); done(); return action.payload; }; const result = runner('onRequest')({})(callback)(myAction, null, null); expect(result).toBe('ABCD'); }) }); ================================================ FILE: test/toExternalTypes.spec.js ================================================ import toExternalTypes from '../src/toExternalTypes'; const baseConfig = { list: { url: 'test' }, listWithUrlFunction: { url: 'test' }, read: { url: 'test' }, readWithExtras: { url: 'test' }, fsaAction: null, secondFsaAction: {}, } describe('toExternalTypes', () => { const reducedTypes = toExternalTypes(baseConfig, { list: 'MY_LIST', listWithUrlFunction: 'MY_LIST_WITH_URL_FUNCTION', read: 'MY_READ', readWithExtras: 'MY_READ_WITH_EXTRAS', fsaAction: 'MY_FSA_ACTION', secondFsaAction: 'MY_SECOND_FSA_ACTION', }); it('should return the types formatted in async types, to be used by reducers', () => { expect(reducedTypes).toEqual({ LIST: { REQUEST: 'MY_LIST_REQUEST', RESPONSE: 'MY_LIST_RESPONSE', }, LIST_WITH_URL_FUNCTION: { REQUEST: 'MY_LIST_WITH_URL_FUNCTION_REQUEST', RESPONSE: 'MY_LIST_WITH_URL_FUNCTION_RESPONSE', }, READ: { REQUEST: 'MY_READ_REQUEST', RESPONSE: 'MY_READ_RESPONSE', }, READ_WITH_EXTRAS: { REQUEST: 'MY_READ_WITH_EXTRAS_REQUEST', RESPONSE: 'MY_READ_WITH_EXTRAS_RESPONSE', }, FSA_ACTION: 'MY_FSA_ACTION', SECOND_FSA_ACTION: 'MY_SECOND_FSA_ACTION', }); }) }); ================================================ FILE: test/validateConfig.spec.js ================================================ import validateConfig from '../src/validateConfig'; describe('validateConfig', () => { it('throws when provide a wrong url', () => { expect(() => validateConfig('my', { list: { url: 1 } })).toThrow(); }); it('throws when not provide a method', () => { expect(() => validateConfig('my', { list: { url: 'path/:id' } })).toThrow(); }); it('throws when modifier is not a function', () => { expect(() => validateConfig('my', { list: { url: 'path/:id', method: 'save', modifier: {} } }) ).toThrow(); }); it('throws when namespace is not a string', () => { expect(() => validateConfig(1, { list: { url: 'path/:id', method: 'save', modifier: '' } }) ).toThrow(); }); it('should not throw when provide a valid config', () => { expect(() => validateConfig('my', { list: { url: 'path/:id', method: 'save', modifier: () => {} } }) ).not.toThrow(); }); });