Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use fresh data for transactions (#139)
* Add reducer with some basic actions The reducer does define some actions, but never produces a different state. * Add function to initalize the data store for fresh-data * Add function to initialize the fresh-data API client * Add empty Payments API spec This will be filled in as we go, but can remain empty for now since we're not actually calling any Payments endpoints for data. * Add dependencies to package.json * Add function to initialize the Payments data store API * Include the fresh-data Payments data store in web app * Refactor TransactionsList to React Component class This is necessary to use the component with fresh-data. Currently the component always shows an empty table. * Add withSelect function This is used to wrap components before exporting them, and allows components to access the fresh-data data store. * Wrap TransactionsList export with `withSelect` This will allow us to access the fresh-data data store. * Add hard coded test data to transactions fresh-data selector The selector really shouldn't be using hard coded data, but this is for testing! All for the greater good, I'm sure. * Add transactions selector to the Payments API spec * Retrieve data to display in TransactionsList using transactions selector By retrieving the relevant selectors we can access the data from the fresh-data data store, and use that to populate the transactions table. As far as I understand this (@coderkevin would be a better person to ask) the `withSelect` method basically injects data from the selector into the component's `this.props`, allowing us to use the data in the component. * Update package-lock.json * Fix wrong parameter name in `createApiClient` * Add reducer for `fresh-data` * Add lodash to dependencies * Add read operation for transactions list This operation defines how requests are sent for `fresh-data` transactions resources, i.e. how the transactions list is retrieved. * Expose the transactions list 'read' operation to `fresh-data` * Add transactions list selector The selector can be used to retrieve transactions list for components. This commit also adapts the code to fit the new selector better. * Add code to determine if the table loading view should be displayed * Fix `getTransactionsLoading` function Incorrectly accessing the resource object caused the timestamps to be undefined when evaluating whether the transactions request is loading or not. * Update fresh-data version in package.json * Change loading status selector to not specify requirements * Export transactions operations to improve testability * Remove unnecessary setup code from fresh-data data store initialization * Add packages necessary to run tests For some reason jest can't find '@wordpress/api-fetch' unless it's installed. Even though the test doesn't actually _use_ api-fetch, it complains because one of the included files imports api-fetch. * Fix transactions operations * Add transactions operation tests * Add transactions selector tests * Merge changes to transactions-list.js from master * Add `showTransactionsPlaceholder` selector Used to indicate whether the `TableCard` in the transactions list should show the loading placeholder view or not. * Fix `...` (spread operator) missing error. Babel seems to need a specific package to support the `...` operator for now. See: babel/babel#10179 (comment) npm i --save-dev @babel/plugin-proposal-object-rest-spread is sufficient as a fix.
- Loading branch information
Showing
18 changed files
with
1,009 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/** @format */ | ||
|
||
/** | ||
* Internal dependencies. | ||
*/ | ||
import transactions from './transactions'; | ||
|
||
function createPaymentsApiSpec() { | ||
return { | ||
name: 'wcPaymentsApi', | ||
mutations: {}, | ||
selectors: { | ||
...transactions.selectors, | ||
}, | ||
operations: { | ||
read( resourceNames ) { | ||
return [ | ||
...transactions.operations.read( resourceNames ), | ||
]; | ||
}, | ||
update( resourceNames, data ) { | ||
return []; | ||
}, | ||
updateLocally( resourceNames, data ) { | ||
return []; | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
export default createPaymentsApiSpec(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/** @format */ | ||
|
||
/** | ||
* Internal dependencies. | ||
*/ | ||
import selectors from './selectors'; | ||
import operations from './operations'; | ||
|
||
export default { | ||
selectors, | ||
operations, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/** @format */ | ||
|
||
/** | ||
* External dependencies. | ||
*/ | ||
import apiFetch from '@wordpress/api-fetch'; | ||
import { includes } from 'lodash'; | ||
|
||
/** | ||
* Internal dependencies. | ||
*/ | ||
import { NAMESPACE } from '../../constants'; | ||
|
||
function read( resourceNames, fetch = apiFetch, dataToResources = transactionsToResources ) { | ||
return readTransactions( resourceNames, fetch, dataToResources ); | ||
} | ||
|
||
export function readTransactions( resourceNames, fetch, dataToResources ) { | ||
if ( includes( resourceNames, 'transactions-list' ) ) { | ||
const url = `${ NAMESPACE }/payments/transactions`; | ||
|
||
return [ | ||
fetch( { path: url } ) | ||
.then( dataToResources ) | ||
.catch( error => { | ||
return { [ resourceName ]: { error } }; | ||
} ) | ||
]; | ||
} | ||
|
||
return []; | ||
} | ||
|
||
export function transactionsToResources( transactions ) { | ||
return { | ||
[ 'transactions-list' ]: { | ||
data: transactions, | ||
} | ||
}; | ||
} | ||
|
||
export default { | ||
read, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/** @format */ | ||
|
||
/** | ||
* External dependencies. | ||
*/ | ||
import { isNil } from 'lodash'; | ||
|
||
/** | ||
* Internal dependencies. | ||
*/ | ||
import { DEFAULT_REQUIREMENT } from '../../constants'; | ||
|
||
const getTransactions = ( getResource, requireResource ) => ( | ||
requirement = DEFAULT_REQUIREMENT | ||
) => { | ||
return requireResource( requirement, 'transactions-list' ).data || {}; | ||
} | ||
|
||
const isWaitingForInitialLoad = ( getResource ) => () => { | ||
const resourceName = 'transactions-list'; | ||
const transactionsResource = getResource( resourceName ); | ||
|
||
return transactionsResource.lastReceived === undefined; | ||
} | ||
|
||
const getTransactionsIsLoading = ( getResource ) => () => { | ||
const resourceName = 'transactions-list'; | ||
const transactionsResource = getResource( resourceName ); | ||
|
||
return transactionsResource.lastRequested > transactionsResource.lastReceived; | ||
} | ||
|
||
const showTransactionsPlaceholder = ( getResource ) => () => { | ||
return isWaitingForInitialLoad( getResource )(); | ||
} | ||
|
||
export default { | ||
getTransactions, | ||
getTransactionsIsLoading, | ||
isWaitingForInitialLoad, | ||
showTransactionsPlaceholder, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
|
||
/** | ||
* External dependencies. | ||
*/ | ||
import { MINUTE } from '@fresh-data/framework'; | ||
|
||
export const DEFAULT_REQUIREMENT = { | ||
timeout: 1 * MINUTE, | ||
freshness: 30 * MINUTE, | ||
}; | ||
|
||
export const NAMESPACE = '/wc/v3'; |
45 changes: 45 additions & 0 deletions
45
client/payments-api/payments-data-store/create-api-client.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
|
||
/** | ||
* External dependencies. | ||
*/ | ||
import { ApiClient } from '@fresh-data/framework'; | ||
|
||
/** | ||
* Internal dependencies. | ||
*/ | ||
import createStore from './create-store'; | ||
|
||
function createDataHandlers( store ) { | ||
return { | ||
dataRequested: resourceNames => { | ||
store.dispatch( { | ||
type: 'FRESH_DATA_REQUESTED', | ||
resourceNames, | ||
time: new Date(), | ||
} ); | ||
}, | ||
dataReceived: resources => { | ||
store.dispatch( { | ||
type: 'FRESH_DATA_RECEIVED', | ||
resources, | ||
time: new Date(), | ||
} ); | ||
}, | ||
}; | ||
} | ||
|
||
function createApiClient( name, apiSpec ) { | ||
const store = createStore( name ); | ||
const dataHandlers = createDataHandlers( store ); | ||
const apiClient = new ApiClient( apiSpec ); | ||
apiClient.setDataHandlers( dataHandlers ); | ||
|
||
const storeChanged = () => { | ||
apiClient.setState( store.getState() ); | ||
} | ||
store.subscribe( storeChanged ); | ||
|
||
return apiClient; | ||
} | ||
|
||
export default createApiClient; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
|
||
/** | ||
* External dependencies. | ||
*/ | ||
import { createStore } from 'redux'; | ||
|
||
/** | ||
* Internal dependencies. | ||
*/ | ||
import reducer from './reducer'; | ||
|
||
export default name => { | ||
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__; | ||
|
||
return createStore( reducer, devTools && devTools( { name: name, instanceId: name } ) ); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
|
||
/** | ||
* External dependencies. | ||
*/ | ||
import { registerGenericStore } from '@wordpress/data'; | ||
|
||
/** | ||
* Internal dependencies. | ||
*/ | ||
import createApiClient from './create-api-client'; | ||
import paymentsApiSpec from '../api-spec/payments-rest-api'; | ||
|
||
if ( 'development' === process.env.NODE_ENV ) { | ||
window.__FRESH_DATA_DEV_INFO__ = true; | ||
} | ||
|
||
function createPaymentsApiStore() { | ||
const apiClient = createApiClient( 'wc-payments-api', paymentsApiSpec ); | ||
|
||
return { | ||
getSelectors: () => { | ||
return apiClient.getSelectors(); | ||
}, | ||
getActions: () => { | ||
return apiClient.getMutations(); | ||
}, | ||
subscribe: apiClient.subscribe, | ||
}; | ||
} | ||
|
||
registerGenericStore( 'wc-payments-api', createPaymentsApiStore() ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
|
||
const defaultState = { | ||
resources: {}, | ||
}; | ||
|
||
export default function reducer( state = defaultState, action ) { | ||
switch ( action.type ) { | ||
case 'FRESH_DATA_REQUESTED': | ||
return reduceRequested( state, action ); | ||
case 'FRESH_DATA_RECEIVED': | ||
return reduceReceived( state, action ); | ||
default: | ||
return state; | ||
} | ||
} | ||
|
||
export function reduceRequested( state, action ) { | ||
const newResources = action.resourceNames.reduce( ( resources, name ) => { | ||
resources[ name ] = { lastRequested: action.time }; | ||
return resources; | ||
}, {} ); | ||
return reduceResources( state, newResources ); | ||
} | ||
|
||
export function reduceReceived( state, action ) { | ||
const newResources = Object.keys( action.resources ).reduce( ( resources, name ) => { | ||
const resource = { ...action.resources[ name ] }; | ||
if ( resource.data ) { | ||
resource.lastReceived = action.time; | ||
} | ||
if ( resource.error ) { | ||
resource.lastError = action.time; | ||
} | ||
resources[ name ] = resource; | ||
return resources; | ||
}, {} ); | ||
return reduceResources( state, newResources ); | ||
} | ||
|
||
export function reduceResources( state, newResources ) { | ||
const updatedResources = Object.keys( newResources ).reduce( | ||
( resources, resourceName ) => { | ||
const resource = resources[ resourceName ]; | ||
const newResource = newResources[ resourceName ]; | ||
resources[ resourceName ] = { ...resource, ...newResource }; | ||
return resources; | ||
}, | ||
{ ...state.resources } | ||
); | ||
|
||
return { ...state, resources: updatedResources }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/** @format */ | ||
import './operations'; | ||
import './selectors'; |
69 changes: 69 additions & 0 deletions
69
client/payments-api/test/api-spec/transactions/operations.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/** @format */ | ||
|
||
/** | ||
* Internal dependencies. | ||
*/ | ||
import { readTransactions, transactionsToResources } from '../../../api-spec/transactions/operations'; | ||
import { NAMESPACE } from '../../../constants'; | ||
|
||
describe( 'Transactions operations', () => { | ||
describe( 'readTransactions()', () => { | ||
const expectedUrl = `${ NAMESPACE }/payments/transactions`; | ||
|
||
it( 'Returns a list with one promise when correct resource names are supplied', () => { | ||
const mockData = [ {}, {}, {} ]; | ||
const expectedResolvedPromise = { | ||
[ "transactions-list" ]: { | ||
data: mockData, | ||
}, | ||
}; | ||
|
||
const mockToResources = jest.fn(); | ||
mockToResources.mockReturnValue( expectedResolvedPromise ); | ||
|
||
const mockPromise = new Promise( () => mockData, () => {} ); | ||
const expectedPromises = [ mockPromise ]; | ||
|
||
const mockFetch = jest.fn(); | ||
mockFetch.mockReturnValue( mockPromise ); | ||
|
||
// Perform read operation. | ||
const promises = readTransactions( [ 'transactions-list' ], mockFetch, mockToResources ); | ||
|
||
expect( mockFetch ).toHaveBeenCalledTimes( 1 ); | ||
expect( mockFetch ).toHaveBeenCalledWith( { path: expectedUrl } ); | ||
expect( promises ).toStrictEqual( expectedPromises ); | ||
promises[0].then( result => { | ||
expect( mockToResources ).toHaveBeenCalledTimes( 1 ); | ||
expect( mockToResources ).toHaveBeenCalledWith( mockData ); | ||
expect( result ).toBe( expectedResolvedPromise ); | ||
} ); | ||
} ); | ||
|
||
it( 'Returns an empty list when wrong resource names are supplied', () => { | ||
const expected = []; | ||
|
||
const mockFetch = jest.fn(); | ||
|
||
// Perform read operation. | ||
const promises = readTransactions( [ 'wrong', 'resource', 'names' ] ); | ||
|
||
expect( mockFetch ).not.toHaveBeenCalled(); | ||
expect( promises ).toStrictEqual( expected ); | ||
} ); | ||
} ); | ||
|
||
describe( 'transactionsToResources()', () => { | ||
it( 'Transactions list is correctly converted to resources', () => { | ||
const mockData = [ {}, {}, {} ]; | ||
const expected = { | ||
[ 'transactions-list' ]: { | ||
data: mockData, | ||
}, | ||
}; | ||
|
||
const resources = transactionsToResources( mockData ); | ||
expect( resources ).toStrictEqual( expected ); | ||
} ); | ||
} ); | ||
} ); |
Oops, something went wrong.