mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
commit
d08c44e3a1
7 changed files with 422 additions and 3 deletions
|
|
@ -8,5 +8,6 @@ import { reducer as form } from 'redux-form';
|
|||
import Auth from './Auth.duck';
|
||||
import FlashNotification from './FlashNotification.duck';
|
||||
import LocationFilter from './LocationFilter.duck';
|
||||
import sdkReducer from './sdk.duck';
|
||||
|
||||
export { form, Auth, FlashNotification, LocationFilter };
|
||||
export { form, Auth, FlashNotification, LocationFilter, sdkReducer as data };
|
||||
|
|
|
|||
172
src/ducks/sdk.duck.js
Normal file
172
src/ducks/sdk.duck.js
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
/* eslint-disable no-constant-condition, no-console */
|
||||
import { call, put, take, fork } from 'redux-saga/effects';
|
||||
import { updatedEntities } from '../util/data';
|
||||
|
||||
const requestAction = actionType => params => ({ type: actionType, payload: { params } });
|
||||
|
||||
const successAction = actionType => data => ({ type: actionType, payload: data });
|
||||
|
||||
const errorAction = actionType => error => ({ type: actionType, payload: error, error: true });
|
||||
|
||||
// ================ Action types ================ //
|
||||
|
||||
export const SHOW_LISTINGS_REQUEST = 'app/sdk/SHOW_LISTINGS_REQUEST';
|
||||
export const SHOW_LISTINGS_SUCCESS = 'app/sdk/SHOW_LISTINGS_SUCCESS';
|
||||
export const SHOW_LISTINGS_ERROR = 'app/sdk/SHOW_LISTINGS_ERROR';
|
||||
|
||||
export const QUERY_LISTINGS_REQUEST = 'app/sdk/QUERY_LISTINGS_REQUEST';
|
||||
export const QUERY_LISTINGS_SUCCESS = 'app/sdk/QUERY_LISTINGS_SUCCESS';
|
||||
export const QUERY_LISTINGS_ERROR = 'app/sdk/QUERY_LISTINGS_ERROR';
|
||||
|
||||
export const SEARCH_LISTINGS_REQUEST = 'app/sdk/SEARCH_LISTINGS_REQUEST';
|
||||
export const SEARCH_LISTINGS_SUCCESS = 'app/sdk/SEARCH_LISTINGS_SUCCESS';
|
||||
export const SEARCH_LISTINGS_ERROR = 'app/sdk/SEARCH_LISTINGS_ERROR';
|
||||
|
||||
export const SHOW_MARKETPLACE_REQUEST = 'app/sdk/SHOW_MARKETPLACE_REQUEST';
|
||||
export const SHOW_MARKETPLACE_SUCCESS = 'app/sdk/SHOW_MARKETPLACE_SUCCESS';
|
||||
export const SHOW_MARKETPLACE_ERROR = 'app/sdk/SHOW_MARKETPLACE_ERROR';
|
||||
|
||||
export const SHOW_USERS_REQUEST = 'app/sdk/SHOW_USERS_REQUEST';
|
||||
export const SHOW_USERS_SUCCESS = 'app/sdk/SHOW_USERS_SUCCESS';
|
||||
export const SHOW_USERS_ERROR = 'app/sdk/SHOW_USERS_ERROR';
|
||||
|
||||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = {
|
||||
// Error instance placeholders for each endpoint
|
||||
showListingsError: null,
|
||||
queryListingsError: null,
|
||||
searchListingsError: null,
|
||||
showMarketplaceError: null,
|
||||
showUsersError: null,
|
||||
// Database of all the fetched entities.
|
||||
entities: {},
|
||||
};
|
||||
|
||||
const merge = (state, payload) => {
|
||||
const entities = updatedEntities(state.entities, payload.data);
|
||||
return { ...state, entities };
|
||||
};
|
||||
|
||||
export default function sdkReducer(state = initialState, action = {}) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case SHOW_LISTINGS_REQUEST:
|
||||
return { ...state, showListingsError: null };
|
||||
case SHOW_LISTINGS_SUCCESS:
|
||||
return merge(state, payload);
|
||||
case SHOW_LISTINGS_ERROR:
|
||||
console.error(payload);
|
||||
return { ...state, showListingsError: payload };
|
||||
|
||||
case QUERY_LISTINGS_REQUEST:
|
||||
return { ...state, queryListingsError: null };
|
||||
case QUERY_LISTINGS_SUCCESS:
|
||||
return merge(state, payload);
|
||||
case QUERY_LISTINGS_ERROR:
|
||||
console.error(payload);
|
||||
return { ...state, queryListingsError: payload };
|
||||
|
||||
case SEARCH_LISTINGS_REQUEST:
|
||||
return { ...state, searchListingsError: null };
|
||||
case SEARCH_LISTINGS_SUCCESS:
|
||||
return merge(state, payload);
|
||||
case SEARCH_LISTINGS_ERROR:
|
||||
console.error(payload);
|
||||
return { ...state, searchListingsError: payload };
|
||||
|
||||
case SHOW_MARKETPLACE_REQUEST:
|
||||
return { ...state, showMarketplaceError: null };
|
||||
case SHOW_MARKETPLACE_SUCCESS:
|
||||
return merge(state, payload);
|
||||
case SHOW_MARKETPLACE_ERROR:
|
||||
console.error(payload);
|
||||
return { ...state, showMarketplaceError: payload };
|
||||
|
||||
case SHOW_USERS_REQUEST:
|
||||
return { ...state, showUsersError: null };
|
||||
case SHOW_USERS_SUCCESS:
|
||||
return merge(state, payload);
|
||||
case SHOW_USERS_ERROR:
|
||||
console.error(payload);
|
||||
return { ...state, showUsersError: payload };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// ================ Action creators ================ //
|
||||
|
||||
export const showListings = requestAction(SHOW_LISTINGS_REQUEST);
|
||||
export const showListingsSuccess = successAction(SHOW_LISTINGS_SUCCESS);
|
||||
export const showListingsError = errorAction(SHOW_LISTINGS_ERROR);
|
||||
|
||||
export const queryListings = requestAction(QUERY_LISTINGS_REQUEST);
|
||||
export const queryListingsSuccess = successAction(QUERY_LISTINGS_SUCCESS);
|
||||
export const queryListingsError = errorAction(QUERY_LISTINGS_ERROR);
|
||||
|
||||
export const searchListings = requestAction(SEARCH_LISTINGS_REQUEST);
|
||||
export const searchListingsSuccess = successAction(SEARCH_LISTINGS_SUCCESS);
|
||||
export const searchListingsError = errorAction(SEARCH_LISTINGS_ERROR);
|
||||
|
||||
export const showMarketplace = requestAction(SHOW_MARKETPLACE_REQUEST);
|
||||
export const showMarketplaceSuccess = successAction(SHOW_MARKETPLACE_SUCCESS);
|
||||
export const showMarketplaceError = errorAction(SHOW_MARKETPLACE_ERROR);
|
||||
|
||||
export const showUsers = requestAction(SHOW_USERS_REQUEST);
|
||||
export const showUsersSuccess = successAction(SHOW_USERS_SUCCESS);
|
||||
export const showUsersError = errorAction(SHOW_USERS_ERROR);
|
||||
|
||||
// ================ Worker sagas ================ //
|
||||
|
||||
function* callEndpoint(sdkMethod, success, error, action) {
|
||||
const params = action.payload.params;
|
||||
try {
|
||||
const response = yield call(sdkMethod, params);
|
||||
yield put(success(response));
|
||||
} catch (e) {
|
||||
yield put(error(e));
|
||||
}
|
||||
}
|
||||
|
||||
// ================ Watcher sagas ================ //
|
||||
|
||||
// TODO: Think about structuring this file to avoid repetition without
|
||||
// too much metaprogramming that makes the code harder to understand.
|
||||
const endpoints = {
|
||||
[SHOW_LISTINGS_REQUEST]: {
|
||||
method: sdk => sdk.listings.show,
|
||||
success: showListingsSuccess,
|
||||
error: showListingsError,
|
||||
},
|
||||
[QUERY_LISTINGS_REQUEST]: {
|
||||
method: sdk => sdk.listings.query,
|
||||
success: queryListingsSuccess,
|
||||
error: queryListingsError,
|
||||
},
|
||||
[SEARCH_LISTINGS_REQUEST]: {
|
||||
method: sdk => sdk.listings.search,
|
||||
success: searchListingsSuccess,
|
||||
error: searchListingsError,
|
||||
},
|
||||
[SHOW_MARKETPLACE_REQUEST]: {
|
||||
method: sdk => sdk.marketplace.show,
|
||||
success: showMarketplaceSuccess,
|
||||
error: showMarketplaceError,
|
||||
},
|
||||
[SHOW_USERS_REQUEST]: {
|
||||
method: sdk => sdk.users.show,
|
||||
success: showUsersSuccess,
|
||||
error: showUsersError,
|
||||
},
|
||||
};
|
||||
|
||||
export function* watchSdk(sdk) {
|
||||
const requestTypes = Object.keys(endpoints);
|
||||
while (true) {
|
||||
const action = yield take(requestTypes);
|
||||
const { method, success, error } = endpoints[action.type];
|
||||
yield fork(callEndpoint, method(sdk), success, error, action);
|
||||
}
|
||||
}
|
||||
15
src/index.js
15
src/index.js
|
|
@ -13,12 +13,21 @@
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { createInstance, types } from 'sharetribe-sdk';
|
||||
import { ClientApp, renderApp } from './app';
|
||||
import configureStore from './store';
|
||||
import { matchPathname } from './util/routes';
|
||||
import * as sample from './util/sample';
|
||||
import createRootSaga from './sagas';
|
||||
import config from './config';
|
||||
import {
|
||||
showListings,
|
||||
queryListings,
|
||||
searchListings,
|
||||
showMarketplace,
|
||||
showUsers,
|
||||
} from './ducks/sdk.duck';
|
||||
|
||||
import './index.css';
|
||||
|
||||
|
|
@ -35,7 +44,11 @@ if (typeof window !== 'undefined') {
|
|||
|
||||
// Expose stuff for the browser REPL
|
||||
if (config.dev) {
|
||||
window.app = { config, sdk, sdkTypes: types };
|
||||
const actions = bindActionCreators(
|
||||
{ showListings, queryListings, searchListings, showMarketplace, showUsers },
|
||||
store.dispatch,
|
||||
);
|
||||
window.app = { config, sdk, sdkTypes: types, actions, store, sample };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { watchAuth } from './ducks/Auth.duck';
|
||||
import { watchLoadListings } from './containers/SearchPage/SearchPage.duck';
|
||||
import { watchSdk } from './ducks/sdk.duck';
|
||||
|
||||
const createRootSaga = sdk => function* rootSaga() {
|
||||
yield [watchAuth(sdk), watchLoadListings(sdk)];
|
||||
yield [watchAuth(sdk), watchLoadListings(sdk), watchSdk(sdk)];
|
||||
};
|
||||
|
||||
export default createRootSaga;
|
||||
|
|
|
|||
54
src/util/data.js
Normal file
54
src/util/data.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Combine the given relationships objects
|
||||
*
|
||||
* See: http://jsonapi.org/format/#document-resource-object-relationships
|
||||
*/
|
||||
export const combinedRelationships = (oldRels, newRels) => {
|
||||
if (!oldRels && !newRels) {
|
||||
// Special case to avoid adding an empty relationships object when
|
||||
// none of the resource objects had any relationships.
|
||||
return null;
|
||||
}
|
||||
return { ...oldRels, ...newRels };
|
||||
};
|
||||
|
||||
/**
|
||||
* Combine the given resource objects
|
||||
*
|
||||
* See: http://jsonapi.org/format/#document-resource-objects
|
||||
*/
|
||||
export const combinedResourceObjects = (oldRes, newRes) => {
|
||||
const { id, type } = oldRes;
|
||||
if (newRes.id.uuid !== id.uuid || newRes.type !== type) {
|
||||
throw new Error('Cannot merge resource objects with different ids or types');
|
||||
}
|
||||
const attributes = newRes.attributes || oldRes.attributes;
|
||||
const attrs = attributes ? { attributes } : null;
|
||||
const relationships = combinedRelationships(oldRes.relationships, newRes.relationships);
|
||||
const rels = relationships ? { relationships } : null;
|
||||
return { id, type, ...attrs, ...rels };
|
||||
};
|
||||
|
||||
/**
|
||||
* Combine the resource objects form the given api response to the
|
||||
* existing entities.
|
||||
*/
|
||||
export const updatedEntities = (oldEntities, apiResponse) => {
|
||||
const { data, included = [] } = apiResponse;
|
||||
const objects = (Array.isArray(data) ? data : [data]).concat(included);
|
||||
|
||||
/* eslint-disable no-param-reassign */
|
||||
const newEntities = objects.reduce(
|
||||
(entities, curr) => {
|
||||
const { id, type } = curr;
|
||||
entities[type] = entities[type] || {};
|
||||
const entity = entities[type][id.uuid];
|
||||
entities[type][id.uuid] = entity ? combinedResourceObjects(entity, curr) : curr;
|
||||
return entities;
|
||||
},
|
||||
oldEntities,
|
||||
);
|
||||
/* eslint-enable no-param-reassign */
|
||||
|
||||
return newEntities;
|
||||
};
|
||||
165
src/util/data.test.js
Normal file
165
src/util/data.test.js
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import { types } from 'sharetribe-sdk';
|
||||
import { combinedRelationships, combinedResourceObjects, updatedEntities } from './data';
|
||||
|
||||
const { UUID } = types;
|
||||
|
||||
describe('data utils', () => {
|
||||
describe('combinedRelationships()', () => {
|
||||
it('handles no rels', () => {
|
||||
expect(combinedRelationships(undefined, undefined)).toEqual(null);
|
||||
});
|
||||
|
||||
it('takes existings rels if no new rels are given', () => {
|
||||
const listingRels = {
|
||||
author: { data: { id: new UUID('author-id'), type: 'author' } },
|
||||
images: {
|
||||
data: [
|
||||
{ id: new UUID('image1'), type: 'image' },
|
||||
{ id: new UUID('image2'), type: 'image' },
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(combinedRelationships(listingRels, undefined)).toEqual(listingRels);
|
||||
});
|
||||
|
||||
it('takes new rels if no existing rels are given', () => {
|
||||
const listingRels = {
|
||||
author: { data: { id: new UUID('author-id'), type: 'author' } },
|
||||
images: { data: { id: new UUID('image1'), type: 'image' } },
|
||||
};
|
||||
expect(combinedRelationships(undefined, listingRels)).toEqual(listingRels);
|
||||
});
|
||||
|
||||
it('merges two nonempty rels', () => {
|
||||
const authorRel = { data: { id: new UUID('author-id'), type: 'author' } };
|
||||
const imagesRel = {
|
||||
data: [
|
||||
{ id: new UUID('image1'), type: 'image' },
|
||||
{ id: new UUID('image2'), type: 'image' },
|
||||
],
|
||||
};
|
||||
const newImagesRel = { data: { id: new UUID('image1'), type: 'image' } };
|
||||
const reviewsRel = {
|
||||
data: [
|
||||
{ id: new UUID('review1'), type: 'review' },
|
||||
{ id: new UUID('review2'), type: 'review' },
|
||||
],
|
||||
};
|
||||
|
||||
const oldListingRels = { author: authorRel, images: imagesRel };
|
||||
const newListingRels = { images: newImagesRel, reviews: reviewsRel };
|
||||
expect(combinedRelationships(oldListingRels, newListingRels)).toEqual({
|
||||
author: authorRel,
|
||||
images: newImagesRel,
|
||||
reviews: reviewsRel,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('combinedResourceObjects()', () => {
|
||||
it("throws if ids don't match", () => {
|
||||
const listing1 = { id: new UUID('listing1'), type: 'listing' };
|
||||
const listing2 = { id: new UUID('listing2'), type: 'listing' };
|
||||
expect(() => combinedResourceObjects(listing1, listing2)).toThrow(
|
||||
'Cannot merge resource objects with different ids or types',
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if types don't match", () => {
|
||||
const listing1 = { id: new UUID('listing1'), type: 'listing' };
|
||||
const author1 = { id: new UUID('author1'), type: 'author' };
|
||||
expect(() => combinedResourceObjects(listing1, author1)).toThrow(
|
||||
'Cannot merge resource objects with different ids or types',
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't add attributes or relationships keys unnecessarily", () => {
|
||||
const res1 = { id: new UUID('listing1'), type: 'listing' };
|
||||
const res2 = { id: new UUID('listing1'), type: 'listing' };
|
||||
const merged = combinedResourceObjects(res1, res2);
|
||||
expect(merged).toEqual(res1);
|
||||
const keys = Object.keys(merged).sort();
|
||||
expect(keys).toEqual(['id', 'type']);
|
||||
});
|
||||
|
||||
it('merges new attributes', () => {
|
||||
const res1 = {
|
||||
id: new UUID('listing1'),
|
||||
type: 'listing',
|
||||
attributes: { title: 'Listing 1 title', description: 'Listing 1 description' },
|
||||
};
|
||||
const res2 = {
|
||||
id: new UUID('listing1'),
|
||||
type: 'listing',
|
||||
attributes: { title: 'Changed title', description: 'Some description' },
|
||||
};
|
||||
expect(combinedResourceObjects(res1, res2)).toEqual(res2);
|
||||
});
|
||||
|
||||
it("keeps old attributes if new does't have any", () => {
|
||||
const res1Attributes = { title: 'Listing 1 title', description: 'Listing 1 description' };
|
||||
const res2Relationships = { author: { data: { id: new UUID('author1'), type: 'author' } } };
|
||||
|
||||
const res1 = { id: new UUID('listing1'), type: 'listing', attributes: res1Attributes };
|
||||
const res2 = { id: new UUID('listing1'), type: 'listing', relationships: res2Relationships };
|
||||
expect(combinedResourceObjects(res1, res2)).toEqual({
|
||||
id: new UUID('listing1'),
|
||||
type: 'listing',
|
||||
attributes: res1Attributes,
|
||||
relationships: res2Relationships,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatedEntities()', () => {
|
||||
it('adds a single entity', () => {
|
||||
const listing1 = { id: new UUID('listing1'), type: 'listing' };
|
||||
const response = { data: listing1 };
|
||||
const entities = updatedEntities({}, response);
|
||||
expect(entities).toEqual({ listing: { listing1 } });
|
||||
});
|
||||
|
||||
it('add multiple entities', () => {
|
||||
const listing1 = { id: new UUID('listing1'), type: 'listing' };
|
||||
const listing2 = { id: new UUID('listing2'), type: 'listing' };
|
||||
const response = { data: [listing1, listing2] };
|
||||
const entities = updatedEntities({}, response);
|
||||
expect(entities).toEqual({ listing: { listing1, listing2 } });
|
||||
});
|
||||
|
||||
it('handles a more complex merge', () => {
|
||||
const listing1 = {
|
||||
id: new UUID('listing1'),
|
||||
type: 'listing',
|
||||
attributes: { title: 'Listing 1 title', description: 'Listing 1 description' },
|
||||
};
|
||||
const listing2 = {
|
||||
id: new UUID('listing2'),
|
||||
type: 'listing',
|
||||
attributes: { title: 'Listing 2 title', description: 'Listing 2 description' },
|
||||
};
|
||||
const initialResponse = { data: [listing1, listing2] };
|
||||
const initialEntities = updatedEntities({}, initialResponse);
|
||||
expect(initialEntities).toEqual({ listing: { listing1, listing2 } });
|
||||
const author1 = { id: new UUID('author1'), type: 'author' };
|
||||
const author1WithAttributes = {
|
||||
id: new UUID('author1'),
|
||||
type: 'author',
|
||||
attributes: { name: 'Author 1 name' },
|
||||
};
|
||||
const listing2Updated = {
|
||||
id: new UUID('listing2'),
|
||||
type: 'listing',
|
||||
attributes: { title: 'New listing 2 title', description: 'new listing 2 description' },
|
||||
relationships: { author: { data: author1 } },
|
||||
};
|
||||
const included = [author1WithAttributes];
|
||||
const response = { data: [listing2Updated], included };
|
||||
const entities = updatedEntities(initialEntities, response, true);
|
||||
expect(entities).toEqual({
|
||||
listing: { listing1, listing2: listing2Updated },
|
||||
author: { author1: author1WithAttributes },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
13
src/util/sample.js
Normal file
13
src/util/sample.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { types } from 'sharetribe-sdk';
|
||||
|
||||
export const marketplaceId = new types.UUID('16c6a4b8-88ee-429b-835a-6725206cd08c');
|
||||
export const userJoeId = new types.UUID('3c073fae-6172-4e75-8b92-f560d58cd47c');
|
||||
export const userJaneId = new types.UUID('7b98dd96-74c7-4ddc-9f46-38c0f91c4a19');
|
||||
export const listing1Id = new types.UUID('c6ff7190-bdf7-47a0-8a2b-e3136e74334f');
|
||||
export const listing2Id = new types.UUID('9009efe1-25ec-4ed5-9413-e80c584ff6bf');
|
||||
export const listing3Id = new types.UUID('8918693a-7a58-4d46-8324-f7996ef7579b');
|
||||
export const listing4Id = new types.UUID('5e1f2086-522c-46f3-87b4-451c6770c833');
|
||||
export const listing5Id = new types.UUID('f5130a7f-4b8b-453b-98e5-78e38ad02c3f');
|
||||
export const listing6Id = new types.UUID('927a30a2-3a69-4b0d-9c2e-a41744488703');
|
||||
export const image1Id = new types.UUID('4617c584-edfd-49e9-be43-2f1d50fb6e35');
|
||||
export const image2Id = new types.UUID('948b1926-67f9-402a-a7c7-f8ff3090c249');
|
||||
Loading…
Add table
Reference in a new issue