Example 1: Activities Viewer

Example 1: Activities View

In this section we will develop a simple react+redux CRM Web Resource that is read-only. We will find that there are a few things we need to do to combine CRM and react+redux. There are clearly a wide range of ways you may need to integrate the frameworks together and we cannot cover them all in this book.

The extension will be targeted for an entity form and will show all of the Activities and Annotations related to that entity in more easily readable interface. You would need such a component if you have a system that is highly dependent on capturing simple notes and where OneNote may be bit overkill for many things.

Setup

Our build environment:

  • webpack

  • babel

  • eslint

We will use several javascript libraries including (partial list below):

  • react: view ui framework

  • redux: state management

  • react-redux: react state management

  • immutable-helper: Utility belt, we will mix in some lenses from ramda for variety

  • ramda: Functional programming utility belt

  • office-ui-fabric-react: Component library.

  • lunr.js/elasticlunr.js: local client search

  • recompose: Create components using function composition.

  • reselect: Create memoized selectors from the state to reduce rerender.

  • redux-saga: redux middleware for processing multi-step data processing.

  • redux-thunk: redux middleware, Promise-based processing. Overlaps with redux-saga.

The UI Extension

Our target extension will be used on the main entity forms, such as the Contact form, and list all of the Annotations (notes) and Activities related to that Contact in more easily browseable fashion.

...show finished picture here...

Extensibility

The viewer needs to be extensible. We should be able to:

  • Manage the set of data sources used to obtain data.

  • Add new menu options, new UI components that affect the list shown.

  • Customize the display of the detail for different items.

Extensibility is hard. Many people call these plugins, but plugins is probably too grand of a word and often imply some type of API conformance. At some point, you must conform to an API somewhere, but we would like to minimize that.

Instead, we will provide extensibility through composition of smaller parts. Although plugins make it easier to identify extension points, composition makes it easier and more flexible to extend. We will claim that the only API that needs to be really honored is the target data model. If you add a new data source for example, you need to add something that translates the returned data to a data model that the viewer minimally understands.

Data

Since we are using redux-saga for async data fetching, we can design a simple data source exetnsibility mechanism by assuming that each data source is a saga. We will need to pass a few "sagas" as callbacks that add the data to the store or indicates an error occurred. Each data source should be named so that we can track fetching by data source "name."

redux-sagas rely on saga middleware. A saga is nothing more than a sequence of redux actions expressed using javascript generators. If you think of a single data source's process, it looks something like the following. Notice that instead of callbacks via then we can write imperative looking code:

dispatchActionToSignalStartOfFetch(name)
try {
  const newItems = fetch(...) // async fetch via Promise
  dispatchActionToSignalNewItems(items, name)
} catch(e) {
  dispatchActionToSignalFailure(e, name)
}

Each time we need to "refresh" the data, this type of pattern can be used for every data source. If we use sagas for the signals instead of individual actions we have:

function *() {
  yield start(name)
  try {
    const items = fetch(...)
    yield receive(items, name)
  } catch(e) {
    yield error(e, name)
}}

So as long as our framework provides the generators behind start, receive and error, we have a way to create data sources easily. Our framework just needs to provide a way to ensure they are run at the start of our view. We can create a middleware.js that exports the middleware needed in redux's createStorefor the application and a start function that must be called.

const sagaMiddleware = createSagaMiddleware()

// Add to createStore(.., applyMiddleware(...middleware))
export const middleware = [ sagaMiddleware ]

// Call when you create your view
export function start({sources}) {
  sagaMiddleware.run(rootSaga(sources))
}
// Use if you want all default data sources
export defaultDataSources = [...]
...

At the application level, middleware would be added to the createStore redux call and we would require start([data sources]) to be called with the extended data sources. We could also create a non-rendering react component that adds a data source upon instantiation but a function API is good enough for now. Note that since we will store the dataSources as part of the state of the application, you can add and remove dataSources as the app is running and as long as our fetching manager allows the list to change, dataSources can also be dynamic. If you use redux-devtools make sure you add "sources: true" to the options when you create your compose function so that redux-devtools does not lose your "function state" when it serializes:

// In your top most view file

let composeEnhancers = compose
if(process.env.NODE_ENV !== "production") {
    if(parent.window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) {
        composeEnhancers =
            parent.window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({serialize: true})
    }
}

const store createStore(combineReducers({...reducers}),
     composeEnhancers(applyMiddleware(...middlewares)))

start()

Each dataSource will fetch some items but they will need to be processed (think pipeline) before use. The overall process is: fetch => enhance/prep => sort/filter (client side). Once this pipeline is complete, items are ready for use in the component. Since we are using redux, each step roughly corresponds to a different state and our state needs to store the data needed:

const initialDataState = {
  dataSources: [],
  fetchStatus: {}, // keys are data source names
  buffer: [], // buffer of fetched and enhanced items
  enhancers: [], // item => Promise(item)
  processors: [], // [items] => [items]
  items: [] // component items to be displayed
}

Each fetch status contains a buffer to hold its dataSource specific items. The individual dataSource buffers are combined and enhanced then placed into the main buffer array in the state. Then the buffer is processed (think sorting and filtering) and the result is the "activities" array for display in the component. The enhancers return an item wrapped in a Promise (an effect) as the enhancement process may require asynchronous processing.

Last updated