├── connect.js ├── types.js ├── assets └── tophat.png ├── .gitignore ├── lerna.json ├── packages ├── kasia-plugin-wp-api-menus │ ├── .babelrc │ ├── .gitignore │ ├── .npmignore │ ├── src │ │ ├── constants │ │ │ └── ActionTypes.js │ │ ├── actions.js │ │ └── index.js │ ├── CHANGELOG.md │ ├── .editorconfig │ ├── LICENSE │ ├── package.json │ └── README.md ├── kasia-plugin-wp-api-all-terms │ ├── .babelrc │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── .editorconfig │ ├── LICENSE │ ├── src │ │ └── index.js │ ├── package.json │ └── README.md └── kasia-plugin-wp-api-response-modify │ ├── .gitignore │ ├── CHANGELOG.md │ ├── .editorconfig │ ├── index.js │ ├── package.json │ ├── LICENSE │ └── README.md ├── .npmignore ├── test ├── __mocks__ │ ├── states │ │ ├── initial.js │ │ ├── multipleBooks.js │ │ └── multipleEntities.js │ ├── WP.js │ └── components │ │ ├── BadContentType.js │ │ ├── ExplicitIdentifier.js │ │ ├── CustomContentType.js │ │ ├── CustomQuery.js │ │ ├── BuiltInContentType.js │ │ └── CustomQueryNestedPreload.js ├── __fixtures__ │ └── wp-api-responses │ │ ├── status.js │ │ ├── type.js │ │ ├── taxonomy.js │ │ ├── tag.js │ │ ├── category.js │ │ ├── user.js │ │ ├── comment.js │ │ ├── book.js │ │ ├── media.js │ │ ├── page.js │ │ └── post.js ├── util │ ├── queryBuilder.js │ ├── pickEntityIds.js │ ├── contentTypesManager.js │ ├── findEntities.js │ ├── normalise.js │ └── preload.js ├── index.js ├── redux │ ├── reducer.js │ ├── sagas.js │ └── reducer-journey.js ├── connect │ ├── preload.js │ ├── connectWpQuery.js │ └── connectWpPost.js ├── plugin.js └── universal-journey.js ├── src ├── util │ ├── debug.js │ ├── queryCounter.js │ ├── runSagas.js │ ├── pickEntityIds.js │ ├── findEntities.js │ ├── normalise.js │ ├── queryBuilder.js │ ├── preload.js │ ├── contentTypesManager.js │ └── schemasManager.js ├── wpapi.js ├── redux │ ├── actions.js │ ├── sagas.js │ └── reducer.js ├── constants.js ├── index.js ├── invariants.js └── connect.js ├── .babelrc ├── .travis.yml ├── .editorconfig ├── LICENSE ├── package.json ├── CHANGELOG.md └── README.md /connect.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/connect') 2 | -------------------------------------------------------------------------------- /types.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/constants').ContentTypes 2 | -------------------------------------------------------------------------------- /assets/tophat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outlandishideas/kasia/HEAD/assets/tophat.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | coverage/ 5 | lib/ 6 | dist/ 7 | 8 | *.log 9 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0-beta.31", 3 | "packages": [ 4 | "packages/*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-menus/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-all-terms/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-menus/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | lib/ 5 | *.log 6 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-response-modify/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | *.log 5 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-all-terms/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | lib/ 5 | *.log 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-menus/.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | coverage/ 5 | *.log 6 | 7 | !lib/ 8 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-all-terms/.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | coverage/ 5 | *.log 6 | 7 | !lib/ 8 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-all-terms/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Kasia Plugin WP API All Terms 2 | 3 | - __v0.0.1__ - _09/09/16_ 4 | 5 | - Release! :tophat: 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | coverage/ 5 | packages/ 6 | assets/ 7 | test/ 8 | src/ 9 | dist/ 10 | *.log 11 | 12 | !lib/ 13 | -------------------------------------------------------------------------------- /test/__mocks__/states/initial.js: -------------------------------------------------------------------------------- 1 | export default (keyEntitiesBy = 'id') => ({ 2 | wordpress: { 3 | keyEntitiesBy, 4 | queries: {}, 5 | entities: {} 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/__mocks__/WP.js: -------------------------------------------------------------------------------- 1 | /* global jest:false */ 2 | 3 | import { setWP } from '../../src/wpapi' 4 | 5 | export const wpapi = { 6 | registerRoute: jest.fn() 7 | } 8 | 9 | export default setWP(wpapi) 10 | -------------------------------------------------------------------------------- /src/util/debug.js: -------------------------------------------------------------------------------- 1 | let on = false 2 | 3 | export default function debug (...args) { 4 | if (on) console.log('[kasia debug]', ...args) 5 | } 6 | 7 | export function toggleDebug (bool) { 8 | on = bool 9 | } 10 | -------------------------------------------------------------------------------- /src/wpapi.js: -------------------------------------------------------------------------------- 1 | let WP 2 | 3 | export default function getWP () { 4 | if (!WP) throw new Error('WP not set') 5 | return WP 6 | } 7 | 8 | export function setWP (_WP) { 9 | WP = _WP 10 | return WP 11 | } 12 | -------------------------------------------------------------------------------- /src/util/queryCounter.js: -------------------------------------------------------------------------------- 1 | let queryId = -1 2 | 3 | export default { 4 | current () { 5 | return queryId 6 | }, 7 | reset () { 8 | queryId = -1 9 | }, 10 | next () { 11 | queryId += 1 12 | return queryId 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/__fixtures__/wp-api-responses/status.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'name': 'Published', 3 | 'public': true, 4 | 'queryable': true, 5 | 'slug': 'publish', 6 | '_links': { 7 | 'archives': [{ 8 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/posts' 9 | }] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "transform-decorators-legacy", 9 | "transform-proto-to-assign", 10 | "transform-class-properties", 11 | ["transform-es2015-classes", {"loose": true}] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | # https://github.com/travis-ci/travis-ci/issues/4653#issuecomment-194051953 5 | before_install: "if [[ `npm -v` != 4* ]]; then npm i -g npm@4; fi" 6 | script: "npm run test:coverage" 7 | after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 8 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-menus/src/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | REQUEST_MENUS: 'kasia/menus/REQUEST_MENUS', 3 | REQUEST_MENU: 'kasia/menus/REQUEST_MENU', 4 | REQUEST_LOCATIONS: 'kasia/menus/REQUEST_LOCATIONS', 5 | REQUEST_LOCATION: 'kasia/menus/REQUEST_LOCATION', 6 | RECEIVE_DATA: 'kasia/menus/RECEIVE_DATA' 7 | } 8 | -------------------------------------------------------------------------------- /test/__mocks__/components/BadContentType.js: -------------------------------------------------------------------------------- 1 | /* global jest:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import React, { Component } from 'react' 6 | 7 | import { connectWpPost } from '../../../src/connect' 8 | 9 | @connectWpPost(' :-( ', (props) => props.params.id) 10 | export default class BadContentType extends Component { 11 | render () { 12 | return
13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-menus/src/actions.js: -------------------------------------------------------------------------------- 1 | import ActionTypes from './constants/ActionTypes' 2 | 3 | export const fetchMenus = () => 4 | ({ type: ActionTypes.REQUEST_MENUS }) 5 | 6 | export const fetchMenu = (id) => 7 | ({ type: ActionTypes.REQUEST_MENU, id }) 8 | 9 | export const fetchThemeLocations = () => 10 | ({ type: ActionTypes.REQUEST_LOCATIONS }) 11 | 12 | export const fetchThemeLocation = (id) => 13 | ({ type: ActionTypes.REQUEST_LOCATION, id }) 14 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-menus/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Kasia Plugin WP API Menus 2 | 3 | - __v3.0.0__ - _05/08/16_ 4 | 5 | - [BREAKING] Updated for Kasia v3. 6 | - `makePreloader` now returns a function that can be passed straight to `runSaga` instead 7 | of an array that describes the saga operation. 8 | 9 | --- 10 | 11 | - __v2.0.0__ - _05/08/16_ 12 | 13 | - [BREAKING] Updated for Kasia v2. 14 | 15 | --- 16 | 17 | - __v1.0.0__ - _03/08/16_ 18 | 19 | - Release! :tophat: 20 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-response-modify/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Kasia Plugin WP API Menus 2 | 3 | - __v3.0.0__ - _05/08/16_ 4 | 5 | - [BREAKING] Updated for Kasia v3. 6 | - `makePreloader` now returns a function that can be passed straight to `runSaga` instead 7 | of an array that describes the saga operation. 8 | 9 | --- 10 | 11 | - __v2.0.0__ - _05/08/16_ 12 | 13 | - [BREAKING] Updated for Kasia v2. 14 | 15 | --- 16 | 17 | - __v1.0.0__ - _03/08/16_ 18 | 19 | - Release! :tophat: 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = false 18 | 19 | [*.js] 20 | 21 | insert_final_newline = true 22 | -------------------------------------------------------------------------------- /test/__fixtures__/wp-api-responses/type.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'description': '', 3 | 'hierarchical': false, 4 | 'name': 'Posts', 5 | 'slug': 'post', 6 | '_links': { 7 | 'collection': [{ 8 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/types' 9 | }], 10 | 'wp:items': [{ 11 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/posts' 12 | }], 13 | 'curies': [{ 14 | 'name': 'wp', 15 | 'href': 'https://api.w.org/{rel}', 16 | 'templated': true 17 | }] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-menus/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = false 18 | 19 | [*.js] 20 | 21 | insert_final_newline = true 22 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-all-terms/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = false 18 | 19 | [*.js] 20 | 21 | insert_final_newline = true 22 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-response-modify/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = false 18 | 19 | [*.js] 20 | 21 | insert_final_newline = true 22 | -------------------------------------------------------------------------------- /test/__fixtures__/wp-api-responses/taxonomy.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'name': 'Categories', 3 | 'slug': 'category', 4 | 'description': '', 5 | 'types': ['post'], 6 | 'hierarchical': true, 7 | '_links': { 8 | 'collection': [{ 9 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/taxonomies' 10 | }], 11 | 'wp:items': [{ 12 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/categories' 13 | }], 14 | 'curies': [{ 15 | 'name': 'wp', 16 | 'href': 'https://api.w.org/{rel}', 17 | 'templated': true 18 | }] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/__mocks__/states/multipleBooks.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge' 2 | 3 | import bookJson from '../../__fixtures__/wp-api-responses/book' 4 | 5 | export default { 6 | wordpress: { 7 | keyEntitiesBy: 'id', 8 | queries: { 9 | '0': { 10 | id: 0, 11 | complete: true, 12 | OK: true, 13 | entities: [bookJson.id, bookJson.id + 1] 14 | } 15 | }, 16 | entities: { 17 | books: { 18 | [bookJson.id]: bookJson, 19 | [bookJson.id + 1]: merge({}, bookJson, { id: bookJson.id + 1, slug: 'new-slug' }) 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/__mocks__/states/multipleEntities.js: -------------------------------------------------------------------------------- 1 | import postJson from '../../__fixtures__/wp-api-responses/post' 2 | import bookJson from '../../__fixtures__/wp-api-responses/book' 3 | 4 | const post2 = Object.assign({}, postJson, { 5 | id: postJson.id + 1, 6 | title: { rendered: 'new title' } 7 | }) 8 | 9 | export default { 10 | wordpress: { 11 | keyEntitiesBy: 'id', 12 | queries: {}, 13 | entities: { 14 | posts: { 15 | [postJson.id]: postJson, 16 | [postJson.id + 1]: post2 17 | }, 18 | books: { 19 | [bookJson.id]: bookJson 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/__mocks__/components/ExplicitIdentifier.js: -------------------------------------------------------------------------------- 1 | /* global jest:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import React, { Component } from 'react' 6 | 7 | import { ContentTypes } from '../../../src/constants' 8 | import { connectWpPost } from '../../../src/connect' 9 | 10 | export const target = class extends Component { 11 | render () { 12 | const { query, post } = this.props.kasia 13 | if (!query.complete || !query.OK) return
Loading...
14 | return
{post.title.rendered}
15 | } 16 | } 17 | 18 | export default connectWpPost( 19 | ContentTypes.Post, 20 | 'architecto-enim-omnis-repellendus' 21 | )(target) 22 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-response-modify/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var ActionTypes = require('kasia/lib/constants/ActionTypes') 4 | var modifyResponse = require('wp-api-response-modify') 5 | 6 | /** 7 | * Create wp-api-response-modify plugin configuration for Kasia. 8 | * @param {*} _ 9 | * @param {Array} effects Array of wp-api-response-modify effects 10 | * @returns {Object} Plugin configuration 11 | */ 12 | module.exports = function (_, effects) { 13 | return { 14 | reducers: { 15 | [ActionTypes.RequestComplete]: function (state, action) { 16 | return modifyResponse(action.data, effects) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/util/runSagas.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run all `sagas` until they are complete. 3 | * @param {Object} store Enhanced redux store with `runSaga` method 4 | * @param {Array} sagas Array of saga operations 5 | * @returns {Promise} 6 | */ 7 | export default function runSagas (store, sagas) { 8 | if (typeof store !== 'object' || typeof store.runSaga !== 'function') { 9 | throw new Error('Expecting store to be redux store with runSaga enhancer method.') 10 | } 11 | 12 | return sagas.reduce((promise, saga) => { 13 | return promise.then(() => { 14 | const state = store.getState() 15 | return store.runSaga(saga(state)).done 16 | }) 17 | }, Promise.resolve()) 18 | } 19 | -------------------------------------------------------------------------------- /test/__mocks__/components/CustomContentType.js: -------------------------------------------------------------------------------- 1 | /* global jest:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import React, { Component } from 'react' 6 | 7 | import contentTypesManager from '../../../src/util/contentTypesManager' 8 | import { connectWpPost } from '../../../src/connect' 9 | 10 | contentTypesManager.register({ 11 | name: 'book', 12 | plural: 'books', 13 | slug: 'books' 14 | }) 15 | 16 | @connectWpPost('book', (props) => props.params.id) 17 | export default class Book extends Component { 18 | render () { 19 | const { query, book } = this.props.kasia 20 | if (!query.complete) return
Loading...
21 | return
{book.title.rendered}
22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/__mocks__/components/CustomQuery.js: -------------------------------------------------------------------------------- 1 | /* global jest:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import React, { Component } from 'react' 6 | 7 | import { connectWpQuery } from '../../../src/connect' 8 | import bookJson from '../../__fixtures__/wp-api-responses/book' 9 | 10 | export const queryFn = (wpapi, props) => { 11 | // return wpapi.books(props.params.id).get() 12 | return Promise.resolve(bookJson) 13 | } 14 | 15 | export const target = class extends Component { 16 | render () { 17 | const { query, data: { books } } = this.props.kasia 18 | if (!query.complete || !query.OK) return
Loading...
19 | return
{books[this.props.params.id].slug}
20 | } 21 | } 22 | 23 | export default connectWpQuery(queryFn, () => true)(target) 24 | -------------------------------------------------------------------------------- /test/__mocks__/components/BuiltInContentType.js: -------------------------------------------------------------------------------- 1 | /* global jest:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import React, { Component } from 'react' 6 | 7 | import { ContentTypes } from '../../../src/constants' 8 | import { connectWpPost } from '../../../src/connect' 9 | 10 | export const target = class extends Component { 11 | render () { 12 | const { query, post } = this.props.kasia 13 | if (!query.complete || !query.OK) return
Loading...
14 | return
{post.title.rendered}
15 | } 16 | } 17 | 18 | export default connectWpPost( 19 | ContentTypes.Post, 20 | // check for both id and slug so we can test both 21 | // usually you would target one or the other depending on `keyEntitiesBy` 22 | (props) => props.params.id || props.params.slug 23 | )(target) 24 | -------------------------------------------------------------------------------- /src/util/pickEntityIds.js: -------------------------------------------------------------------------------- 1 | import pickToArray from 'pick-to-array' 2 | 3 | import contentTypesManager from './contentTypesManager' 4 | import { ContentTypesWithoutId } from '../constants' 5 | 6 | /** 7 | * Pick all entity identifiers from a WP-API response. 8 | * @param {Object} data Raw WP-API JSON 9 | * @returns {Array} Entity identifiers 10 | */ 11 | export default function pickEntityIds (data) { 12 | const entityIdentifiers = pickToArray(data, 'id') 13 | 14 | // Accommodate content types that do not have an `id` property 15 | ;[].concat(data).forEach((entity) => { 16 | const type = contentTypesManager.derive(entity) 17 | if (ContentTypesWithoutId.includes(type)) { 18 | entityIdentifiers.push(...pickToArray(entity, 'slug')) 19 | } 20 | }) 21 | 22 | return entityIdentifiers 23 | } 24 | -------------------------------------------------------------------------------- /test/util/queryBuilder.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import getWP, { setWP } from '../../src/wpapi' 6 | import { deriveQueryFunction } from '../../src/util/queryBuilder' 7 | 8 | setWP({ 9 | posts: () => ({ 10 | id: (id) => ({ 11 | embed: () => ({ 12 | get: () => { 13 | output = id 14 | } 15 | }) 16 | }) 17 | }) 18 | }) 19 | 20 | const input = 16 21 | const queryFn = deriveQueryFunction('posts', input) 22 | 23 | let output 24 | 25 | describe('util/queryBuilder', () => { 26 | describe('#_deriveQuery', () => { 27 | it('returns a function', () => { 28 | expect(typeof queryFn).toEqual('function') 29 | }) 30 | 31 | it('that calls chain with correct method name and identifier', () => { 32 | queryFn(getWP()) 33 | expect(output).toEqual(input) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /test/__mocks__/components/CustomQueryNestedPreload.js: -------------------------------------------------------------------------------- 1 | /* global jest:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import React, { Component } from 'react' 6 | 7 | import { connectWpQuery } from '../../../src/connect' 8 | import { preload } from '../../../src/util/preload' 9 | 10 | import bookJson from '../../__fixtures__/wp-api-responses/book' 11 | 12 | const nested = connectWpQuery(() => { 13 | return Promise.resolve({...bookJson, id: bookJson.id + 1 }) 14 | }, () => true)(class {}) 15 | 16 | export const queryFn = function * () { 17 | yield preload([nested])() 18 | return yield Promise.resolve(bookJson) 19 | } 20 | 21 | export const target = class extends Component { 22 | render () { 23 | const { query, data: { books } } = this.props.kasia 24 | if (!query.complete || !query.OK) return
Loading...
25 | return
{books[this.props.params.id].slug}
26 | } 27 | } 28 | 29 | export default connectWpQuery(queryFn, () => true)(target) 30 | -------------------------------------------------------------------------------- /test/util/pickEntityIds.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import pickEntityIds from '../../src/util/pickEntityIds' 6 | 7 | describe('util/pickEntityIds', () => { 8 | it('picks an id', () => { 9 | const ids = pickEntityIds([{ id: 'id' }]) 10 | expect(ids).toEqual(['id']) 11 | }) 12 | 13 | it('picks id over slug for content type with id', () => { 14 | const ids = pickEntityIds([{ id: 'id', slug: 'slug', type: 'post' }]) 15 | expect(ids).toEqual(['id']) 16 | }) 17 | 18 | it('picks slug from content type without id', () => { 19 | const ids = pickEntityIds([{ taxonomy: 'category', slug: 'slug' }]) 20 | expect(ids).toEqual(['slug']) 21 | }) 22 | 23 | it('picks slug and id from multiple entities', () => { 24 | const ids = pickEntityIds([ 25 | { taxonomy: 'category', slug: 'slug' }, 26 | { type: 'post', id: 'id' } 27 | ]) 28 | expect(ids).toEqual(['id', 'slug']) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/__fixtures__/wp-api-responses/tag.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'id': 16, 3 | 'count': 3, 4 | 'description': 'Magni ea ad enim ducimus adipisci alias quisquam reiciendis asperiores et molestias officia impedit', 5 | 'link': 'http://demo.wp-api.org/tag/doloremque-rerum-natus-quas/', 6 | 'name': 'Doloremque rerum natus quas', 7 | 'slug': 'doloremque-rerum-natus-quas', 8 | 'taxonomy': 'post_tag', 9 | '_links': { 10 | 'self': [{ 11 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/tags/16' 12 | }], 13 | 'collection': [{ 14 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/tags' 15 | }], 16 | 'about': [{ 17 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/taxonomies/post_tag' 18 | }], 19 | 'wp:post_type': [{ 20 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/posts?tags=16' 21 | }], 22 | 'curies': [{ 23 | 'name': 'wp', 24 | 'href': 'https://api.w.org/{rel}', 25 | 'templated': true 26 | }] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/redux/actions.js: -------------------------------------------------------------------------------- 1 | import { ActionTypes } from '../constants' 2 | 3 | /** Initiate a request for a single entity from the WP-API. */ 4 | export const createPostRequest = (contentType, identifier) => 5 | ({ type: ActionTypes.RequestCreatePost, contentType, identifier }) 6 | 7 | /** Initiate an arbitrary request to the WP-API. */ 8 | export const createQueryRequest = (queryFn) => 9 | ({ type: ActionTypes.RequestCreateQuery, queryFn }) 10 | 11 | /** Acknowledge a create* request by placing record of it on the store. */ 12 | export const acknowledgeRequest = (action) => 13 | ({ ...action, type: ActionTypes.AckRequest }) 14 | 15 | /** Place the result of a successful request on the store */ 16 | export const completeRequest = (id, data) => 17 | ({ type: ActionTypes.RequestComplete, id, data }) 18 | 19 | /** Update the record of a request with the error returned from a failed response. */ 20 | export const failRequest = (id, error) => 21 | ({ type: ActionTypes.RequestFail, id, error }) 22 | -------------------------------------------------------------------------------- /test/__fixtures__/wp-api-responses/category.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'id': 90, 3 | 'count': 0, 4 | 'description': 'Nihil et consequatur id dolorem eius itaque iste vitae nesciunt fugiat velit', 5 | 'link': 'http://demo.wp-api.org/category/atque-possimus-qui-sint-molestiae-tempore-ratione/', 6 | 'name': 'Atque possimus qui sint molestiae tempore ratione', 7 | 'slug': 'atque-possimus-qui-sint-molestiae-tempore-ratione', 8 | 'taxonomy': 'category', 9 | 'parent': 0, 10 | '_links': { 11 | 'self': [{ 12 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/categories/90' 13 | }], 14 | 'collection': [{ 15 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/categories' 16 | }], 17 | 'about': [{ 18 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/taxonomies/category' 19 | }], 20 | 'wp:post_type': [{ 21 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/posts?categories=90' 22 | }], 23 | 'curies': [{ 24 | 'name': 'wp', 25 | 'href': 'https://api.w.org/{rel}', 26 | 'templated': true 27 | }] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/redux/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeEvery } from 'redux-saga' 2 | import { call, put } from 'redux-saga/effects' 3 | 4 | import getWP from '../wpapi' 5 | import { ActionTypes } from '../constants' 6 | import { acknowledgeRequest, completeRequest, failRequest } from './actions' 7 | import { buildQueryFunction } from '../util/queryBuilder' 8 | 9 | /** 10 | * Make a fetch request to the WP-API according to the action 11 | * object and record the result in the store. 12 | * @param {Object} action Action object 13 | */ 14 | export function * fetch (action) { 15 | try { 16 | yield put(acknowledgeRequest(action)) 17 | const wpapi = getWP() 18 | const fn = action.queryFn || buildQueryFunction(action) 19 | const data = yield call(fn, wpapi) 20 | yield put(completeRequest(action.id, data)) 21 | } catch (error) { 22 | yield put(failRequest(action.id, error.stack || error.message)) 23 | } 24 | } 25 | 26 | /** Watch request create actions and fetch data for them. */ 27 | export function * watchRequests () { 28 | yield takeEvery([ 29 | ActionTypes.RequestCreatePost, 30 | ActionTypes.RequestCreateQuery 31 | ], fetch) 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Outlandish 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-response-modify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kasia-plugin-wp-api-response-modify", 3 | "version": "4.0.0", 4 | "description": "Apply wp-api-response-modify to Kasia", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "standard index.js", 8 | "prepublish": "npm run lint" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/outlandishideas/kasia-plugin-wp-api-response-modify.git" 13 | }, 14 | "keywords": [ 15 | "javascript", 16 | "react", 17 | "redux", 18 | "wordpress", 19 | "word", 20 | "press", 21 | "wp-api", 22 | "wordpress", 23 | "api", 24 | "response", 25 | "modify", 26 | "kasia" 27 | ], 28 | "author": "Outlandish ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/outlandishideas/kasia-plugin-wp-api-response-modify/issues" 32 | }, 33 | "homepage": "https://github.com/outlandishideas/kasia-plugin-wp-api-response-modify#readme", 34 | "dependencies": { 35 | "wp-api-response-modify": "^2.0.0" 36 | }, 37 | "devDependencies": { 38 | "standard": "^7.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/util/findEntities.js: -------------------------------------------------------------------------------- 1 | const isDef = (v) => typeof v !== 'undefined' 2 | 3 | /** Filter `entities` to contain only those whose `keyToInspect` is in `identifiers`. */ 4 | export default function findEntities (entities, keyToInspect, identifiers) { 5 | identifiers = identifiers.map(String) 6 | 7 | const reduced = {} 8 | 9 | for (const entityTypeName in entities) { 10 | const entitiesOfType = entities[entityTypeName] 11 | 12 | for (const key in entitiesOfType) { 13 | const entity = entitiesOfType[key] 14 | 15 | // Try to find entity by `keyToInspect` but fall back on id and then slug as 16 | // for entities that don't have an `id` identifiers will contain their slug 17 | // and vice-versa for entities that don't have a `slug` 18 | let entityId = isDef(entity[keyToInspect]) 19 | ? entity[keyToInspect] 20 | : isDef(entity.id) ? entity.id : entity.slug 21 | 22 | entityId = String(entityId) 23 | 24 | if (identifiers.indexOf(entityId) !== -1) { 25 | reduced[entityTypeName] = reduced[entityTypeName] || {} 26 | reduced[entityTypeName][key] = entity 27 | } 28 | } 29 | } 30 | 31 | return reduced 32 | } 33 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-menus/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Outlandish 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-all-terms/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Outlandish 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-response-modify/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Outlandish 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import { wpapi } from './__mocks__/WP' 6 | import kasia, { preload, preloadQuery } from '../src' 7 | 8 | describe('Kasia', () => { 9 | it('exports a function', () => { 10 | expect(typeof kasia).toEqual('function') 11 | }) 12 | 13 | it('exports preloaders', () => { 14 | expect(typeof preload).toEqual('function') 15 | expect(typeof preloadQuery).toEqual('function') 16 | }) 17 | 18 | it('throws with bad WP value', () => { 19 | expect(() => { 20 | kasia({ wpapi: '' }) 21 | }).toThrowError(/Expecting WP to be instance of `node-wpapi`/) 22 | }) 23 | 24 | it('throws with bad plugins value', () => { 25 | expect(() => { 26 | kasia({ wpapi, plugins: '' }) 27 | }).toThrowError(/Expecting plugins to be array/) 28 | }) 29 | 30 | it('throws with bad index value', () => { 31 | expect(() => { 32 | kasia({ wpapi, keyEntitiesBy: 0 }) 33 | }).toThrowError(/Expecting keyEntitiesBy/) 34 | }) 35 | 36 | it('throws with bad contentTypes value', () => { 37 | expect(() => { 38 | kasia({ wpapi, contentTypes: '' }) 39 | }).toThrowError(/Expecting contentTypes to be array/) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/util/normalise.js: -------------------------------------------------------------------------------- 1 | import { normalize, arrayOf } from 'normalizr' 2 | import merge from 'lodash.merge' 3 | 4 | import schemasManager from './schemasManager' 5 | import contentTypesManager from './contentTypesManager' 6 | 7 | /** Split a response from the WP-API into its constituent entities. */ 8 | export default function normalise (response, idAttribute) { 9 | const schemas = schemasManager.getAll() || schemasManager.init(idAttribute) 10 | 11 | return [].concat(response).reduce((entities, entity) => { 12 | const type = contentTypesManager.derive(entity) 13 | 14 | if (!type) { 15 | console.log( 16 | `[kasia] could not derive entity type - ignoring.`, 17 | `Entity: ${entity ? JSON.stringify(entity) : typeof entity}` 18 | ) 19 | return entities 20 | } 21 | 22 | const contentTypeSchema = schemas[type] 23 | // Built-in content type or previously registered custom content type 24 | ? schemas[type] 25 | // Custom content type, will only get here once for each type 26 | : schemasManager.createSchema(type, idAttribute) 27 | 28 | const schema = Array.isArray(entity) 29 | ? arrayOf(contentTypeSchema) 30 | : contentTypeSchema 31 | 32 | const normalised = normalize(entity, schema) 33 | 34 | return merge(entities, normalised.entities) 35 | }, {}) 36 | } 37 | -------------------------------------------------------------------------------- /src/util/queryBuilder.js: -------------------------------------------------------------------------------- 1 | import contentTypesManager from './contentTypesManager' 2 | 3 | /** Fetch data for a single post via the `wpapi` instance. */ 4 | function queryFn (wpapi, contentTypeMethodName, idTypeMethodName, id) { 5 | const contentTypeApi = wpapi[contentTypeMethodName]() 6 | const query = contentTypeApi[idTypeMethodName](id) 7 | return query.embed().get() 8 | } 9 | 10 | /** 11 | * Create a function that dynamically calls the necessary wpapi 12 | * methods that will fetch data for the given content type item 13 | * and given identifier (ID or slug depending on its type). 14 | * 15 | * @example 16 | * Returned fn for contentTypeMethodName="posts" identifier=16: 17 | * ```js 18 | * () => WP.posts().id(16).embed().get() 19 | * ``` 20 | * 21 | * @param {Object} contentTypeMethodName The method name on wpapi instance 22 | * @param {String|Number} identifier The identifier's id or slug 23 | * @returns {Function} A function to make a request to the WP-API 24 | */ 25 | export function deriveQueryFunction (contentTypeMethodName, identifier) { 26 | const idMethodName = typeof identifier === 'string' ? 'slug' : 'id' 27 | return (wpapi) => queryFn(wpapi, contentTypeMethodName, idMethodName, identifier) 28 | } 29 | 30 | /** Given an `action` produce a function that will query the WP-API. */ 31 | export function buildQueryFunction (action) { 32 | const options = contentTypesManager.get(action.contentType) 33 | return deriveQueryFunction(options.methodName, action.identifier) 34 | } 35 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-all-terms/src/index.js: -------------------------------------------------------------------------------- 1 | import * as effects from 'redux-saga/effects' 2 | import merge from 'lodash.merge' 3 | 4 | const REQUEST_TERMS = 'kasia/terms/REQUEST_TERMS' 5 | const RECEIVE_DATA = 'kasia/terms/RECEIVE_DATA' 6 | 7 | export default kasiaPluginWpApiAllTerms 8 | 9 | export const fetchTerms = () => 10 | ({ type: REQUEST_TERMS }) 11 | 12 | function fetch (WP, action) { 13 | switch (action.type) { 14 | case REQUEST_TERMS: 15 | return WP.allTerms().get() 16 | default: 17 | throw new Error(`Unknown request type "${action.request}".`) 18 | } 19 | } 20 | 21 | function * fetchResource (WP, action) { 22 | const { id, type } = action 23 | const data = yield effects.call(fetch, WP, action) 24 | yield effects.put({ type: RECEIVE_DATA, request: type, data, id }) 25 | } 26 | 27 | function kasiaPluginWpApiAllTerms (WP, config) { 28 | WP.allTerms = WP.registerRoute(config.route || 'wp/v2', 'all-terms') 29 | 30 | return { 31 | reducers: { 32 | [RECEIVE_DATA]: (state, action) => { 33 | return merge({}, state, { terms: action.data }) 34 | } 35 | }, 36 | sagas: [function * fetchTermsSaga () { 37 | while (true) { 38 | const action = yield effects.take((action) => action === REQUEST_TERMS) 39 | yield fetchResource(WP, action) 40 | } 41 | }] 42 | } 43 | } 44 | 45 | kasiaPluginWpApiAllTerms.preload = function (WP) { 46 | return function * () { 47 | yield * fetchResource(WP, fetchTerms()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-menus/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kasia-plugin-wp-api-menus", 3 | "version": "4.0.5", 4 | "description": "Provides support for the WP-API menus plugin to Kasia", 5 | "main": "lib/index.js", 6 | "jsnext:main": "src/index.js", 7 | "scripts": { 8 | "build": "rimraf lib && babel -d lib/ src/", 9 | "lint": "standard src/**/*.js", 10 | "prepublish": "npm run lint && npm run build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/outlandishideas/kasia-plugin-wp-api-menus.git" 15 | }, 16 | "keywords": [ 17 | "javascript", 18 | "react", 19 | "redux", 20 | "wordpress", 21 | "word", 22 | "press", 23 | "wp-api", 24 | "wordpress", 25 | "api", 26 | "menus", 27 | "menu", 28 | "kasia" 29 | ], 30 | "author": "Outlandish ", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/outlandishideas/kasia-plugin-wp-api-menus/issues" 34 | }, 35 | "homepage": "https://github.com/outlandishideas/kasia-plugin-wp-api-menus#readme", 36 | "dependencies": { 37 | "lodash.merge": "^4.3.5", 38 | "redux-saga": "^0.11.0" 39 | }, 40 | "devDependencies": { 41 | "babel-cli": "6.6.5", 42 | "babel-core": "6.7.2", 43 | "babel-plugin-add-module-exports": "^0.1.4", 44 | "babel-polyfill": "^6.7.4", 45 | "babel-preset-es2015": "^6.6.0", 46 | "babel-preset-react": "^6.11.1", 47 | "babel-preset-stage-0": "^6.5.0", 48 | "rimraf": "^2.5.2", 49 | "standard": "^7.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-all-terms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kasia-plugin-wp-api-all-terms", 3 | "version": "4.0.4", 4 | "description": "Provides support for the WP-API all terms plugin to Kasia", 5 | "main": "lib/index.js", 6 | "jsnext:main": "src/index.js", 7 | "scripts": { 8 | "build": "rimraf lib && babel -d lib/ src/", 9 | "lint": "standard src/index.js", 10 | "prepublish": "npm run lint && npm run build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/outlandishideas/kasia-plugin-wp-api-all-terms.git" 15 | }, 16 | "keywords": [ 17 | "javascript", 18 | "react", 19 | "redux", 20 | "wordpress", 21 | "word", 22 | "press", 23 | "wp-api", 24 | "wordpress", 25 | "api", 26 | "terms", 27 | "term", 28 | "all", 29 | "kasia" 30 | ], 31 | "author": "Outlandish ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/outlandishideas/kasia-plugin-wp-api-all-terms/issues" 35 | }, 36 | "homepage": "https://github.com/outlandishideas/kasia-plugin-wp-api-all-terms#readme", 37 | "dependencies": { 38 | "lodash.merge": "^4.3.5", 39 | "redux-saga": "^0.11.0" 40 | }, 41 | "devDependencies": { 42 | "babel-cli": "6.6.5", 43 | "babel-core": "6.7.2", 44 | "babel-plugin-add-module-exports": "^0.1.4", 45 | "babel-polyfill": "^6.7.4", 46 | "babel-preset-es2015": "^6.6.0", 47 | "babel-preset-react": "^6.11.1", 48 | "babel-preset-stage-0": "^6.5.0", 49 | "rimraf": "^2.5.2", 50 | "standard": "^7.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/redux/reducer.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import '../__mocks__/WP' 6 | import postJson from '../__fixtures__/wp-api-responses/post' 7 | import { INITIAL_STATE, acknowledgeReducer, completeReducer, failReducer } from '../../src/redux/reducer' 8 | 9 | describe('redux/reducer', () => { 10 | const id = 0 11 | 12 | let state 13 | let query 14 | 15 | function assertState (fn) { 16 | return () => { 17 | const newState = fn() 18 | expect(typeof newState).toEqual('object') 19 | expect(newState === state).toEqual(false) 20 | state = newState 21 | query = state.queries[id] 22 | } 23 | } 24 | 25 | describe('acknowledge', () => { 26 | it('returns a new object', assertState(() => acknowledgeReducer(INITIAL_STATE, { id }))) 27 | it('sets query on state', () => expect(query).toEqual({ id, prepared: true, complete: false, OK: null })) 28 | }) 29 | 30 | describe('complete', () => { 31 | it('returns a new object', assertState(() => completeReducer((data) => data)(state, { id, data: [postJson] }))) 32 | it('sets entity ids', () => expect(query.entities).toEqual([postJson.id])) 33 | it('sets complete', () => expect(query.complete).toEqual(true)) 34 | it('sets OK', () => expect(query.OK).toEqual(true)) 35 | }) 36 | 37 | describe('fail', () => { 38 | const error = new Error('Wuh-oh!').stack 39 | 40 | it('returns a new object', assertState(() => failReducer(state, { id, error }))) 41 | it('sets error', () => expect(query.error).toEqual(error)) 42 | it('sets complete', () => expect(query.complete).toEqual(true)) 43 | it('sets OK', () => expect(query.OK).toEqual(false)) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/__fixtures__/wp-api-responses/user.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'id': 95, 3 | 'name': 'Sonya', 4 | 'url': 'http://Beatty.info/mollitia-qui-sed-est-beatae', 5 | 'description': 'Et ut est aut rerum quis qui magnam. Vero assumenda laudantium qui autem. Saepe reprehenderit consequatur perferendis libero nam consequatur. Eius voluptate atque illum dolorem repellendus. Reprehenderit assumenda est tempore nesciunt ea.\r\n\r\nUt quia magnam sit molestiae quos. Accusamus voluptate non animi nihil esse. Labore porro quod vitae consequuntur reprehenderit saepe.\r\n\r\nMolestiae porro dolorem nihil neque sint. Reiciendis ducimus rerum suscipit labore temporibus nisi nostrum. Natus amet eum eum excepturi tempore nisi fuga. Totam aut facilis sapiente voluptatem alias unde.\r\n\r\nEaque consequatur omnis adipisci voluptatem illo. Quia neque necessitatibus ut quaerat distinctio hic molestias. Ipsum nisi molestiae vitae.\r\n\r\nAperiam sint nobis quia ut qui sint veniam. Nam animi corporis et ratione numquam est perferendis. Et dolore sit quos earum eius soluta nemo iste.\r\n\r\nVeniam incidunt voluptas reiciendis aut culpa natus. Dolor est numquam consequatur commodi.', 6 | 'link': 'http://demo.wp-api.org/author/elmira-luettgen/', 7 | 'slug': 'elmira-luettgen', 8 | 'avatar_urls': { 9 | '24': 'http://0.gravatar.com/avatar/3eb34c383eaebf4c71409ba2a95fc34a?s=24&d=mm&r=g', 10 | '48': 'http://0.gravatar.com/avatar/3eb34c383eaebf4c71409ba2a95fc34a?s=48&d=mm&r=g', 11 | '96': 'http://0.gravatar.com/avatar/3eb34c383eaebf4c71409ba2a95fc34a?s=96&d=mm&r=g' 12 | }, 13 | '_links': { 14 | 'self': [{ 15 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users/95' 16 | }], 17 | 'collection': [{ 18 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users' 19 | }] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/util/preload.js: -------------------------------------------------------------------------------- 1 | import { put, join, fork } from 'redux-saga/effects' 2 | 3 | import { completeRequest } from '../redux/actions' 4 | 5 | /** Make a preloader saga for all Kasia components within the `components` array. */ 6 | export function preload (components, renderProps = {}, state = {}) { 7 | if (!Array.isArray(components)) { 8 | throw new Error(`Expecting components to be array, got "${typeof components}".`) 9 | } else if (typeof renderProps !== 'object') { 10 | throw new Error(`Expecting renderProps to be an object, got "${typeof renderProps}".`) 11 | } else if (typeof state !== 'object') { 12 | throw new Error(`Expecting state to be an object, got "${typeof state}".`) 13 | } 14 | 15 | return function * () { 16 | const tasks = yield components 17 | .map((c) => c && typeof c.preload === 'function' ? c : false) 18 | .filter(Boolean) 19 | .map((component) => component.preload(renderProps, state)) 20 | .map(([ fn, action ]) => fork(fn, action)) 21 | 22 | if (tasks.length) { 23 | yield join(...tasks) 24 | } 25 | } 26 | } 27 | 28 | /** Make a preloader saga that fetches data for an arbitrary WP API query. */ 29 | export function preloadQuery (queryFn, renderProps = {}, state = {}) { 30 | if (typeof queryFn !== 'function') { 31 | throw new Error(`Expecting queryFn to be a function, got "${queryFn}".`) 32 | } else if (typeof renderProps !== 'object') { 33 | throw new Error(`Expecting renderProps to be an object, got "${typeof renderProps}".`) 34 | } else if (typeof state !== 'object') { 35 | throw new Error(`Expecting state to be an object, got "${typeof state}".`) 36 | } 37 | 38 | return function * () { 39 | const data = yield queryFn(renderProps, state) 40 | yield put(completeRequest(null, data)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // WP-API namespace (`wp-json/${namespace}`) 2 | export const WpApiNamespace = 'wp/v2' 3 | 4 | export const ActionTypes = { 5 | // Initiate a request to the WP API 6 | RequestCreatePost: 'kasia/REQUEST_CREATE_POST', 7 | RequestCreateQuery: 'kasia/REQUEST_CREATE_QUERY', 8 | // Place record of a request in the store 9 | AckRequest: 'kasia/ACK_REQUEST', 10 | // Place the result of a request on the store 11 | RequestComplete: 'kasia/REQUEST_COMPLETE', 12 | // Record the failure of a request on the store 13 | RequestFail: 'kasia/REQUEST_FAILED' 14 | } 15 | 16 | // The built-in content types available in WordPress. 17 | export const ContentTypes = { 18 | Category: 'category', 19 | Comment: 'comment', 20 | Media: 'media', 21 | Page: 'page', 22 | Post: 'post', 23 | PostStatus: 'status', 24 | PostType: 'type', 25 | PostRevision: 'revision', 26 | Tag: 'tag', 27 | Taxonomy: 'taxonomy', 28 | User: 'user' 29 | } 30 | 31 | // Plural names of the built-in content types. These are used in determining the 32 | // wpapi method to call when fetching a content type's respective data. 33 | export const ContentTypesPlural = { 34 | [ContentTypes.Category]: 'categories', 35 | [ContentTypes.Comment]: 'comments', 36 | [ContentTypes.Media]: 'media', 37 | [ContentTypes.Page]: 'pages', 38 | [ContentTypes.Post]: 'posts', 39 | [ContentTypes.PostStatus]: 'statuses', 40 | [ContentTypes.PostType]: 'types', 41 | [ContentTypes.PostRevision]: 'revisions', 42 | [ContentTypes.Tag]: 'tags', 43 | [ContentTypes.Taxonomy]: 'taxonomies', 44 | [ContentTypes.User]: 'users' 45 | } 46 | 47 | // These content types do not have `id` properties. 48 | export const ContentTypesWithoutId = [ 49 | ContentTypes.Category, 50 | ContentTypes.PostType, 51 | ContentTypes.PostStatus, 52 | ContentTypes.Taxonomy 53 | ] 54 | -------------------------------------------------------------------------------- /test/util/contentTypesManager.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import '../__mocks__/WP' 6 | import contentTypesManager from '../../src/util/contentTypesManager' 7 | import { ContentTypes } from '../../src/constants' 8 | 9 | describe('util/contentTypesManager', () => { 10 | describe('#getAll', () => { 11 | it('returns an object', () => { 12 | const actual = typeof contentTypesManager.getAll() 13 | expect(actual).toEqual('object') 14 | }) 15 | }) 16 | 17 | describe('#get', () => { 18 | it('returns an object', () => { 19 | const actual = typeof contentTypesManager.get(ContentTypes.Post) 20 | expect(actual).toEqual('object') 21 | }) 22 | }) 23 | 24 | describe('#register', () => { 25 | it('throws with bad options object', () => { 26 | const fn = () => contentTypesManager.register('') 27 | expect(fn).toThrowError(/Invalid content type object/) 28 | }) 29 | 30 | Object.values(ContentTypes).forEach((builtInType) => { 31 | it('throws when name is ' + builtInType, () => { 32 | const opts = { name: builtInType, plural: builtInType, slug: builtInType } 33 | const fn = () => contentTypesManager.register(opts) 34 | const expected = `Content type with name "${builtInType}" already exists.` 35 | expect(fn).toThrowError(expected) 36 | }) 37 | }) 38 | 39 | it('adds custom content type to cache', () => { 40 | const opts = { name: 'article', plural: 'articles', slug: 'articles' } 41 | contentTypesManager.register(opts) 42 | const actual = contentTypesManager.getAll().get('article') 43 | const expected = Object.assign({}, opts, { 44 | methodName: 'articles', 45 | route: '/articles/(?P)' 46 | }) 47 | expect(actual).toEqual(expected) 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/util/findEntities.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import findEntities from '../../src/util/findEntities' 6 | 7 | describe('util/findEntities', () => { 8 | it('should be a function', () => { 9 | expect(typeof findEntities).toEqual('function') 10 | }) 11 | 12 | it('should filter entities by id', () => { 13 | const actual = findEntities({ 14 | posts: { 0: { id: 0, slug: 'post', title: 'post' } }, 15 | pages: { 1: { id: 1, slug: 'page', title: 'page' } } 16 | }, 'id', [0]) 17 | const expected = { 18 | posts: { 0: { id: 0, slug: 'post', title: 'post' } } 19 | } 20 | expect(actual).toEqual(expected) 21 | }) 22 | 23 | it('should filter entities by slug', () => { 24 | const actual = findEntities({ 25 | posts: { 0: { id: 0, slug: 'post', title: 'post' } }, 26 | pages: { 1: { id: 1, slug: 'page', title: 'page' } } 27 | }, 'slug', ['page']) 28 | const expected = { 29 | pages: { 1: { id: 1, slug: 'page', title: 'page' } } 30 | } 31 | expect(actual).toEqual(expected) 32 | }) 33 | 34 | it('should filter entities by id, fallback on slug', () => { 35 | const actual = findEntities({ 36 | posts: { 0: { slug: 'post', title: 'post' } }, 37 | pages: { 1: { id: 1, slug: 'page', title: 'page' } } 38 | }, 'id', ['post']) 39 | const expected = { 40 | posts: { 0: { slug: 'post', title: 'post' } } 41 | } 42 | expect(actual).toEqual(expected) 43 | }) 44 | 45 | it('should filter entities by slug, fallback on id', () => { 46 | const actual = findEntities({ 47 | posts: { 0: { id: 0, slug: 'post', title: 'post' } }, 48 | pages: { 1: { id: 1, title: 'page' } } 49 | }, 'slug', [1]) 50 | const expected = { 51 | pages: { 1: { id: 1, title: 'page' } } 52 | } 53 | expect(actual).toEqual(expected) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /test/connect/preload.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import queryCounter from '../../src/util/queryCounter' 6 | import { ActionTypes, ContentTypes } from '../../src/constants' 7 | import { fetch } from '../../src/redux/sagas' 8 | 9 | import BuiltInContentType from '../__mocks__/components/BuiltInContentType' 10 | import BadContentType from '../__mocks__/components/BadContentType' 11 | import CustomQuery from '../__mocks__/components/CustomQuery' 12 | 13 | describe('connect/preload', () => { 14 | const props = { params: { id: 16 } } 15 | 16 | beforeAll(() => queryCounter.reset()) 17 | 18 | describe('connectWpPost', () => { 19 | const preloader = BuiltInContentType.preload 20 | 21 | let result 22 | 23 | it('has static preload method', () => { 24 | expect(typeof preloader).toEqual('function') 25 | }) 26 | 27 | it('throws with bad content type', () => { 28 | expect(() => BadContentType.preload()).toThrowError(/not recognised/) 29 | }) 30 | 31 | it('preloader returns an array', () => { 32 | result = preloader(props) 33 | expect(Array.isArray(result)).toEqual(true) 34 | }) 35 | 36 | it('returns an array of length two', () => { 37 | expect(result.length).toEqual(2) 38 | }) 39 | 40 | it('contains fetch saga fn', () => { 41 | expect(result[0]).toEqual(fetch) 42 | }) 43 | 44 | it('contains RequestCreatePost action', () => { 45 | expect(result[1].type).toEqual(ActionTypes.RequestCreatePost) 46 | expect(result[1].contentType).toEqual(ContentTypes.Post) 47 | expect(result[1].identifier).toEqual(16) 48 | }) 49 | }) 50 | 51 | describe('connectWpQuery', () => { 52 | const preloader = CustomQuery.preload 53 | 54 | let result 55 | 56 | it('has static preload method', () => { 57 | expect(typeof preloader).toEqual('function') 58 | }) 59 | 60 | it('preloader returns an array', () => { 61 | result = preloader(props) 62 | expect(Array.isArray(result)).toEqual(true) 63 | }) 64 | 65 | it('returns an array of length two', () => { 66 | expect(result.length).toEqual(2) 67 | }) 68 | 69 | it('contains fetch saga fn', () => { 70 | expect(result[0]).toEqual(fetch) 71 | }) 72 | 73 | it('contains RequestCreateQuery action', () => { 74 | expect(result[1].type).toEqual(ActionTypes.RequestCreateQuery) 75 | }) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /test/redux/sagas.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import { put, call } from 'redux-saga/effects' 6 | 7 | import '../__mocks__/WP' 8 | import getWP from '../../src/wpapi' 9 | import { buildQueryFunction } from '../../src/util/queryBuilder' 10 | import { createPostRequest, createQueryRequest } from '../../src/redux/actions' 11 | import { fetch } from '../../src/redux/sagas' 12 | import { ActionTypes, ContentTypes } from '../../src/constants' 13 | 14 | describe('redux/sagas', () => { 15 | describe('createPostRequest', () => { 16 | const action = createPostRequest(ContentTypes.Post, 16) 17 | const generator = fetch(action) 18 | 19 | it('yields a put with acknowledgeRequest action', () => { 20 | const actual = generator.next().value 21 | const expected = put({ ...action, type: ActionTypes.AckRequest }) 22 | expect(actual).toEqual(expected) 23 | }) 24 | 25 | it('yields a call to result of buildQueryFunction', () => { 26 | const actual = generator.next().value 27 | const expected = call(buildQueryFunction(action), getWP()) 28 | actual.CALL.fn = actual.CALL.fn.toString() 29 | expected.CALL.fn = expected.CALL.fn.toString() 30 | expect(actual).toEqual(expected) 31 | }) 32 | 33 | it('yields a put with completeRequest action', () => { 34 | const actual = generator.next('mockResult').value 35 | const expected = put({ type: ActionTypes.RequestComplete, data: 'mockResult' }) 36 | expect(actual).toEqual(expected) 37 | }) 38 | }) 39 | 40 | describe('createQueryRequest', () => { 41 | const queryFn = () => {} 42 | const action = createQueryRequest(queryFn) 43 | const generator = fetch(action) 44 | 45 | it('yields a put with acknowledgeRequest action', () => { 46 | const actual = generator.next().value 47 | const expected = put({ ...action, type: ActionTypes.AckRequest }) 48 | expect(actual).toEqual(expected) 49 | }) 50 | 51 | it('yields a call to queryFn', () => { 52 | const actual = generator.next().value 53 | const expected = call(action.queryFn, getWP()) 54 | expect(actual).toEqual(expected) 55 | }) 56 | 57 | it('puts a completeRequest action with result', () => { 58 | const actual = generator.next('mockResult').value 59 | const expected = put({ type: ActionTypes.RequestComplete, data: 'mockResult' }) 60 | expect(actual).toEqual(expected) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-all-terms/README.md: -------------------------------------------------------------------------------- 1 | # Kasia Plugin WP-API All Terms 2 | 3 | > Adds support for the [WP-API all terms plugin](https://wordpress.org/plugins/wp-rest-api-all-terms/) to Kasia 4 | 5 | Made with ❤ at [@outlandish](http://www.twitter.com/outlandish) 6 | 7 | npm version 8 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 9 | 10 | ## Install 11 | 12 | ```sh 13 | npm install --save kasia-plugin-wp-api-all-terms 14 | ``` 15 | 16 | ## Import 17 | 18 | ```js 19 | // ES2015 20 | import KasiaWpApiAllTermsPlugin from 'kasia-plugin-wp-api-all-terms' 21 | ``` 22 | 23 | ```js 24 | // CommonJS 25 | var KasiaWpApiAllTermsPlugin = require('kasia-plugin-wp-api-all-terms') 26 | ``` 27 | 28 | ## Initialise 29 | 30 | Pass to Kasia via the `plugins` option: 31 | 32 | ```js 33 | const { kasiaReducer, kasiaSagas } = Kasia({ 34 | WP, 35 | plugins: [KasiaWpApiAllTermsPlugin] 36 | }) 37 | ``` 38 | 39 | ## Actions 40 | 41 | Import: 42 | 43 | ```js 44 | import { fetchTerms } from 'kasia-plugin-wp-api-all-terms' 45 | ``` 46 | 47 | ### `fetchTerms()` 48 | 49 | Get all terms available. 50 | 51 | Terms will be available at `store.wordpress.terms`, for example: 52 | 53 | ```js 54 | { 55 | categories: [{ 56 | term_id: 3 57 | }], 58 | tags: [{ 59 | term_id: 16 60 | }], 61 | technologies: [{ 62 | term_id: 15 63 | }] 64 | } 65 | ``` 66 | 67 | ## Universal Applications 68 | 69 | ```js 70 | import kasiaPluginWpApiAllTerms from 'kasia-plugin-wp-api-all-terms' 71 | ``` 72 | 73 | ### `kasiaPluginWpApiAllTerms.preload(WP)` 74 | 75 | - __WP__ {Object} WP API instance 76 | 77 | Returns a single saga generator. 78 | 79 | ## Contributing 80 | 81 | All pull requests and issues welcome! 82 | 83 | - When submitting an issue please provide adequate steps to reproduce the problem. 84 | - PRs must be made using the `standard` code style. 85 | - PRs must update the version of the library according to [semantic versioning](http://semver.org/). 86 | 87 | If you're not sure how to contribute, check out Kent C. Dodds' 88 | [great video tutorials on egghead.io](https://egghead.io/lessons/javascript-identifying-how-to-contribute-to-an-open-source-project-on-github)! 89 | 90 | ## Author & License 91 | 92 | `kasia-plugin-wp-api-all-terms` was created by [Outlandish](https://twitter.com/outlandish) and is released under the MIT license. 93 | -------------------------------------------------------------------------------- /test/plugin.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import Wpapi from 'wpapi' 6 | import { combineReducers, createStore } from 'redux' 7 | import { spawn } from 'redux-saga/effects' 8 | 9 | import kasia from '../src' 10 | import { ActionTypes } from '../src/constants' 11 | import { acknowledgeRequest, completeRequest } from '../src/redux/actions' 12 | import postJson from './__fixtures__/wp-api-responses/post' 13 | 14 | const testActionType = 'kasia/TEST_ACTION' 15 | 16 | let countHitPluginOwnActionTypeReducer = 0 17 | let countHitPluginNativeActionTypeReducer = 0 18 | 19 | function pluginSaga () {} 20 | 21 | function setup () { 22 | const plugin = () => ({ 23 | sagas: [pluginSaga], 24 | reducers: { 25 | [testActionType]: (state) => { 26 | countHitPluginOwnActionTypeReducer++ 27 | return state 28 | }, 29 | [ActionTypes.RequestComplete]: (state) => { 30 | countHitPluginNativeActionTypeReducer++ 31 | return state 32 | } 33 | } 34 | }) 35 | 36 | const { kasiaReducer, kasiaSagas } = kasia({ 37 | wpapi: new Wpapi({ endpoint: '' }), 38 | plugins: [plugin] 39 | }) 40 | 41 | const rootReducer = combineReducers(kasiaReducer) 42 | const store = createStore(rootReducer) 43 | 44 | return { store, kasiaSagas } 45 | } 46 | 47 | describe('Plugin', () => { 48 | const { store, kasiaSagas } = setup() 49 | 50 | it('should run untouched native action type', () => { 51 | store.dispatch(acknowledgeRequest({ type: ActionTypes.RequestCreatePost, id: 0 })) 52 | expect(store.getState().wordpress.queries['0']).toBeTruthy() 53 | }) 54 | 55 | describe('with native action type', () => { 56 | it('should hit native action handler', () => { 57 | store.dispatch(completeRequest(0, postJson)) 58 | expect(store.getState().wordpress.queries['0']).toBeTruthy() 59 | }) 60 | 61 | it('should hit third-party action handler for native action type', () => { 62 | expect(countHitPluginNativeActionTypeReducer).toEqual(1) 63 | }) 64 | }) 65 | 66 | describe('with new action type', () => { 67 | it('should hit third-party action handler for third-party action type', () => { 68 | store.dispatch({ type: testActionType }) 69 | expect(countHitPluginOwnActionTypeReducer).toEqual(1) 70 | }) 71 | 72 | it('should add the plugin saga to sagas array', () => { 73 | const actual = kasiaSagas.toString() 74 | const expected = spawn(pluginSaga).toString() 75 | expect(actual).toContain(expected) 76 | }) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as effects from 'redux-saga/effects' 2 | 3 | import debug, { toggleDebug } from './util/debug' 4 | import makeReducer from './redux/reducer' 5 | import invariants from './invariants' 6 | import contentTypesManager from './util/contentTypesManager' 7 | import queryCounter from './util/queryCounter' 8 | import { default as _runSagas } from './util/runSagas' 9 | import { setWP } from './wpapi' 10 | import { watchRequests } from './redux/sagas' 11 | import { rewind as connectRewind } from './connect' 12 | 13 | export * from './util/preload' 14 | 15 | export default kasia 16 | 17 | // Components of the toolset that are extensible via plugins 18 | const COMPONENTS_BASE = { 19 | sagas: [watchRequests], 20 | reducers: {} 21 | } 22 | 23 | /** Reset the internal query counter and first mount bool. 24 | * Should be called before each SSR. */ 25 | kasia.rewind = function rewind () { 26 | connectRewind() 27 | queryCounter.reset() 28 | } 29 | 30 | /** Run all `sagas` until they are complete. */ 31 | export function runSagas (store, sagas) { 32 | kasia.rewind() 33 | return _runSagas(store, sagas) 34 | } 35 | 36 | /** 37 | * Configure Kasia. 38 | * @param {WP} opts.wpapi Instance of node-wpapi 39 | * @param {String} [opts.keyEntitiesBy] Property used to key entities in the store 40 | * @param {Boolean} [opts.debug] Log debug statements 41 | * @param {Array} [opts.plugins] Kasia plugins 42 | * @param {Array} [opts.contentTypes] Custom content type definition objects 43 | * @returns {Object} Kasia reducer 44 | */ 45 | function kasia (opts = {}) { 46 | let { 47 | WP, 48 | wpapi, 49 | debug: _debug = false, 50 | keyEntitiesBy = 'id', 51 | plugins: userPlugins = [], 52 | contentTypes = [] 53 | } = opts 54 | 55 | toggleDebug(_debug) 56 | 57 | debug('initialised with: ', opts) 58 | 59 | if (WP) { 60 | console.log('[kasia] config option `WP` is replaced by `wpapi` in v4.') 61 | wpapi = WP 62 | } 63 | 64 | invariants.isWpApiInstance(setWP(wpapi)) 65 | invariants.isKeyEntitiesByOption(keyEntitiesBy) 66 | invariants.isArray('plugins', userPlugins) 67 | invariants.isArray('contentTypes', contentTypes) 68 | 69 | contentTypes.forEach((type) => contentTypesManager.register(type)) 70 | 71 | // Merge plugins into internal sagas array and reducers object 72 | const { sagas, reducers } = userPlugins.reduce((components, p, i) => { 73 | const isArr = p instanceof Array 74 | invariants.isPlugin('plugin at index ' + i, isArr ? p[0] : p) 75 | const { sagas, reducers } = isArr ? p[0](wpapi, p[1] || {}, opts) : p(wpapi, {}, opts) 76 | components.sagas.push(...sagas) 77 | Object.assign(components.reducers, reducers) 78 | return components 79 | }, COMPONENTS_BASE) 80 | 81 | return { 82 | kasiaReducer: makeReducer({ keyEntitiesBy, reducers }), 83 | kasiaSagas: sagas.map((saga) => effects.spawn(saga)) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/connect/connectWpQuery.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import React from 'react' 6 | import merge from 'lodash.merge' 7 | import { mount } from 'enzyme' 8 | 9 | import queryCounter from '../../src/util/queryCounter' 10 | import { wrapQueryFn } from '../../src/connect' 11 | import { ActionTypes } from '../../src/constants' 12 | 13 | import '../__mocks__/WP' 14 | import initialState from '../__mocks__/states/initial' 15 | import multipleBooks from '../__mocks__/states/multipleBooks' 16 | import bookJson from '../__fixtures__/wp-api-responses/book' 17 | import CustomQueryComponent, { target, queryFn } from '../__mocks__/components/CustomQuery' 18 | 19 | const CustomQuery = (props, store) => mount(, { context: { store } }) 20 | 21 | function setup (state) { 22 | const dispatch = jest.fn() 23 | const subscribe = () => {} 24 | const getState = () => state 25 | const store = { dispatch, getState, subscribe } 26 | return { store } 27 | } 28 | 29 | describe('connectWpQuery', () => { 30 | beforeEach(() => queryCounter.reset()) 31 | 32 | it('should wrap the component', () => { 33 | // Components are wrapped first by react-redux connect() 34 | expect(CustomQueryComponent.WrappedComponent.WrappedComponent).toBe(target) 35 | expect(CustomQueryComponent.WrappedComponent.__kasia__).toBe(true) 36 | }) 37 | 38 | it('should render loading message with bad query', () => { 39 | const query = { prepared: true } 40 | const state = merge({}, initialState('id'), { wordpress: { queries: { 0: query } } }) 41 | const { store } = setup(state) 42 | const rendered = CustomQuery({ params: { id: 10 } }, store) 43 | expect(rendered.html()).toEqual('
Loading...
') 44 | }) 45 | 46 | it('should render loading message with incomplete query', () => { 47 | const query = { id: 0, complete: false, OK: null, prepared: true } 48 | const state = merge({}, initialState('id'), { wordpress: { queries: { 0: query } } }) 49 | const { store } = setup(state) 50 | const rendered = CustomQuery({ params: { id: 10 } }, store) 51 | expect(rendered.html()).toEqual('
Loading...
') 52 | }) 53 | 54 | it('should render prepared post data with complete query', () => { 55 | const query = { prepared: true } 56 | const { store } = setup(merge({}, multipleBooks, { wordpress: { queries: { 0: query } } })) 57 | const rendered = CustomQuery({ params: { id: bookJson.id } }, store) 58 | expect(rendered.html()).toEqual(`
${bookJson.slug}
`) 59 | }) 60 | 61 | it('should request data without query', () => { 62 | const { store } = setup(initialState('id')) 63 | CustomQuery({ params: { id: 10 } }, store) 64 | const action = store.dispatch.mock.calls[0][0] 65 | expect(action.id).toEqual(0) 66 | expect(action.type).toEqual(ActionTypes.RequestCreateQuery) 67 | expect(action.queryFn.toString()).toEqual(wrapQueryFn(queryFn).toString()) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-menus/README.md: -------------------------------------------------------------------------------- 1 | # Kasia Plugin WP-API Menus 2 | 3 | > Adds support for the WP-API menus to Kasia 4 | 5 | Made with ❤ at [@outlandish](http://www.twitter.com/outlandish) 6 | 7 | npm version 8 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 9 | 10 | ## Install 11 | 12 | ```sh 13 | npm install --save kasia-plugin-wp-api-menus 14 | ``` 15 | 16 | ## Import 17 | 18 | ```js 19 | // ES2015 20 | import KasiaWpApiMenusPlugin from 'kasia-plugin-wp-api-menus' 21 | ``` 22 | 23 | ```js 24 | // CommonJS 25 | var KasiaWpApiMenusPlugin = require('kasia-plugin-wp-api-menus') 26 | ``` 27 | 28 | ## Initialise 29 | 30 | Pass to Kasia via the `plugins` option: 31 | 32 | ```js 33 | const { kasiaReducer, kasiaSagas } = Kasia({ 34 | WP, 35 | plugins: [KasiaWpApiMenusPlugin] 36 | }) 37 | ``` 38 | 39 | ## Action creators 40 | 41 | ```js 42 | import { 43 | fetchMenus, fetchMenu, 44 | fetchThemeLocations, fetchThemeLocation 45 | } from 'kasia-plugin-wp-api-menus' 46 | ``` 47 | 48 | Dispatch the returned action objects with `store.dispatch()`. 49 | 50 | ### `actions.fetchMenus()` 51 | 52 | Get all menus available. 53 | 54 | Menus will be available at `store.wordpress.menus`. 55 | 56 | ### `actions.fetchMenu(id)` 57 | 58 | Get a single menu. 59 | 60 | - __id__ {Number|String} ID or slug of the menu to fetch 61 | 62 | Menu will be available at `store.wordpress.menus[id]`. 63 | 64 | ### `actions.fetchThemeLocations()` 65 | 66 | Get all theme menu locations. 67 | 68 | Theme locations will be available at `store.wordpress.menusLocations`. 69 | 70 | ### `actions.fetchThemeLocation(id)` 71 | 72 | Get a single theme menu location. 73 | 74 | - __id__ {Number|String} ID or slug of the theme menu location to fetch 75 | 76 | Menu will be available at `store.wordpress.menuLocations[id]`. 77 | 78 | 79 | ## Universal Applications 80 | 81 | ```js 82 | import kasiaPluginWpApiMenus from 'kasia-plugin-wp-api-menus' 83 | ``` 84 | 85 | ### `kasiaPluginWpApiMenus.preload(WP)` 86 | 87 | - __WP__ {Object} WP API instance 88 | 89 | Returns a single saga generator. 90 | 91 | ## Contributing 92 | 93 | All pull requests and issues welcome! 94 | 95 | - When submitting an issue please provide adequate steps to reproduce the problem. 96 | - PRs must be made using the `standard` code style. 97 | - PRs must update the version of the library according to [semantic versioning](http://semver.org/). 98 | 99 | If you're not sure how to contribute, check out Kent C. Dodds' 100 | [great video tutorials on egghead.io](https://egghead.io/lessons/javascript-identifying-how-to-contribute-to-an-open-source-project-on-github)! 101 | 102 | ## Author & License 103 | 104 | `kasia-plugin-wp-api-menus` was created by [Outlandish](https://twitter.com/outlandish) and is released under the MIT license. 105 | -------------------------------------------------------------------------------- /test/redux/reducer-journey.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | jest.mock('redux-saga') 6 | 7 | import { combineReducers, createStore } from 'redux' 8 | import Wpapi from 'wpapi' 9 | 10 | import postJson from '../__fixtures__/wp-api-responses/post' 11 | import initialState from '../__mocks__/states/initial' 12 | 13 | import kasia from '../../src' 14 | import normalise from '../../src/util/normalise' 15 | import queryCounter from '../../src/util/queryCounter' 16 | import pickEntityIds from '../../src/util/pickEntityIds' 17 | import schemasManager from '../../src/util/schemasManager' 18 | import { ContentTypes } from '../../src/constants' 19 | import { createPostRequest, completeRequest, failRequest } from '../../src/redux/actions' 20 | 21 | function setup (keyEntitiesBy) { 22 | const { kasiaReducer } = kasia({ 23 | wpapi: new Wpapi({ endpoint: '123' }), 24 | keyEntitiesBy 25 | }) 26 | const rootReducer = combineReducers(kasiaReducer) 27 | const store = createStore(rootReducer) 28 | return { store, initialState: initialState(keyEntitiesBy) } 29 | } 30 | 31 | describe('reducer journey', () => { 32 | const error = new Error('Request failed').stack 33 | 34 | const tests = [ 35 | ['id', 16], 36 | ['slug', 'architecto-enim-omnis-repellendus'] 37 | ] 38 | 39 | beforeEach(() => { 40 | queryCounter.reset() 41 | schemasManager.__flush__() 42 | }) 43 | 44 | tests.forEach(([ keyEntitiesBy, param ]) => { 45 | describe('keyEntitiesBy = ' + keyEntitiesBy, () => { 46 | const { store, initialState } = setup(keyEntitiesBy) 47 | 48 | it('has initial state on store', () => { 49 | expect(store.getState()).toEqual(initialState) 50 | }) 51 | 52 | it('does not modify store without action namespace', () => { 53 | store.dispatch({ type: 'REQUEST_COMPLETE', id: 0, entities: [''] }) 54 | expect(store.getState()).toEqual(initialState) 55 | }) 56 | 57 | it('can Request*Create', () => { 58 | store.dispatch(createPostRequest(ContentTypes.Post, param)) 59 | }) 60 | 61 | it('can RequestComplete', () => { 62 | store.dispatch(completeRequest(0, postJson)) 63 | const expected = normalise(postJson, keyEntitiesBy) 64 | const actual = store.getState().wordpress.entities 65 | expect(actual).toEqual(expected) 66 | }) 67 | 68 | it('places query on store', () => { 69 | const entities = pickEntityIds(postJson) 70 | const expected = { id: 0, complete: true, OK: true, paging: {}, entities, prepared: true } 71 | const actual = store.getState().wordpress.queries[0] 72 | expect(actual).toEqual(expected) 73 | }) 74 | 75 | it('can RequestFail', () => { 76 | store.dispatch(createPostRequest(ContentTypes.Post, param)) 77 | store.dispatch(failRequest(1, error)) 78 | const expected = { id: 1, complete: true, OK: false, error, prepared: true } 79 | const actual = store.getState().wordpress.queries[1] 80 | expect(actual).toEqual(expected) 81 | }) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kasia", 3 | "version": "4.1.0", 4 | "description": "A React Redux toolset for the WordPress API", 5 | "main": "lib/index.js", 6 | "jsnext:main": "src/index.js", 7 | "scripts": { 8 | "build": "rimraf lib && babel -d lib/ src/", 9 | "lint": "snazzy", 10 | "test": "jest", 11 | "test:watch": "jest --watch", 12 | "test:coverage": "jest --coverage", 13 | "example:simple": "node examples/server.js", 14 | "prepublish": "check-node-version --npm \">= 4.0.0\"", 15 | "prepublishOnly": "in-publish && npm run lint && npm run test && npm run build" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/outlandishideas/kasia.git" 20 | }, 21 | "keywords": [ 22 | "javascript", 23 | "react", 24 | "redux", 25 | "wordpress", 26 | "word", 27 | "press", 28 | "wp-api", 29 | "wp", 30 | "api", 31 | "saga" 32 | ], 33 | "author": "Outlandish ", 34 | "license": "MIT", 35 | "bugs": "https://github.com/outlandishideas/kasia/issues", 36 | "homepage": "https://github.com/outlandishideas/kasia#readme", 37 | "dependencies": { 38 | "humps": "2.0.0", 39 | "is-node-fn": "1.0.0", 40 | "lodash.merge": "4.3.5", 41 | "normalizr": "2.0.1", 42 | "pick-to-array": "1.0.0", 43 | "react": "15.0.1", 44 | "react-dom": "15.0.1", 45 | "react-redux": "5.0.2", 46 | "redux-saga": "0.13.0", 47 | "wpapi": "0.11.0" 48 | }, 49 | "devDependencies": { 50 | "babel-cli": "6.18.0", 51 | "babel-core": "6.18.2", 52 | "babel-eslint": "7.1.1", 53 | "babel-jest": "17.0.2", 54 | "babel-plugin-add-module-exports": "0.2.1", 55 | "babel-plugin-transform-class-properties": "^6.24.1", 56 | "babel-plugin-transform-decorators-legacy": "1.3.4", 57 | "babel-plugin-transform-es2015-classes": "^6.24.1", 58 | "babel-plugin-transform-proto-to-assign": "^6.23.0", 59 | "babel-polyfill": "6.7.4", 60 | "babel-preset-es2015": "6.6.0", 61 | "babel-preset-react": "6.5.0", 62 | "babel-preset-stage-0": "6.5.0", 63 | "check-node-version": "1.1.2", 64 | "coveralls": "2.11.15", 65 | "enzyme": "2.2.0", 66 | "in-publish": "2.0.0", 67 | "jest": "18.1.0", 68 | "jest-cli": "17.0.3", 69 | "lerna": "2.0.0-beta.31", 70 | "react-addons-test-utils": "15.3.0", 71 | "redux": "3.4.0", 72 | "rimraf": "2.5.2", 73 | "snazzy": "5.0.0", 74 | "standard": "8.6.0" 75 | }, 76 | "standard": { 77 | "env": [ 78 | "jasmine" 79 | ], 80 | "parser": "babel-eslint", 81 | "ignore": [ 82 | "/lib", 83 | "/packages" 84 | ] 85 | }, 86 | "jest": { 87 | "automock": true, 88 | "verbose": true, 89 | "testRegex": "/test/.*\\.js$", 90 | "unmockedModulePathPatterns": [ 91 | "/node_modules/react", 92 | "/node_modules/react-dom", 93 | "/node_modules/react-addons-test-utils", 94 | "/node_modules/enzyme", 95 | "/node_modules/lodash" 96 | ], 97 | "testPathIgnorePatterns": [ 98 | "/node_modules/", 99 | "/__mocks__/", 100 | "/__fixtures__/" 101 | ] 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/util/contentTypesManager.js: -------------------------------------------------------------------------------- 1 | import wpFilterMixins from 'wpapi/lib/mixins/filters' 2 | import wpParamMixins from 'wpapi/lib/mixins/parameters' 3 | import humps from 'humps' 4 | 5 | import getWP from '../wpapi' 6 | import invariants from '../invariants' 7 | import { WpApiNamespace, ContentTypes, ContentTypesPlural } from '../constants' 8 | 9 | /** Assert that an object has all `keys`. */ 10 | function hasKeys (obj, ...keys) { 11 | return keys.reduce((bool, key) => { 12 | if (!bool) return bool 13 | return obj.hasOwnProperty(key) 14 | }, true) 15 | } 16 | 17 | const optionsCache = new Map() 18 | 19 | const contentTypes = { register, get, getAll, derive } 20 | 21 | export default contentTypes 22 | 23 | // Pre-populate cache with built-in content type options. 24 | Object.keys(ContentTypes).forEach((key) => { 25 | const name = ContentTypes[key] 26 | const plural = ContentTypesPlural[name] 27 | const slug = ContentTypesPlural[name] 28 | contentTypes.register({ name, plural, slug }, true) 29 | }) 30 | 31 | /** Create and set options object for a type in the cache and register on wpapi instance. */ 32 | function register (contentType, builtIn) { 33 | invariants.isValidContentTypeObject(contentType) 34 | invariants.isNewContentType(contentTypes.getAll(), contentType) 35 | 36 | const { 37 | namespace = WpApiNamespace, 38 | name, plural, slug, 39 | route: _route, 40 | methodName: _methodName 41 | } = contentType 42 | 43 | const route = _route || `/${slug}/(?P)` 44 | const methodName = humps.camelize(_methodName || plural) 45 | const mixins = Object.assign({}, wpFilterMixins, wpParamMixins) 46 | const options = Object.assign({}, contentType, { route, methodName }) 47 | 48 | // Only register custom types with node-wpapi instance as built-ins are already available 49 | if (!builtIn) { 50 | const WP = getWP() 51 | WP[methodName] = WP.registerRoute(namespace, route, { mixins }) 52 | } 53 | 54 | optionsCache.set(name, options) 55 | } 56 | 57 | /** Get the options for a content type. */ 58 | function get (contentType) { 59 | return optionsCache.get(contentType) 60 | } 61 | 62 | /** Get all registered content types and their options. */ 63 | function getAll () { 64 | return optionsCache 65 | } 66 | 67 | /** Derive the content type of an entity from the WP-API. */ 68 | function derive (entity) { 69 | if (!entity) { 70 | throw new Error(`Expecting entity to be an object, got "${typeof entity}".`) 71 | } 72 | 73 | if (typeof entity.type !== 'undefined') { 74 | switch (entity.type) { 75 | case 'attachment': return ContentTypes.Media 76 | case 'comment': return ContentTypes.Comment 77 | case 'page': return ContentTypes.Page 78 | case 'post': return ContentTypes.Post 79 | default: return entity.type // Custom content type 80 | } 81 | } 82 | 83 | if (typeof entity.taxonomy !== 'undefined') { 84 | if (entity.taxonomy === 'post_tag') return ContentTypes.Tag 85 | if (entity.taxonomy === 'category') return ContentTypes.Category 86 | return ContentTypes.Taxonomy 87 | } 88 | 89 | if (entity.avatar_urls) return ContentTypes.User 90 | if (Array.isArray(entity.types)) return ContentTypes.Taxonomy 91 | if (hasKeys(entity, 'public', 'queryable', 'slug')) return ContentTypes.PostStatus 92 | if (hasKeys(entity, 'description', 'hierarchical', 'name')) return ContentTypes.PostType 93 | 94 | return null 95 | } 96 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-response-modify/README.md: -------------------------------------------------------------------------------- 1 | # Kasia Plugin WP-API Response Modify 2 | 3 | > Apply wp-api-response-modify to Kasia 4 | 5 | Made with ❤ at [@outlandish](http://www.twitter.com/outlandish) 6 | 7 | npm version 8 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 9 | 10 | ## Install 11 | 12 | ```sh 13 | npm install --save kasia-plugin-wp-api-response-modify 14 | ``` 15 | 16 | ## Import 17 | 18 | ```js 19 | // ES2015 20 | import KasiaWpApiResponseModifyPlugin from 'kasia-plugin-wp-api-response-modify' 21 | ``` 22 | 23 | ```js 24 | // CommonJS 25 | var KasiaWpApiResponseModifyPlugin = require('kasia-plugin-wp-api-response-modify') 26 | ``` 27 | 28 | ## Initialise 29 | 30 | Pass to Kasia via the `plugins` option: 31 | 32 | ```js 33 | const { kasiaReducer, kasiaSagas } = Kasia({ 34 | WP, 35 | plugins: [KasiaWpApiResponseModifyPlugin] 36 | }) 37 | ``` 38 | 39 | ## The Shape of Things 40 | 41 | With this plugin Kasia restructures the [shape of things](https://www.youtube.com/watch?v=Zn2JFlteeJ0) returned from the WP-API. 42 | 43 | The changes made to the data are all effects available in the 44 | [`wp-api-response-modify`](https://github.com/outlandishideas/wp-api-response-modify) library. 45 | 46 | ### Why? 47 | 48 | The JSON returned from WP-API contains such things as objects with a single property (e.g. objects with `rendered`) 49 | and meta data property names prefixed with an underscore (e.g. `_links`). 50 | 51 | ### What changes should I be aware of? 52 | 53 | - All property names are camel-cased. 54 | 55 | ```js 56 | "featured_media" => "featuredMedia" 57 | ``` 58 | 59 | - Links are removed. 60 | 61 | ```js 62 | { title: 'Wow what an amazing title!', _links: {}, ... } 63 | // becomes... 64 | { title: 'Wow what an amazing title!', ... } 65 | ``` 66 | 67 | - Objects that have a single property `'rendered'` are flattened. 68 | 69 | ```js 70 | { content: { rendered: '

Hello, World!

' }, ... } 71 | // becomes... 72 | { content: '

Hello, World!

', ... } 73 | ``` 74 | 75 | - Content types are normalised using [`normalizr`](https://github.com/paularmstrong/normalizr). 76 | This means that any embedded content data is made available on the store within its respective content type collection. 77 | For example: 78 | 79 | ```js 80 | { 81 | posts: {}, 82 | users: {}, 83 | pages: {}, 84 | news: {}, // custom content type 85 | ... 86 | } 87 | ``` 88 | 89 | ## Contributing 90 | 91 | All pull requests and issues welcome! 92 | 93 | - When submitting an issue please provide adequate steps to reproduce the problem. 94 | - PRs must be made using the `standard` code style. 95 | - PRs must update the version of the library according to [semantic versioning](http://semver.org/). 96 | 97 | If you're not sure how to contribute, check out Kent C. Dodds' 98 | [great video tutorials on egghead.io](https://egghead.io/lessons/javascript-identifying-how-to-contribute-to-an-open-source-project-on-github)! 99 | 100 | ## Author & License 101 | 102 | `kasia-plugin-wp-api-response-modify` was created by [Outlandish](https://twitter.com/outlandish) and is released under the MIT license. 103 | -------------------------------------------------------------------------------- /src/invariants.js: -------------------------------------------------------------------------------- 1 | const NODE_WPAPI_GITHUB_URL = 'http://bit.ly/2adfKKg' 2 | const KASIA_URL = 'http://kasia.io' 3 | 4 | function invariant (predicate, message, ...args) { 5 | if (!predicate) { 6 | const interpolated = args.reduce((str, arg) => str.replace(/%s/, arg), message) 7 | const err = new Error('[kasia] ' + interpolated) 8 | err.framesToPop = 1 9 | throw err 10 | } 11 | } 12 | 13 | export default { 14 | isString: (name, value) => invariant( 15 | typeof value === 'string', 16 | 'Expecting %s to be string, got "%s".', 17 | name, typeof value 18 | ), 19 | isFunction: (name, value) => invariant( 20 | typeof value === 'function', 21 | 'Expecting %s to be function, got "%s".', 22 | name, typeof value 23 | ), 24 | isPlugin: (name, value) => invariant( 25 | typeof value === 'function', 26 | 'Expecting %s to be function, got "%s". ' + 27 | 'Please file an issue with the plugin if you ' + 28 | 'think there might be a problem with it.', 29 | name, typeof value 30 | ), 31 | isArray: (name, value) => invariant( 32 | Array.isArray(value), 33 | 'Expecting %s to be array, got "%s".', 34 | name, typeof value 35 | ), 36 | isBoolean: (name, value) => invariant( 37 | typeof value === 'object', 38 | 'Expecting %s to be boolean, got "%s".', 39 | name, typeof value 40 | ), 41 | isWpApiInstance: (value = {}) => invariant( 42 | typeof value.registerRoute === 'function', 43 | 'Expecting WP to be instance of `node-wpapi`. ' + 44 | `See documentation: ${NODE_WPAPI_GITHUB_URL}.` 45 | ), 46 | isIdentifierArg: (identifier) => invariant( 47 | typeof identifier === 'function' || typeof identifier === 'string' || typeof identifier === 'number', 48 | 'Expecting id given to connectWpPost to be function/string/number, got "%s".', 49 | typeof identifier 50 | ), 51 | isValidContentTypeObject: (obj) => invariant( 52 | typeof obj.name === 'string' && 53 | typeof obj.plural === 'string' && 54 | typeof obj.slug === 'string', 55 | 'Invalid content type object. ' + 56 | `See documentation: ${KASIA_URL}.` 57 | ), 58 | isValidContentType: (contentTypeOptions, name, checkStr) => invariant( 59 | typeof contentTypeOptions !== 'undefined', 60 | 'Content type "%s" is not recognised. ' + 61 | 'Pass built-ins from `kasia/types`, e.g. `connectWpPost(Post, ...)`. ' + 62 | 'Pass the name of custom content types, e.g. `connectWpPost("Book", ...)`. ' + 63 | 'Check %s.', 64 | name, checkStr 65 | ), 66 | isNewContentType: (typesMap, contentType) => invariant( 67 | typesMap && !typesMap.get(contentType.name), 68 | 'Content type with name "%s" already exists.', 69 | contentType.name 70 | ), 71 | isNotWrapped: (target, displayName) => invariant( 72 | !target.__kasia, 73 | '%s is already wrapped by Kasia.', 74 | displayName 75 | ), 76 | isIdentifierValue: (id) => invariant( 77 | typeof id === 'string' || typeof id === 'number', 78 | 'The final identifier is invalid. ' + 79 | 'Expecting a string or number, got "%s".', 80 | typeof id 81 | ), 82 | hasWordpressObject: (wordpress) => invariant( 83 | wordpress, 84 | 'No `wordpress` object on the store. ' + 85 | 'Is your store configured correctly? ' + 86 | `See documentation ${KASIA_URL}.`, 87 | typeof wordpress 88 | ), 89 | queryHasError: (query, displayName) => { 90 | if (query && query.error) { 91 | console.log(`[kasia] error in query for ${displayName || 'unknown component'}:\n` + query.error) 92 | } 93 | }, 94 | isKeyEntitiesByOption: (keyEntitiesBy) => invariant( 95 | keyEntitiesBy === 'slug' || keyEntitiesBy === 'id', 96 | 'Expecting keyEntitiesBy to be "slug" or "id", got "%s".', 97 | keyEntitiesBy 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /packages/kasia-plugin-wp-api-menus/src/index.js: -------------------------------------------------------------------------------- 1 | import { take, call, put } from 'redux-saga/effects' 2 | import merge from 'lodash.merge' 3 | 4 | import ActionTypes from './constants/ActionTypes' 5 | 6 | export * from './actions' 7 | 8 | export default kasiaPluginWpApiMenus 9 | 10 | // The default wp-api-menus namespace in the WP-API. 11 | const defaultRoute = 'wp-api-menus/v2' 12 | 13 | // Set of action type names that is used to capture 14 | // only actions of these type in the reducer. 15 | const requestTypes = [ 16 | ActionTypes.REQUEST_MENU, 17 | ActionTypes.REQUEST_MENUS, 18 | ActionTypes.REQUEST_LOCATION, 19 | ActionTypes.REQUEST_LOCATIONS 20 | ] 21 | 22 | const reducer = { 23 | // RECEIVE DATA 24 | // Place data in the store according to the type of data that was requested. 25 | [ActionTypes.RECEIVE_DATA]: (state, action) => { 26 | switch (action.request) { 27 | case ActionTypes.REQUEST_MENU: 28 | return merge({}, state, { menus: { [action.id]: action.data } }) 29 | case ActionTypes.REQUEST_MENUS: 30 | return merge({}, state, { menus: action.data }) 31 | case ActionTypes.REQUEST_LOCATION: 32 | return merge({}, state, { menuLocations: { [action.id]: action.data } }) 33 | case ActionTypes.REQUEST_LOCATIONS: 34 | return merge({}, state, { menuLocations: action.data }) 35 | default: 36 | return state 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * Perform the actual request for data from wp-api-menus. 43 | * @param {Object} WP Instance of `wpapi` 44 | * @param {Object} action Action object 45 | * @returns {Promise} Resolves to response data 46 | */ 47 | function fetch (WP, action) { 48 | switch (action.type) { 49 | case ActionTypes.REQUEST_MENU: 50 | return typeof action.id === 'string' 51 | ? WP.menus().slug(action.id).get() 52 | : WP.menus().id(action.id).get() 53 | case ActionTypes.REQUEST_MENUS: 54 | return WP.menus().get() 55 | case ActionTypes.REQUEST_LOCATION: 56 | return WP.locations().id(action.id).get() 57 | case ActionTypes.REQUEST_LOCATIONS: 58 | return WP.locations().get() 59 | default: 60 | throw new Error(`Unknown request type "${action.request}".`) 61 | } 62 | } 63 | 64 | /** 65 | * Produce the plugin's own sagas array. 66 | * A single saga that deals with requests for wp-api-menus data. 67 | * @param {Object} WP Instance of `wpapi` 68 | * @returns {Array} Array of sagas 69 | */ 70 | function makeSagas (WP) { 71 | return [function * fetchSaga () { 72 | while (true) { 73 | const action = yield take((action) => requestTypes.indexOf(action.type) !== -1) 74 | yield fetchResource(WP, action) 75 | } 76 | }] 77 | } 78 | 79 | /** 80 | * Orchestrate a request for wp-api-menus data. 81 | * @param {Object} WP Instance of `wpapi` 82 | * @param {Object} action Action object 83 | */ 84 | function * fetchResource (WP, action) { 85 | const { id, type } = action 86 | const data = yield call(fetch, WP, action) 87 | yield put({ type: ActionTypes.RECEIVE_DATA, request: type, data, id }) 88 | } 89 | 90 | /** 91 | * Initialise the plugin, returning the plugins own reducer and sagas. 92 | * @param {Object} WP Instance of `wpapi` 93 | * @param {Object} config User's plugin configuration 94 | * @returns {Object} Plugin reducer and sagas 95 | */ 96 | function kasiaPluginWpApiMenus (WP, config) { 97 | config.route = config.route || defaultRoute 98 | 99 | WP.menus = WP.registerRoute(config.route, '/menus/(?P)') 100 | WP.locations = WP.registerRoute(config.route, '/menu-locations/(?P)') 101 | 102 | return { 103 | reducers: reducer, 104 | sagas: makeSagas(WP) 105 | } 106 | } 107 | 108 | kasiaPluginWpApiMenus.preload = function (WP, action) { 109 | return function * () { 110 | yield * fetchResource(WP, action) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/util/schemasManager.js: -------------------------------------------------------------------------------- 1 | import { Schema, arrayOf } from 'normalizr' 2 | 3 | import { ContentTypes, ContentTypesPlural } from '../constants' 4 | 5 | const schemasManager = { __flush__, getAll, createSchema, init } 6 | 7 | export default schemasManager 8 | 9 | /** Schema object cache, populated in `makeSchemas`. */ 10 | let schemas 11 | 12 | /** Individual schema definitions, defined like this so we can reference one from another. */ 13 | let categorySchema, mediaSchema, pageSchema, postSchema, revisionSchema, tagSchema, userSchema, 14 | commentSchema, postTypeSchema, postStatusSchema, taxonomySchema 15 | 16 | /** Invalidate the schema object cache. */ 17 | function __flush__ () { 18 | schemas = null 19 | } 20 | 21 | /** Get the schema object cache. */ 22 | function getAll () { 23 | return schemas 24 | } 25 | 26 | /** Create a custom schema definition (for custom content types). */ 27 | function createSchema (name, idAttribute) { 28 | if (!schemas) { 29 | throw new Error('createSchema called before schema cache populated, call makeSchemas first.') 30 | } else if (typeof name !== 'string') { 31 | throw new Error(`Expecting name to be a string, got "${typeof name}".`) 32 | } else if (typeof idAttribute !== 'string') { 33 | throw new Error(`Expecting idAttribute to be a string, got "${typeof idAttribute}".`) 34 | } 35 | 36 | const contentTypeSchema = new Schema(name, { idAttribute }) 37 | 38 | contentTypeSchema.define({ 39 | author: userSchema, 40 | post: postSchema, 41 | featuredMedia: mediaSchema 42 | }) 43 | 44 | return contentTypeSchema 45 | } 46 | 47 | /** Populate the cache of schemas for built-in content types. */ 48 | function init (idAttribute) { 49 | if (typeof idAttribute !== 'string') { 50 | throw new Error(`Expecting idAttribute to be a string, got "${typeof idAttribute}".`) 51 | } 52 | 53 | if (schemas) return schemas 54 | 55 | // Content types with `id` properties 56 | categorySchema = new Schema(ContentTypesPlural[ContentTypes.Category], { idAttribute }) 57 | commentSchema = new Schema(ContentTypesPlural[ContentTypes.Comment], { idAttribute }) 58 | mediaSchema = new Schema(ContentTypesPlural[ContentTypes.Media], { idAttribute }) 59 | pageSchema = new Schema(ContentTypesPlural[ContentTypes.Page], { idAttribute }) 60 | postSchema = new Schema(ContentTypesPlural[ContentTypes.Post], { idAttribute }) 61 | revisionSchema = new Schema(ContentTypesPlural[ContentTypes.PostRevision], { idAttribute }) 62 | tagSchema = new Schema(ContentTypesPlural[ContentTypes.Tag], { idAttribute }) 63 | userSchema = new Schema(ContentTypesPlural[ContentTypes.User], { idAttribute }) 64 | 65 | // Content types without `id` properties 66 | postTypeSchema = new Schema(ContentTypesPlural[ContentTypes.PostType], { idAttribute: 'slug' }) 67 | postStatusSchema = new Schema(ContentTypesPlural[ContentTypes.PostStatus], { idAttribute: 'slug' }) 68 | taxonomySchema = new Schema(ContentTypesPlural[ContentTypes.Taxonomy], { idAttribute: 'slug' }) 69 | 70 | mediaSchema.define({ 71 | author: userSchema, 72 | post: postSchema 73 | }) 74 | 75 | pageSchema.define({ 76 | author: userSchema, 77 | featuredMedia: mediaSchema 78 | }) 79 | 80 | postSchema.define({ 81 | author: userSchema, 82 | featuredMedia: mediaSchema, 83 | categories: arrayOf(categorySchema), 84 | tags: arrayOf(tagSchema) 85 | }) 86 | 87 | schemas = { 88 | [ContentTypes.Category]: categorySchema, 89 | [ContentTypes.Comment]: commentSchema, 90 | [ContentTypes.Media]: mediaSchema, 91 | [ContentTypes.Page]: pageSchema, 92 | [ContentTypes.Post]: postSchema, 93 | [ContentTypes.PostStatus]: postStatusSchema, 94 | [ContentTypes.PostType]: postTypeSchema, 95 | [ContentTypes.PostRevision]: revisionSchema, 96 | [ContentTypes.Tag]: tagSchema, 97 | [ContentTypes.Taxonomy]: taxonomySchema, 98 | [ContentTypes.User]: userSchema 99 | } 100 | 101 | return schemas 102 | } 103 | -------------------------------------------------------------------------------- /test/__fixtures__/wp-api-responses/comment.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'id': 16, 3 | 'post': 43, 4 | 'parent': 0, 5 | 'author': 0, 6 | 'author_name': 'Dereck Friesen', 7 | 'author_url': 'https://www.Kris.com/alias-explicabo-recusandae-architecto-aliqua', 8 | 'date': '2015-05-27T02:12:29', 9 | 'date_gmt': '2015-05-27T02:12:29', 10 | 'content': { 11 | 'rendered': '

Molestiae aliquam eligendi facere officiis quae impedit eos. Sapiente quas non eum aut. Alias quam similique odio in cupiditate. Ut aut aut delectus officia iusto error maxime neque.

\n

Consequatur eius ut aut vero culpa. Beatae delectus qui culpa occaecati maiores. Tempora voluptas quo inventore temporibus consectetur vitae. Voluptatem sit expedita et harum dolores. Dolorum magni sequi officiis temporibus.

\n

Perferendis dolor sit rem. Omnis repudiandae quia minima animi sequi. Aspernatur recusandae quod ea tempora.

\n' 12 | }, 13 | 'link': 'http://demo.wp-api.org/2015/05/17/voluptates-quis-ut-qui/#comment-16', 14 | 'status': 'approved', 15 | 'type': 'comment', 16 | 'author_avatar_urls': { 17 | '24': 'http://0.gravatar.com/avatar/fe6e007015afc17a401d8ea853a3e05c?s=24&d=mm&r=g', 18 | '48': 'http://0.gravatar.com/avatar/fe6e007015afc17a401d8ea853a3e05c?s=48&d=mm&r=g', 19 | '96': 'http://0.gravatar.com/avatar/fe6e007015afc17a401d8ea853a3e05c?s=96&d=mm&r=g' 20 | }, 21 | '_links': { 22 | 'self': [{ 23 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/comments/16' 24 | }], 25 | 'collection': [{ 26 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/comments' 27 | }], 28 | 'up': [{ 29 | 'embeddable': true, 30 | 'post_type': 'post', 31 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/posts/43' 32 | }] 33 | }, 34 | '_embedded': { 35 | 'up': [{ 36 | 'id': 43, 37 | 'date': '2015-05-17T12:30:17', 38 | 'slug': 'voluptates-quis-ut-qui', 39 | 'type': 'post', 40 | 'link': 'http://demo.wp-api.org/2015/05/17/voluptates-quis-ut-qui/', 41 | 'title': { 42 | 'rendered': 'Voluptates quis ut qui' 43 | }, 44 | 'excerpt': { 45 | 'rendered': '

Omnis quia quam aliquid fugit. Saepe voluptas excepturi ratione corrupti cum. Animi asperiores qui nulla ut mollitia. Et neque eos aut magni et id vel Modi consequatur et qui perferendis. Dolor adipisci quo. Sequi autem illo deserunt excepturi enim ut. Vel dolores aperiam et ullam facilis. Maiores illo quis dolorem iure fugit non Magnam et […]

\n' 46 | }, 47 | 'author': 44, 48 | '_links': { 49 | 'self': [{ 50 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/posts/43' 51 | }], 52 | 'collection': [{ 53 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/posts' 54 | }], 55 | 'about': [{ 56 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/types/post' 57 | }], 58 | 'author': [{ 59 | 'embeddable': true, 60 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users/44' 61 | }], 62 | 'replies': [{ 63 | 'embeddable': true, 64 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/comments?post=43' 65 | }], 66 | 'version-history': [{ 67 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/posts/43/revisions' 68 | }], 69 | 'wp:featuredmedia': [{ 70 | 'embeddable': true, 71 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media/42' 72 | }], 73 | 'wp:attachment': [{ 74 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media?parent=43' 75 | }], 76 | 'wp:term': [{ 77 | 'taxonomy': 'category', 78 | 'embeddable': true, 79 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/categories?post=43' 80 | }, 81 | { 82 | 'taxonomy': 'post_tag', 83 | 'embeddable': true, 84 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/tags?post=43' 85 | }], 86 | 'curies': [{ 87 | 'name': 'wp', 88 | 'href': 'https://api.w.org/{rel}', 89 | 'templated': true 90 | }] 91 | } 92 | }] 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/util/normalise.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import '../__mocks__/WP' 6 | import normalise from '../../src/util/normalise' 7 | import contentTypesManager from '../../src/util/contentTypesManager' 8 | import schemasManager from '../../src/util/schemasManager' 9 | import { ContentTypes } from '../../src/constants' 10 | 11 | function setup () { 12 | const testKeyById = true 13 | const testKeyBySlug = true 14 | 15 | contentTypesManager.register({ 16 | name: 'book', 17 | plural: 'books', 18 | slug: 'books' 19 | }) 20 | 21 | return { 22 | [ContentTypes.Category]: { 23 | // The expected entity collections on the store 24 | collections: ['categories'], 25 | // Whether to test normalisation by 'id' attr. 26 | testKeyById, 27 | // Whether to test normalisation by 'slug' attr. 28 | testKeyBySlug 29 | }, 30 | [ContentTypes.Comment]: { 31 | collections: ['comments'], 32 | testKeyById 33 | }, 34 | [ContentTypes.Media]: { 35 | collections: ['media'], 36 | testKeyById 37 | }, 38 | [ContentTypes.Page]: { 39 | collections: ['pages'], 40 | testKeyById, 41 | testKeyBySlug 42 | }, 43 | [ContentTypes.Post]: { 44 | collections: ['posts'], 45 | testKeyById, 46 | testKeyBySlug 47 | }, 48 | [ContentTypes.PostStatus]: { 49 | collections: ['statuses'], 50 | testKeyBySlug 51 | }, 52 | [ContentTypes.PostType]: { 53 | collections: ['types'], 54 | testKeyBySlug 55 | }, 56 | [ContentTypes.Tag]: { 57 | collections: ['tags'], 58 | testKeyById, 59 | testKeyBySlug 60 | }, 61 | [ContentTypes.Taxonomy]: { 62 | collections: ['taxonomies'], 63 | testKeyBySlug 64 | }, 65 | [ContentTypes.User]: { 66 | collections: ['users'], 67 | testKeyById, 68 | testKeyBySlug 69 | }, 70 | book: { 71 | collections: ['books'], 72 | testKeyById, 73 | testKeyBySlug 74 | } 75 | } 76 | } 77 | 78 | function fixtures (contentType) { 79 | const first = require('../__fixtures__/wp-api-responses/' + contentType).default 80 | 81 | // Imitate another entity by modifying identifiers 82 | const second = Object.assign({}, first, { 83 | id: first.id + 1, 84 | slug: first.slug + '1' 85 | }) 86 | 87 | return { 88 | first, 89 | second, 90 | multiple: [first, second] 91 | } 92 | } 93 | 94 | describe('util/normalise', () => { 95 | const tests = setup() 96 | 97 | Object.keys(tests).forEach((contentType) => { 98 | describe('Normalise ' + contentType, () => { 99 | const { plural } = contentTypesManager.get(contentType) 100 | const { first, second, multiple } = fixtures(contentType) 101 | const { collections, testKeyBySlug, testKeyById } = tests[contentType] 102 | 103 | afterEach(() => schemasManager.__flush__()) 104 | 105 | if (testKeyById) { 106 | it(`should normalise single "${contentType}" by id`, () => { 107 | const result = normalise(first, 'id') 108 | const actual = Object.keys(result) 109 | expect(actual).toEqual(collections) 110 | }) 111 | 112 | it(`should normalise multiple "${contentType}" by id`, () => { 113 | const result = normalise(multiple, 'id') 114 | const actual = Object.keys(result[plural]) 115 | const expected = [first.id, second.id].map(String) 116 | expect(actual).toEqual(expected) 117 | }) 118 | } 119 | 120 | if (testKeyBySlug) { 121 | it(`should normalise single "${contentType}" by slug`, () => { 122 | const result = normalise(first, 'slug') 123 | const actual = Object.keys(result) 124 | expect(actual).toEqual(collections) 125 | }) 126 | 127 | it(`should normalise multiple "${contentType}" by slug`, () => { 128 | const result = normalise(multiple, 'slug') 129 | const actual = Object.keys(result[plural]) 130 | const expected = [first.slug, second.slug] 131 | expect(actual).toEqual(expected) 132 | }) 133 | } 134 | }) 135 | }) 136 | }) 137 | -------------------------------------------------------------------------------- /src/redux/reducer.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge' 2 | import isNode from 'is-node-fn' 3 | 4 | import pickEntityIds from '../util/pickEntityIds' 5 | import normalise from '../util/normalise' 6 | import { ActionTypes } from '../constants' 7 | 8 | export const INITIAL_STATE = { 9 | // WP-API request/response metadata are stored here 10 | queries: {}, 11 | // Entities are normalised and stored here 12 | entities: {} 13 | } 14 | 15 | /** 16 | * Merge all native and plugin reducers such that a single function reduces for a single action type. 17 | * @param {Object} reducers Plugin reducers 18 | * @param {Function} normaliser Function to normalise response data 19 | * @returns {Object} Reducer object 20 | */ 21 | function mergeNativeAndThirdPartyReducers (reducers, normaliser) { 22 | const baseReducer = { 23 | [ActionTypes.AckRequest]: [acknowledgeReducer], 24 | [ActionTypes.RequestComplete]: [completeReducer(normaliser)], 25 | [ActionTypes.RequestFail]: [failReducer] 26 | } 27 | 28 | // Group reducers by their action type 29 | const reducersByActionType = Object.keys(reducers) 30 | .reduce(function groupByActionType (reducer, actionType) { 31 | reducer[actionType] = [].concat( 32 | reducer[actionType] || [], 33 | reducers[actionType] || [] 34 | ) 35 | return reducer 36 | }, baseReducer) 37 | 38 | const reducer = {} 39 | 40 | // Produce a single function for each action type 41 | for (const actionType in reducersByActionType) { 42 | if (reducersByActionType[actionType].length > 1) { 43 | // Call each reducer function in succession, passing the state returned from each to the next 44 | reducer[actionType] = (state, action) => { 45 | return reducersByActionType[actionType].reduce((state, fn) => fn(state, action), state) 46 | } 47 | } else { 48 | // Take the first and only function as the whole reducer 49 | reducer[actionType] = reducersByActionType[actionType][0] 50 | } 51 | } 52 | 53 | return reducer 54 | } 55 | 56 | // ACKNOWLEDGE 57 | // Place record of request o 58 | export function acknowledgeReducer (state, action) { 59 | return merge({}, state, { 60 | queries: { 61 | [action.id]: { 62 | id: action.id, 63 | prepared: isNode(), 64 | complete: false, 65 | OK: null 66 | } 67 | } 68 | }) 69 | } 70 | 71 | // COMPLETE 72 | // Place entity on the store; update query record if for component (has an id) 73 | export function completeReducer (normalise) { 74 | return (state_, action) => { 75 | const state = merge({}, state_) 76 | 77 | state.entities = merge( 78 | state.entities, 79 | normalise(action.data) 80 | ) 81 | 82 | // The action id would be null if the preloadQuery method has initiated 83 | // the completeRequest action as they do not need a query in the store 84 | // (there is no component to pick it up). 85 | if (typeof action.id === 'number') { 86 | state.queries[action.id] = { 87 | id: action.id, 88 | entities: pickEntityIds(action.data), 89 | paging: action.data._paging || {}, 90 | prepared: isNode(), 91 | complete: true, 92 | OK: true 93 | } 94 | } 95 | 96 | return state 97 | } 98 | } 99 | 100 | // FAIL 101 | // Update query record only 102 | export function failReducer (state, action) { 103 | return merge({}, state, { 104 | queries: { 105 | [action.id]: { 106 | id: action.id, 107 | error: action.error, 108 | prepared: isNode(), 109 | complete: true, 110 | OK: false 111 | } 112 | } 113 | }) 114 | } 115 | 116 | /** 117 | * Create the aggregate reducer for an instance of Kasia. 118 | * @param {Object} keyEntitiesBy Entity property used as key in store 119 | * @param {Object} reducers Plugin reducers 120 | * @returns {Object} Kasia reducer 121 | */ 122 | export default function createReducer ({ keyEntitiesBy, reducers }) { 123 | const normaliser = (data) => normalise(data, keyEntitiesBy) 124 | const reducer = mergeNativeAndThirdPartyReducers(reducers, normaliser) 125 | const initialState = Object.assign({}, INITIAL_STATE, { keyEntitiesBy }) 126 | 127 | return { 128 | wordpress: function kasiaReducer (state = initialState, action) { 129 | const [ actionNamespace ] = action.type.split('/') 130 | 131 | if (actionNamespace === 'kasia' && action.type in reducer) { 132 | return reducer[action.type](state, action) 133 | } 134 | 135 | return state 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Kasia Changelog 2 | 3 | - __v4.0.0__ 4 | 5 | - [BREAKING] WP API responses no longer modified by `wp-api-response-modify` by default. Functionality moved into [`kasia-plugin-wp-api-response-modify`](https://github.com/outlandishideas/kasia/tree/master/packages/kasia-plugin-wp-api-response-modify) plugin. 6 | - [BREAKING] Structure of `props.kasia` object changed: `entities` key is now `data`. This is to ready the library for [#30](https://github.com/outlandishideas/kasia/issues/30), where query result may not be a normalised collection of entities. 7 | - [BREAKING] `shouldUpdate` function is mandatory for `connectWpQuery` decorator. 8 | - [BREAKING] `WP` config option is now `wpapi`. 9 | - [BREAKING] Preloaders renamed and fn signatures changes (see docs). `makePostPreloaderSaga` removed, use `preloadQuery` instead. 10 | - Provide stack trace for captured query errors. ([#37](https://github.com/outlandishideas/kasia/issues/37)) 11 | - Plugins can intercept native reducers, e.g. `kasia-plugin-wp-api-response-modify`. ([#40](https://github.com/outlandishideas/kasia/issues/40)) 12 | - `prepublish` scripts not run on `npm install`. ([#28](https://github.com/outlandishideas/kasia/issues/28)) 13 | - Added `debug` config option, if true lib logs useful lifecycle information to the console. 14 | - Big refactor of library to make codebase more maintainable, manageable. 15 | 16 | --- 17 | 18 | - __v3.2.0__ - _23/09/16_ 19 | 20 | - Implemented safer internal query reconciliation logic such that prepared queries 21 | target their components via their `displayName`. 22 | - Added `options` object parameter to connect decorators, where you can specify explicitly the `displayName` 23 | of the component if it is wrapped by other decorators. 24 | - Fix bug where preloader saga creator utilities in `kasia/util` disrupt prepared query reconciliation 25 | by incrementing prepared query count. (They do not have a corresponding component and so should not be 26 | considered "prepared" queries.) 27 | - Fixed query errors not being added to query object (`error: "undefined"`). 28 | - Log warning about erroneous queries. 29 | 30 | - __v3.1.5__ - _22/09/16_ 31 | 32 | - Replace use of redux's `connect` decorator with access to the store via `context`. 33 | - Quick fix for failed queries sneaking through to data reconciliation stage in decorators. 34 | TODO: implement more complete query error-handling solution. 35 | ([#29](https://github.com/outlandishideas/kasia/issues/29)) 36 | 37 | - __v3.1.2__ - _20/09/16_ 38 | 39 | - Pass state as third argument to `connectWpQuery` query function. 40 | 41 | - __v3.1.1__ - _19/09/16_ 42 | 43 | - Fix bug where `wordpress` object removed from props during comparison to determine if 44 | a request for new data should be made in `connectWpQuery`. 45 | 46 | - __v3.1.0__ - _09/09/16_ 47 | 48 | - Additional preloader saga creators for loading WP data into the store when server-side rendering: 49 | `makeQueryPreloaderSaga` and `makePostPreloaderSaga`. 50 | 51 | - __v3.0.0__ - _31/08/16_ 52 | 53 | - [BREAKING] Fix bug in Universal Application solution that prevented components picking 54 | up prepared query data on the client. Also involves change in API that means `ConnectComponent.makePreloader` returns array 55 | immediately instead of a function. ([#24](https://github.com/outlandishideas/kasia/issues/24)) 56 | - Improved test coverage of universal application solution. ([#26](https://github.com/outlandishideas/kasia/issues/26)) 57 | - Additional utility export `kasia/util`, currently only exporting `makePreloaderSaga()` for use with server-side rendering. 58 | - Improved developer feedback. ([#4](https://github.com/outlandishideas/kasia/issues/4), [#24](https://github.com/outlandishideas/kasia/issues/23)) 59 | - Updates to README: better documentation of Universal Application solution. ([#25](https://github.com/outlandishideas/kasia/issues/25)) 60 | - Update to latest version of Jest. 61 | 62 | --- 63 | 64 | - __v2.4.0__ - _10/08/16_ 65 | 66 | - Mixin `node-wpapi`'s available mixins to internal calls to `registerRoute` by default in order 67 | that filtering can be performed on custom content types, e.g. `news.filter().get()`. 68 | 69 | - __v2.3.0__ - _10/08/16_ 70 | 71 | - Fix functions passed as props to `connectWpQuery` causing infinite dispatching. 72 | ([#16](https://github.com/outlandishideas/kasia/issues/16)) 73 | - Added second parameter `propsComparatorFn` to `connectWpQuery`. 74 | - Fix `connectWpPost` derived query chaining failing due to dependency on object property order. 75 | - Updates to README: added docs for new functionality, fix typos 76 | ([#19](https://github.com/outlandishideas/kasia/pull/19)). 77 | 78 | - __v2.2.0__ - _08/08/16_ 79 | 80 | - Added missing dependencies in `package.json`. 81 | - Removed unnecessary unmock of `lodash` in tests. 82 | - Fix query IDs being picked in reverse order when server-side rendering. 83 | - Added `.travis.yml`. 84 | 85 | - __v2.1.0__ - _05/08/16_ 86 | 87 | - Fix `connectWpQuery` fn not receiving props. 88 | 89 | - __v2.0.0__ - _04/08/16_ 90 | 91 | - [BREAKING] Updated sagas export to accommodate changes to redux-saga API introduced in [v0.10.0](). 92 | - Updates to README: fixed bad `node-wpapi` examples with endpoint missing `/wp-json` 93 | suffix, added quick Spongebob example as intro. :ok_hand: 94 | - Added `CHANGELOG.md`. 95 | 96 | --- 97 | 98 | - __v1.0.2__ - _04/08/16_ 99 | 100 | - Removed `postinstall` npm script because it runs before dependencies are installed(?!). 101 | 102 | - __v1.0.1__ - _04/08/16_ 103 | 104 | - Fix bad import statements (see next bullet). 105 | - Rename `ContentTypes.js` -> `contentTypes.js` as this was causing an error on 106 | Unix systems as import statements used the latter filename. 107 | 108 | - __v1.0.0__ - _03/08/16_ 109 | 110 | - Release! :tophat: 111 | -------------------------------------------------------------------------------- /test/util/preload.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | import kasia from '../../src' 4 | 5 | jest.disableAutomock() 6 | 7 | import Wpapi from 'wpapi' 8 | import createSagaMiddleware from 'redux-saga' 9 | import { join, fork } from 'redux-saga/effects' 10 | import { createMockTask } from 'redux-saga/utils' 11 | import { createStore as _createStore, applyMiddleware, compose, combineReducers } from 'redux' 12 | 13 | import '../__mocks__/WP' 14 | import { ActionTypes } from '../../src/constants' 15 | import { fetch } from '../../src/redux/sagas' 16 | import { preload, preloadQuery } from '../../src/util/preload' 17 | import { wrapQueryFn } from '../../src/connect' 18 | import queryCounter from '../../src/util/queryCounter' 19 | import runSagas from '../../src/util/runSagas' 20 | 21 | import initialState from '../__mocks__/states/initial' 22 | import bookJson from '../__fixtures__/wp-api-responses/book' 23 | import ConnectPostC from '../__mocks__/components/BuiltInContentType' 24 | import ConnectQueryC, { queryFn } from '../__mocks__/components/CustomQuery' 25 | import ConnectQueryNestedC from '../__mocks__/components/CustomQueryNestedPreload' 26 | 27 | function setup (keyEntitiesBy) { 28 | queryCounter.reset() 29 | 30 | const { kasiaReducer, kasiaSagas } = kasia({ 31 | wpapi: new Wpapi({ endpoint: '123' }), 32 | keyEntitiesBy 33 | }) 34 | 35 | const sagaMiddleware = createSagaMiddleware() 36 | const createStore = compose(applyMiddleware(sagaMiddleware))(_createStore) 37 | const store = createStore(combineReducers(kasiaReducer), initialState(keyEntitiesBy)) 38 | const runSaga = sagaMiddleware.run 39 | 40 | sagaMiddleware.run(function * () { 41 | yield kasiaSagas 42 | }) 43 | 44 | store.runSaga = runSaga 45 | 46 | return { store, runSaga } 47 | } 48 | 49 | describe('util/preload', () => { 50 | describe('#preload', () => { 51 | const props = { params: { id: 16 } } 52 | const components = [ 53 | class extends ConnectPostC {}, // test can discover wrapped kasia component 54 | ConnectQueryC, // unwrapped kasia component 55 | false // test ignores non-component values 56 | ] 57 | 58 | let iter 59 | let res 60 | 61 | it('throws with bad components', () => { 62 | expect(() => preload({}, '')).toThrowError(/Expecting components to be array/) 63 | }) 64 | 65 | it('throws with bad renderProps', () => { 66 | expect(() => preload([], '')).toThrowError(/Expecting renderProps to be an object/) 67 | }) 68 | 69 | it('throws with bad state', () => { 70 | expect(() => preload([], {}, '')).toThrowError(/Expecting state to be an object/) 71 | }) 72 | 73 | it('returns generator', () => { 74 | const actual = preload(components, props) 75 | 76 | iter = actual() 77 | res = iter.next() 78 | 79 | expect(typeof actual).toEqual('function') 80 | }) 81 | 82 | it('that yields fork effect for each component', () => { 83 | const wrappedQueryFnStr = wrapQueryFn(queryFn, props).toString() 84 | 85 | expect(res.done).toEqual(false) 86 | 87 | expect(res.value[0].FORK).toBeTruthy() 88 | expect(res.value[0]).toEqual(fork(fetch, { 89 | type: ActionTypes.RequestCreatePost, 90 | id: 0, 91 | contentType: 'post', 92 | identifier: 16 93 | })) 94 | 95 | expect(res.value[1].FORK).toBeTruthy() 96 | expect(res.value[1].FORK).toBeTruthy() 97 | expect(res.value[1].FORK.args[0].id).toEqual(1) 98 | expect(res.value[1].FORK.args[0].type).toEqual(ActionTypes.RequestCreateQuery) 99 | expect(res.value[1].FORK.args[0].queryFn.toString()).toEqual(wrappedQueryFnStr) 100 | }) 101 | 102 | it('that yields join effect', () => { 103 | const tasks = [createMockTask(), createMockTask()] 104 | expect(iter.next(tasks).value).toEqual(join(...tasks)) 105 | expect(iter.next().done).toEqual(true) 106 | }) 107 | 108 | it('updates store', async () => { 109 | const { store } = setup() 110 | await runSagas(store, [ 111 | () => preload([ConnectQueryC], { 112 | params: { 113 | id: bookJson.id 114 | } 115 | }) 116 | ]) 117 | expect(store.getState().wordpress.queries).toEqual({ 118 | 0: { 119 | OK: true, 120 | complete: true, 121 | entities: [bookJson.id], 122 | id: 0, 123 | paging: {}, 124 | prepared: true 125 | } 126 | }) 127 | }) 128 | 129 | it('callable within another component query function', async () => { 130 | const { store } = setup() 131 | await runSagas(store, [ 132 | () => preload([ConnectQueryNestedC], { 133 | params: { 134 | id: bookJson.id 135 | } 136 | }) 137 | ]) 138 | expect(store.getState().wordpress.queries).toEqual({ 139 | 0: { 140 | OK: true, 141 | complete: true, 142 | entities: [bookJson.id], 143 | id: 0, 144 | paging: {}, 145 | prepared: true 146 | }, 147 | 1: { 148 | OK: true, 149 | complete: true, 150 | entities: [bookJson.id + 1], 151 | id: 1, 152 | paging: {}, 153 | prepared: true 154 | }, 155 | }) 156 | }) 157 | }) 158 | 159 | describe('#preloadQuery', () => { 160 | let iter 161 | 162 | it('throws with bad queryFn', () => { 163 | expect(() => preloadQuery('')).toThrowError(/Expecting queryFn to be a function/) 164 | }) 165 | 166 | it('throws with bad renderProps', () => { 167 | expect(() => preloadQuery(() => {}, '')).toThrowError(/Expecting renderProps to be an object/) 168 | }) 169 | 170 | it('throws with bad state', () => { 171 | expect(() => preloadQuery(() => {}, {}, '')).toThrowError(/Expecting state to be an object/) 172 | }) 173 | 174 | it('returns generator', () => { 175 | expect(typeof preloadQuery(() => {})).toEqual('function') 176 | }) 177 | 178 | it('yields data from given queryFn', () => { 179 | iter = preloadQuery(() => 'mockResult')() 180 | expect(iter.next().value).toEqual('mockResult') 181 | }) 182 | 183 | it('that yields put completeRequest effect', () => { 184 | const actual = iter.next('mockResult').value 185 | expect(actual.PUT.action.id).toEqual(null) 186 | expect(actual.PUT.action.type).toEqual(ActionTypes.RequestComplete) 187 | expect(actual.PUT.action.data).toEqual('mockResult') 188 | }) 189 | }) 190 | }) 191 | -------------------------------------------------------------------------------- /test/connect/connectWpPost.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | import React from 'react' 6 | import merge from 'lodash.merge' 7 | import { mount } from 'enzyme' 8 | 9 | import queryCounter from '../../src/util/queryCounter' 10 | import { ActionTypes } from '../../src/constants' 11 | 12 | import '../__mocks__/WP' 13 | import stateMultipleEntities from '../__mocks__/states/multipleEntities' 14 | import initialState from '../__mocks__/states/initial' 15 | import BuiltInTypeComponent, { target } from '../__mocks__/components/BuiltInContentType' 16 | import CustomTypeComponent from '../__mocks__/components/CustomContentType' 17 | import BadContentTypeComponent from '../__mocks__/components/BadContentType' 18 | import ExplicitIdentifierComponent from '../__mocks__/components/ExplicitIdentifier' 19 | 20 | import postJson from '../__fixtures__/wp-api-responses/post' 21 | import bookJson from '../__fixtures__/wp-api-responses/book' 22 | 23 | const BuiltInType = (props, store) => mount(, { context: { store } }) 24 | const CustomType = (props, store) => mount(, { context: { store } }) 25 | const BadContentType = (props, store) => mount(, { context: { store } }) 26 | const ExplicitIdentifier = (props, store) => mount(, { context: { store } }) 27 | 28 | let state 29 | 30 | function setup () { 31 | const dispatch = jest.fn() 32 | const subscribe = () => {} 33 | const getState = () => state 34 | return { dispatch, getState, subscribe } 35 | } 36 | 37 | describe('connectWpPost', () => { 38 | describe('with built-in content type', () => { 39 | let store 40 | let rendered 41 | 42 | beforeAll(() => { 43 | queryCounter.reset() 44 | const props = { params: { id: postJson.id } } 45 | state = initialState() 46 | store = setup() 47 | rendered = BuiltInType(props, store) 48 | }) 49 | 50 | it('should wrap the component', () => { 51 | // Components are wrapped first by react-redux connect() 52 | expect(BuiltInTypeComponent.WrappedComponent.WrappedComponent).toBe(target) 53 | expect(BuiltInTypeComponent.WrappedComponent.__kasia__).toBe(true) 54 | }) 55 | 56 | it('should render loading', () => { 57 | expect(rendered.html()).toEqual('
Loading...
') 58 | }) 59 | 60 | it('should dispatch RequestCreatePost', () => { 61 | const action = store.dispatch.mock.calls[0][0] 62 | expect(action.type).toEqual(ActionTypes.RequestCreatePost) 63 | expect(action.contentType).toEqual('post') 64 | expect(action.identifier).toEqual(postJson.id) 65 | expect(action.id).toEqual(0) 66 | }) 67 | 68 | it('should render post title', () => { 69 | const query = { complete: true, OK: true, entities: [postJson.id] } 70 | state = merge({}, stateMultipleEntities, { wordpress: { queries: { 0: query } } }) 71 | rendered.update() // Fake store update from completed request 72 | expect(rendered.html()).toEqual('
Architecto enim omnis repellendus
') 73 | }) 74 | 75 | it('should update to new entity that exists in store straight away', () => { 76 | const nextProps = { params: { id: postJson.id + 1 } } 77 | rendered.setProps(nextProps) 78 | expect(rendered.html()).toEqual('
new title
') 79 | }) 80 | 81 | it('should dispatch RequestCreatePost for entity that is not in store', () => { 82 | const nextProps = { params: { id: 100 } } 83 | rendered.setProps(nextProps) 84 | expect(rendered.html()).toEqual('
Loading...
') 85 | 86 | const action = store.dispatch.mock.calls[1][0] 87 | expect(action.type).toEqual(ActionTypes.RequestCreatePost) 88 | expect(action.contentType).toEqual('post') 89 | expect(action.identifier).toEqual(100) 90 | expect(action.id).toEqual(1) 91 | }) 92 | }) 93 | 94 | describe('with custom content type', () => { 95 | let store 96 | let rendered 97 | 98 | beforeAll(() => { 99 | queryCounter.reset() 100 | const props = { params: { id: bookJson.id } } 101 | state = stateMultipleEntities 102 | store = setup() 103 | rendered = CustomType(props, store) 104 | }) 105 | 106 | it('should dispatch RequestCreatePost', () => { 107 | const action = store.dispatch.mock.calls[0][0] 108 | expect(action.type).toEqual(ActionTypes.RequestCreatePost) 109 | expect(action.contentType).toEqual('book') 110 | expect(action.identifier).toEqual(bookJson.id) 111 | expect(action.id).toEqual(0) 112 | }) 113 | 114 | it('should render book title', () => { 115 | const query = { complete: true, OK: true, entities: [bookJson.id] } 116 | state = merge({}, stateMultipleEntities, { wordpress: { queries: { 0: query } } }) 117 | rendered.update() // Fake store update from completed request 118 | expect(rendered.html()).toEqual('
Hello
') 119 | }) 120 | }) 121 | 122 | // Test that a store keyed by numeric ID can still resolve an 123 | // entity that is fetched using explicit slug identifier 124 | describe('with explicit slug identifier', () => { 125 | let store 126 | let rendered 127 | 128 | beforeAll(() => { 129 | queryCounter.reset() 130 | const props = { params: { id: postJson.id } } 131 | state = stateMultipleEntities 132 | store = setup() 133 | rendered = ExplicitIdentifier(props, store) 134 | }) 135 | 136 | it('should dispatch RequestCreatePost', () => { 137 | const action = store.dispatch.mock.calls[0][0] 138 | expect(action.type).toEqual(ActionTypes.RequestCreatePost) 139 | expect(action.contentType).toEqual('post') 140 | expect(action.identifier).toEqual(postJson.slug) 141 | expect(action.id).toEqual(0) 142 | }) 143 | 144 | it('should render post title', () => { 145 | const query = { complete: true, OK: true, entities: [postJson.id] } 146 | state = merge({}, stateMultipleEntities, { wordpress: { queries: { 0: query } } }) 147 | rendered.update() // Fake store update from completed request 148 | expect(rendered.html()).toEqual('
Architecto enim omnis repellendus
') 149 | }) 150 | }) 151 | 152 | describe('with bad content type', () => { 153 | it('should throw "content type is not recognised" error', () => { 154 | const store = setup(stateMultipleEntities) 155 | const props = { params: { id: postJson.id } } 156 | expect(() => BadContentType(props, store)).toThrowError(/is not recognised/) 157 | }) 158 | }) 159 | }) 160 | -------------------------------------------------------------------------------- /test/__fixtures__/wp-api-responses/book.js: -------------------------------------------------------------------------------- 1 | // Custom content type 2 | export default { 3 | 'id': 4, 4 | 'date': '2016-07-26T10:45:11', 5 | 'date_gmt': '2016-07-26T09:45:11', 6 | 'guid': { 7 | 'rendered': 'http://localhost/wp/?post_type=books&p=4' 8 | }, 9 | 'modified': '2016-07-26T10:45:11', 10 | 'modified_gmt': '2016-07-26T09:45:11', 11 | 'slug': 'hello', 12 | 'type': 'books', 13 | 'link': 'http://localhost/wp/books/hello/', 14 | 'title': { 15 | 'rendered': 'Hello' 16 | }, 17 | 'content': { 18 | 'rendered': '' 19 | }, 20 | 'featured_media': 0, 21 | '_embedded': { 22 | 'author': [{ 23 | 'id': 42, 24 | 'name': 'Sophia', 25 | 'url': 'http://Little.com/fugit-rem-in-consequuntur-perspiciatis-ex', 26 | 'description': 'Cumque esse consequatur facere minus. In tenetur culpa illo sint labore nostrum quis. Explicabo animi explicabo perferendis nesciunt omnis quo sunt nihil. Quod magnam necessitatibus dicta amet omnis aliquid.\r\n\r\nEt dolor consequuntur necessitatibus sequi incidunt est ratione. Unde sint iusto possimus officiis cum molestiae. Nostrum ad illo pariatur et.\r\n\r\nDoloremque neque doloremque voluptas vel voluptas placeat est. Consectetur qui et accusamus.\r\n\r\nCorrupti qui accusamus voluptatum ab architecto sapiente fugit quis. Ut et sit quod ea voluptates nobis. Enim qui eos magnam at.\r\n\r\nQuia error tempora ullam nulla illo sint. Velit perferendis amet facere quia dignissimos. Rerum nihil pariatur eum velit rerum.\r\n\r\nVelit laudantium voluptate nesciunt et cupiditate asperiores. Voluptatem recusandae numquam et neque unde molestiae voluptate. Rerum et harum occaecati quis occaecati voluptas. Sequi qui dolores cumque nostrum ab nostrum.\r\n\r\nExplicabo cum voluptates odit asperiores ut. Ipsam exercitationem eum nostrum qui dolorum sint nulla. Aut perferendis dolorem ab itaque architecto ad enim.\r\n\r\nMinima aut nobis qui enim beatae. Quis quo modi nemo est quam ea inventore. Odit consequatur vitae perferendis mollitia pariatur voluptas ea sit.\r\n\r\nIn qui saepe ducimus labore. Perferendis ducimus quia corporis perspiciatis. Autem nobis explicabo debitis laboriosam tempora aut.', 27 | 'link': 'http://demo.wp-api.org/author/meaghan-willms/', 28 | 'slug': 'meaghan-willms', 29 | 'avatar_urls': { 30 | '24': 'http://2.gravatar.com/avatar/bf432d6e226ae2f791a59ddd2b3f8462?s=24&d=mm&r=g', 31 | '48': 'http://2.gravatar.com/avatar/bf432d6e226ae2f791a59ddd2b3f8462?s=48&d=mm&r=g', 32 | '96': 'http://2.gravatar.com/avatar/bf432d6e226ae2f791a59ddd2b3f8462?s=96&d=mm&r=g' 33 | }, 34 | '_links': { 35 | 'self': [{ 36 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users/42' 37 | }], 38 | 'collection': [{ 39 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users' 40 | }] 41 | } 42 | }], 43 | 'wp:featuredmedia': [{ 44 | 'id': 15, 45 | 'date': '2015-06-03T22:18:52', 46 | 'slug': 'vero-amet-id-commodi-harum', 47 | 'type': 'attachment', 48 | 'link': 'http://demo.wp-api.org/vero-amet-id-commodi-harum/', 49 | 'title': { 50 | 'rendered': 'Vero amet id commodi harum' 51 | }, 52 | 'author': 100, 53 | 'alt_text': '', 54 | 'media_type': 'image', 55 | 'mime_type': 'image/jpeg', 56 | 'media_details': { 57 | 'width': 1080, 58 | 'height': 631, 59 | 'file': '2015/06/0845fa5d-4c5e-3c93-a054-63a976339952.jpg', 60 | 'sizes': { 61 | 'thumbnail': { 62 | 'file': '0845fa5d-4c5e-3c93-a054-63a976339952-150x150.jpg', 63 | 'width': 150, 64 | 'height': 150, 65 | 'mime_type': 'image/jpeg', 66 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952-150x150.jpg' 67 | }, 68 | 'medium': { 69 | 'file': '0845fa5d-4c5e-3c93-a054-63a976339952-300x175.jpg', 70 | 'width': 300, 71 | 'height': 175, 72 | 'mime_type': 'image/jpeg', 73 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952-300x175.jpg' 74 | }, 75 | 'large': { 76 | 'file': '0845fa5d-4c5e-3c93-a054-63a976339952-1024x598.jpg', 77 | 'width': 1024, 78 | 'height': 598, 79 | 'mime_type': 'image/jpeg', 80 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952-1024x598.jpg' 81 | }, 82 | 'full': { 83 | 'file': '0845fa5d-4c5e-3c93-a054-63a976339952.jpg', 84 | 'width': 1080, 85 | 'height': 631, 86 | 'mime_type': 'image/jpeg', 87 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952.jpg' 88 | } 89 | }, 90 | 'image_meta': { 91 | 'aperture': 0, 92 | 'credit': 'JOKINROM', 93 | 'camera': '', 94 | 'caption': '', 95 | 'created_timestamp': 1430341682, 96 | 'copyright': '', 97 | 'focal_length': 0, 98 | 'iso': 0, 99 | 'shutter_speed': 0, 100 | 'title': '', 101 | 'orientation': 0 102 | } 103 | }, 104 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952.jpg', 105 | '_links': { 106 | 'self': [{ 107 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media/15' 108 | }], 109 | 'collection': [{ 110 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media' 111 | }], 112 | 'about': [{ 113 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/types/attachment' 114 | }], 115 | 'author': [{ 116 | 'embeddable': true, 117 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users/100' 118 | }], 119 | 'replies': [{ 120 | 'embeddable': true, 121 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/comments?post=15' 122 | }] 123 | } 124 | }], 125 | 'wp:term': [[{ 126 | 'id': 1, 127 | 'link': 'http://demo.wp-api.org/category/uncategorized/', 128 | 'name': 'Uncategorized', 129 | 'slug': 'uncategorized', 130 | 'taxonomy': 'category', 131 | '_links': { 132 | 'self': [{ 133 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/categories/1' 134 | }], 135 | 'collection': [{ 136 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/categories' 137 | }], 138 | 'about': [{ 139 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/taxonomies/category' 140 | }], 141 | 'wp:post_type': [{ 142 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/posts?categories=1' 143 | }], 144 | 'curies': [{ 145 | 'name': 'wp', 146 | 'href': 'https://api.w.org/{rel}', 147 | 'templated': true 148 | }] 149 | } 150 | }], 151 | []] 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /test/universal-journey.js: -------------------------------------------------------------------------------- 1 | /* global jest:false, expect:false */ 2 | 3 | jest.disableAutomock() 4 | 5 | // we mock queryBuilder after imports 6 | // we need to mock client and server environments 7 | jest.mock('is-node-fn') 8 | 9 | import React from 'react' 10 | import createSagaMiddleware from 'redux-saga' 11 | import isNode from 'is-node-fn' 12 | import Wpapi from 'wpapi' 13 | import { put } from 'redux-saga/effects' 14 | import { createStore as _createStore, applyMiddleware, compose, combineReducers } from 'redux' 15 | import { mount } from 'enzyme' 16 | 17 | import './__mocks__/WP' 18 | import kasia from '../src' 19 | import queryCounter from '../src/util/queryCounter' 20 | import schemasManager from '../src/util/schemasManager' 21 | import { ActionTypes } from '../src/constants' 22 | import { fetch } from '../src/redux/sagas' 23 | 24 | import BuiltInContentType from './__mocks__/components/BuiltInContentType' 25 | import initialState from './__mocks__/states/initial' 26 | import post from './__fixtures__/wp-api-responses/post' 27 | 28 | const post1 = post 29 | const post2 = Object.assign({}, post, { id: 17, slug: 'post-2', title: { rendered: 'Post 2' } }) 30 | const post3 = Object.assign({}, post, { id: 18, slug: 'post-3', title: { rendered: 'Post 3' } }) 31 | 32 | // post to return from queryFn 33 | let returnPost 34 | 35 | // we need to mock responses from WP-API 36 | jest.mock('../src/util/queryBuilder', () => ({ 37 | buildQueryFunction: () => () => new Promise((resolve) => { 38 | setTimeout(() => resolve(returnPost)) 39 | }) 40 | })) 41 | 42 | function setup (keyEntitiesBy) { 43 | const { kasiaReducer, kasiaSagas } = kasia({ 44 | wpapi: new Wpapi({ endpoint: '123' }), 45 | keyEntitiesBy 46 | }) 47 | 48 | const sagaMiddleware = createSagaMiddleware() 49 | const createStore = compose(applyMiddleware(sagaMiddleware))(_createStore) 50 | const store = createStore(combineReducers(kasiaReducer), initialState(keyEntitiesBy)) 51 | const runSaga = sagaMiddleware.run 52 | 53 | sagaMiddleware.run(function * () { 54 | yield kasiaSagas 55 | }) 56 | 57 | return { store, runSaga } 58 | } 59 | 60 | describe('Universal journey', function () { 61 | ['id', 'slug'].forEach((keyEntitiesBy) => { 62 | describe('keyEntitiesBy = ' + keyEntitiesBy, () => { 63 | let rendered 64 | let preloader 65 | let action 66 | let iter 67 | let store 68 | let runSaga 69 | 70 | function newStore () { 71 | const s = setup(keyEntitiesBy) 72 | store = s.store 73 | runSaga = s.runSaga 74 | } 75 | 76 | it('SERVER', () => { 77 | schemasManager.__flush__() 78 | newStore() // we would create new store for each request 79 | queryCounter.reset() 80 | isNode.mockReturnValue(true) 81 | returnPost = post1 82 | }) 83 | 84 | it(' should have a preload static method', () => { 85 | expect(typeof BuiltInContentType.preload).toEqual('function') 86 | }) 87 | 88 | it(' ...that returns a preloader operation array', () => { 89 | const renderProps = { params: { [keyEntitiesBy]: post1[keyEntitiesBy] } } 90 | preloader = BuiltInContentType.preload(renderProps) 91 | expect(Array.isArray(preloader)).toEqual(true) 92 | }) 93 | 94 | it(' ...that contains a saga function and action object', () => { 95 | action = { 96 | id: 0, 97 | type: ActionTypes.RequestCreatePost, 98 | identifier: post1[keyEntitiesBy], 99 | contentType: 'post' 100 | } 101 | expect(preloader[0]).toEqual(fetch) 102 | expect(preloader[1]).toEqual(action) 103 | iter = fetch(action) 104 | }) 105 | 106 | it(' should run preloader', () => { 107 | // acknowledge request 108 | const ackAction = { 109 | id: 0, 110 | type: ActionTypes.AckRequest, 111 | identifier: post1[keyEntitiesBy], 112 | contentType: 'post' 113 | } 114 | const actual1 = iter.next().value 115 | const expected1 = put(ackAction) 116 | expect(actual1).toEqual(expected1) 117 | return runSaga(fetch, action).done 118 | }) 119 | 120 | it(` should have entity keyed by ${keyEntitiesBy} on store`, () => { 121 | const actual = store.getState().wordpress.entities.posts[post1[keyEntitiesBy]] 122 | expect(actual).toEqual(post1) 123 | }) 124 | 125 | it(' should render the prepared data on server', () => { 126 | kasia.rewind() 127 | const params = { [keyEntitiesBy]: post1[keyEntitiesBy] } 128 | rendered = mount(, { context: { store } }) 129 | expect(rendered.html()).toEqual('
Architecto enim omnis repellendus
') 130 | }) 131 | 132 | it('CLIENT', () => { 133 | // imitate client 134 | queryCounter.reset() 135 | isNode.mockReturnValue(false) 136 | returnPost = post2 137 | }) 138 | 139 | it(' should render the prepared query data on client', () => { 140 | const params = { [keyEntitiesBy]: post1[keyEntitiesBy] } 141 | rendered = mount(, { context: { store } }) 142 | expect(rendered.html()).toEqual('
Architecto enim omnis repellendus
') 143 | }) 144 | 145 | it(' should render loading text when props change', () => { 146 | const props = { params: { [keyEntitiesBy]: post2[keyEntitiesBy] } } 147 | rendered.setProps(props) // implicit update 148 | expect(rendered.html()).toEqual('
Loading...
') 149 | }) 150 | 151 | it(' should make a non-prepared query on props change', () => { 152 | const query = store.getState().wordpress.queries[1] 153 | expect(typeof query).toEqual('object') 154 | expect(query.prepared).toEqual(false) 155 | expect(query.complete).toEqual(false) 156 | }) 157 | 158 | it(' should display new post title', (done) => { 159 | // allow fetch saga to complete 160 | // todo surely a better way to do this? 161 | setTimeout(() => { 162 | try { 163 | const query = store.getState().wordpress.queries[1] 164 | expect(query.complete).toEqual(true) 165 | expect(rendered.update().html()).toEqual('
Post 2
') 166 | done() 167 | } catch (err) { 168 | done(err) 169 | } 170 | }, 100) 171 | }) 172 | 173 | it(' can make another non-prepared query', (done) => { 174 | returnPost = post3 175 | 176 | // change props 177 | const props = { params: { [keyEntitiesBy]: post3[keyEntitiesBy] } } 178 | rendered.setProps(props) 179 | expect(rendered.html()).toEqual('
Loading...
') 180 | 181 | // check query is acknowledged 182 | const query = store.getState().wordpress.queries[2] 183 | expect(typeof query).toEqual('object') 184 | expect(query.prepared).toEqual(false) 185 | expect(query.complete).toEqual(false) 186 | 187 | // check renders new data 188 | setTimeout(() => { 189 | try { 190 | const query = store.getState().wordpress.queries[2] 191 | expect(query.complete).toEqual(true) 192 | expect(rendered.update().html()).toEqual('
Post 3
') 193 | done() 194 | } catch (err) { 195 | done(err) 196 | } 197 | }, 100) 198 | }) 199 | }) 200 | }) 201 | }) 202 | -------------------------------------------------------------------------------- /test/__fixtures__/wp-api-responses/media.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'id': 15, 3 | 'date': '2015-06-03T22:18:52', 4 | 'date_gmt': '2015-06-03T22:18:52', 5 | 'guid': { 6 | 'rendered': 'https://hmn-uploads-eu.s3.amazonaws.com/wp-api-demo-production/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952.jpg' 7 | }, 8 | 'modified': '2015-06-03T22:18:52', 9 | 'modified_gmt': '2015-06-03T22:18:52', 10 | 'slug': 'vero-amet-id-commodi-harum', 11 | 'type': 'attachment', 12 | 'link': 'http://demo.wp-api.org/vero-amet-id-commodi-harum/', 13 | 'title': { 14 | 'rendered': 'Vero amet id commodi harum' 15 | }, 16 | 'author': 100, 17 | 'comment_status': 'closed', 18 | 'ping_status': 'closed', 19 | 'alt_text': '', 20 | 'caption': '', 21 | 'description': '
Dolorem labore sit eum ea velit rerum et quasi
\n
Sapiente tempore magnam molestias dolore. pariatur repellendus tempore. Illum nihil suscipit culpa. tempora facilis porro. Reiciendis inventore at sit ipsam nulla. Optio aut eum nulla harum Itaque et consequatur sint ut sed Eveniet asperiores enim dolor dolor et. Consectetur est eveniet quod voluptatem vel
\n

Voluptas qui aut eum nihil rerum est vel. Et ad ipsam placeat accusantium aut magnam

\n
  1. Laboriosam voluptas
  2. Sed neque aspernatur aspernatur sint ipsa
  3. Exercitationem fugit pariatur ea
  4. Optio dicta autem repellat nostrum
\n

Possimus modi consequuntur sit Ab magnam reprehenderit et est Ducimus quis et fuga Dolorem illum odio magni Porro sunt repellendus harum et. dolorem velit occaecati. non nobis molestias sed accusamus temporibus. praesentium quia autem quam. Ullam saepe dolorem. tenetur ducimus architecto. Adipisci impedit culpa facilis voluptas necessitatibus est. Commodi expedita

\n

Maxime optio iusto dolor. Hic ratione enim quos unde repellendus consequatur sed. Deleniti ut iste sit perferendis necessitatibus explicabo

\n
  • Neque quasi repellat cumque et dolorum vel atque
  • Ut eveniet non minus
  • Similique et id sunt voluptates libero ipsa
\n
Quam aut eius non nulla. Asperiores omnis voluptas recusandae ea
\n

Aspernatur veritatis quo quo vel in. Quod mollitia corporis iusto illo perspiciatis. Odit consequuntur ex omnis repellat voluptatem mollitia distinctio eum. Consequuntur eius ipsa qui qui consequatur. Ab consequatur enim sint voluptas. Aliquid sint ut vitae consequatur. Voluptas magnam voluptatem omnis molestias.

\n

Et id dignissimos nostrum autem. Facere eos eum quibusdam vel commodi. Aut distinctio excepturi nesciunt hic similique. Magnam sint omnis temporibus neque qui

\n
  • Optio voluptatem deleniti hic numquam cumque qui
  • Ut nihil tempore veniam incidunt
  • Et incidunt sapiente enim et
  • Eum culpa voluptates labore earum
  • Molestiae ab animi et
  • Qui dolorem eos
  • Quia
  • Nulla sit eos adipisci aut
', 22 | 'media_type': 'image', 23 | 'mime_type': 'image/jpeg', 24 | 'media_details': { 25 | 'width': 1080, 26 | 'height': 631, 27 | 'file': '2015/06/0845fa5d-4c5e-3c93-a054-63a976339952.jpg', 28 | 'sizes': { 29 | 'thumbnail': { 30 | 'file': '0845fa5d-4c5e-3c93-a054-63a976339952-150x150.jpg', 31 | 'width': 150, 32 | 'height': 150, 33 | 'mime_type': 'image/jpeg', 34 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952-150x150.jpg' 35 | }, 36 | 'medium': { 37 | 'file': '0845fa5d-4c5e-3c93-a054-63a976339952-300x175.jpg', 38 | 'width': 300, 39 | 'height': 175, 40 | 'mime_type': 'image/jpeg', 41 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952-300x175.jpg' 42 | }, 43 | 'large': { 44 | 'file': '0845fa5d-4c5e-3c93-a054-63a976339952-1024x598.jpg', 45 | 'width': 1024, 46 | 'height': 598, 47 | 'mime_type': 'image/jpeg', 48 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952-1024x598.jpg' 49 | }, 50 | 'full': { 51 | 'file': '0845fa5d-4c5e-3c93-a054-63a976339952.jpg', 52 | 'width': 1080, 53 | 'height': 631, 54 | 'mime_type': 'image/jpeg', 55 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952.jpg' 56 | } 57 | }, 58 | 'image_meta': { 59 | 'aperture': 0, 60 | 'credit': 'JOKINROM', 61 | 'camera': '', 62 | 'caption': '', 63 | 'created_timestamp': 1430341682, 64 | 'copyright': '', 65 | 'focal_length': 0, 66 | 'iso': 0, 67 | 'shutter_speed': 0, 68 | 'title': '', 69 | 'orientation': 0 70 | } 71 | }, 72 | 'post': null, 73 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952.jpg', 74 | '_links': { 75 | 'self': [{ 76 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media/15' 77 | }], 78 | 'collection': [{ 79 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media' 80 | }], 81 | 'about': [{ 82 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/types/attachment' 83 | }], 84 | 'author': [{ 85 | 'embeddable': true, 86 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users/100' 87 | }], 88 | 'replies': [{ 89 | 'embeddable': true, 90 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/comments?post=15' 91 | }] 92 | }, 93 | '_embedded': { 94 | 'author': [{ 95 | 'id': 100, 96 | 'name': 'Sonya', 97 | 'url': 'http://Beatty.info/mollitia-qui-sed-est-beatae', 98 | 'description': 'Et ut est aut rerum quis qui magnam. Vero assumenda laudantium qui autem. Saepe reprehenderit consequatur perferendis libero nam consequatur. Eius voluptate atque illum dolorem repellendus. Reprehenderit assumenda est tempore nesciunt ea.\r\n\r\nUt quia magnam sit molestiae quos. Accusamus voluptate non animi nihil esse. Labore porro quod vitae consequuntur reprehenderit saepe.\r\n\r\nMolestiae porro dolorem nihil neque sint. Reiciendis ducimus rerum suscipit labore temporibus nisi nostrum. Natus amet eum eum excepturi tempore nisi fuga. Totam aut facilis sapiente voluptatem alias unde.\r\n\r\nEaque consequatur omnis adipisci voluptatem illo. Quia neque necessitatibus ut quaerat distinctio hic molestias. Ipsum nisi molestiae vitae.\r\n\r\nAperiam sint nobis quia ut qui sint veniam. Nam animi corporis et ratione numquam est perferendis. Et dolore sit quos earum eius soluta nemo iste.\r\n\r\nVeniam incidunt voluptas reiciendis aut culpa natus. Dolor est numquam consequatur commodi.', 99 | 'link': 'http://demo.wp-api.org/author/elmira-luettgen/', 100 | 'slug': 'elmira-luettgen', 101 | 'avatar_urls': { 102 | '24': 'http://0.gravatar.com/avatar/3eb34c383eaebf4c71409ba2a95fc34a?s=24&d=mm&r=g', 103 | '48': 'http://0.gravatar.com/avatar/3eb34c383eaebf4c71409ba2a95fc34a?s=48&d=mm&r=g', 104 | '96': 'http://0.gravatar.com/avatar/3eb34c383eaebf4c71409ba2a95fc34a?s=96&d=mm&r=g' 105 | }, 106 | '_links': { 107 | 'self': [{ 108 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users/95' 109 | }], 110 | 'collection': [{ 111 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users' 112 | }] 113 | } 114 | }] 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/connect.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect as reduxConnect } from 'react-redux' 3 | 4 | import debug from './util/debug' 5 | import contentTypesManager from './util/contentTypesManager' 6 | import invariants from './invariants' 7 | import queryCounter from './util/queryCounter' 8 | import findEntities from './util/findEntities' 9 | import { createPostRequest, createQueryRequest } from './redux/actions' 10 | import { fetch } from './redux/sagas' 11 | 12 | const WARN_NO_ENTITIES_PROP = 0 13 | const WARN_NO_REWIND = 1 14 | 15 | // Is a component the first Kasia component to mount? 16 | let firstMount = true 17 | 18 | // What have we warned the consumer of? 19 | let haveWarned = [] 20 | 21 | /** Reset first mount flag, should be called before SSR of each request. */ 22 | export function rewind () { 23 | firstMount = true 24 | } 25 | 26 | /** Get entity identifier: either `id` as-is or the result of calling `id(props)`. */ 27 | export function identifier (displayName, id, props) { 28 | const realId = typeof id === 'function' ? id(props) : id 29 | invariants.isIdentifierValue(realId, displayName) 30 | return realId 31 | } 32 | 33 | /** Wrap `queryFn` in a function that takes the node-wpapi instance. */ 34 | export function wrapQueryFn (queryFn, props, state) { 35 | return (wpapi) => queryFn(wpapi, props, state) 36 | } 37 | 38 | /** Wrap component in react-redux connect. */ 39 | function connect (cls) { 40 | return reduxConnect(({ wordpress }) => { 41 | invariants.hasWordpressObject(wordpress) 42 | return { wordpress } 43 | })(cls) 44 | } 45 | 46 | const base = (target) => { 47 | const displayName = target.displayName || target.name 48 | 49 | return class KasiaConnectedComponent extends React.Component { 50 | static __kasia__ = true 51 | 52 | static WrappedComponent = target 53 | 54 | static contextTypes = { 55 | store: React.PropTypes.object.isRequired 56 | } 57 | 58 | /** Make request for new data from WP-API. */ 59 | _requestWpData (props, queryId) { 60 | const action = this._getRequestWpDataAction(props) 61 | action.id = queryId 62 | this.queryId = queryId 63 | this.props.dispatch(action) 64 | } 65 | 66 | /** Find the query for this component and its corresponding data and return props object containing them. */ 67 | _reconcileWpData (props) { 68 | const query = this._query() 69 | const data = this._makePropsData(props) 70 | const fallbackQuery = { complete: false, OK: null } 71 | 72 | if (query) { 73 | invariants.queryHasError(query, displayName) 74 | } 75 | 76 | const result = { 77 | kasia: { 78 | query: query || fallbackQuery, 79 | [this.dataKey]: data 80 | } 81 | } 82 | 83 | debug(`content for ${displayName} on \`props.kasia.${this.dataKey}\``) 84 | 85 | return Object.defineProperty(result, 'entities', { 86 | get: () => { 87 | if (!haveWarned[WARN_NO_ENTITIES_PROP]) { 88 | console.log('[kasia] `props.kasia.entities` is replaced by `props.kasia.data` in v4.') 89 | haveWarned[WARN_NO_ENTITIES_PROP] = true 90 | } 91 | } 92 | }) 93 | } 94 | 95 | _query () { 96 | return this.props.wordpress.queries[this.queryId] 97 | } 98 | 99 | componentWillMount () { 100 | const state = this.props.wordpress 101 | const numQueries = Object.keys(state.queries).length - 1 102 | const nextCounterIndex = queryCounter.current() + 1 103 | 104 | if (numQueries > nextCounterIndex && !haveWarned[WARN_NO_REWIND]) { 105 | console.log( 106 | '[kasia] the query counter and queries in the store are not in sync. ' + 107 | 'This may be because you are not calling `kasia.rewind()` before running preloaders.' 108 | ) 109 | haveWarned[WARN_NO_REWIND] = true 110 | } 111 | 112 | // When doing SSR we need to reset the counter so that components start 113 | // at queryId=0, aligned with the preloaders that have been run for them. 114 | if (firstMount) { 115 | queryCounter.reset() 116 | firstMount = false 117 | } 118 | 119 | const queryId = this.queryId = queryCounter.next() 120 | const query = state.queries[queryId] 121 | 122 | // We found a prepared query matching `queryId` - use it. 123 | if (query && query.prepared) debug(`found prepared data for ${displayName} at queryId=${queryId}`) 124 | // Did not find prepared query so request new data and reuse the queryId 125 | else if (!query) this._requestWpData(this.props, queryId) 126 | // Request new data with new queryId 127 | else if (!query.prepared) this._requestWpData(this.props, queryCounter.next()) 128 | } 129 | 130 | componentWillReceiveProps (nextProps) { 131 | const willUpdate = this._shouldUpdate(this.props, nextProps, this.context.store.getState()) 132 | if (willUpdate) this._requestWpData(nextProps, queryCounter.next()) 133 | } 134 | 135 | render () { 136 | const props = Object.assign({}, this.props, this._reconcileWpData(this.props)) 137 | return React.createElement(target, props) 138 | } 139 | } 140 | } 141 | 142 | /** 143 | * Connect a component to a single entity from WordPress. 144 | * 145 | * @example Built-in content type, derived slug identifier: 146 | * ```js 147 | * const { Page } from 'kasia/types' 148 | * connectWordPress(Page, (props) => props.params.slug)(Component) 149 | * ``` 150 | * 151 | * @example Built-in content type, explicit ID identifier: 152 | * ```js 153 | * const { Post } from 'kasia/types' 154 | * connectWordPress(Post, 16)(Component) 155 | * ``` 156 | * 157 | * @example Custom content type, derived identifier: 158 | * ```js 159 | * connectWordPress('News', (props) => props.params.slug)(Component) 160 | * ``` 161 | * 162 | * @param {String} contentType Content type of the WP entity to fetch 163 | * @param {Function|String|Number} id Entity's ID/slug/a function that derives either from props 164 | * @returns {Function} Decorated component 165 | */ 166 | export function connectWpPost (contentType, id) { 167 | invariants.isString('contentType', contentType) 168 | invariants.isIdentifierArg(id) 169 | 170 | const typeConfig = contentTypesManager.get(contentType) 171 | 172 | return (target) => { 173 | const displayName = target.displayName || target.name 174 | 175 | invariants.isNotWrapped(target, displayName) 176 | 177 | class KasiaConnectWpPostComponent extends base(target) { 178 | constructor (props, context) { 179 | super(props, context) 180 | this.dataKey = contentType 181 | } 182 | 183 | static preload (props) { 184 | debug(displayName, 'connectWpPost preload with props:', props) 185 | invariants.isValidContentType(typeConfig, contentType, `${displayName} component`) 186 | const action = createPostRequest(contentType, identifier(displayName, id, props)) 187 | action.id = queryCounter.next() 188 | return [fetch, action] 189 | } 190 | 191 | _getRequestWpDataAction (props) { 192 | debug(displayName, 'connectWpPost request with props:', props) 193 | const realId = identifier(displayName, id, props) 194 | return createPostRequest(contentType, realId) 195 | } 196 | 197 | _makePropsData (props) { 198 | const query = this._query() 199 | 200 | if (!query || !query.complete || query.error) return null 201 | 202 | const entities = this.props.wordpress.entities[typeConfig.plural] 203 | 204 | if (entities) { 205 | const keys = Object.keys(entities) 206 | const realId = identifier(displayName, id, props) 207 | 208 | for (let i = 0, len = keys.length; i < len; i++) { 209 | const entity = entities[keys[i]] 210 | if (entity.id === realId || entity.slug === realId) return entity 211 | } 212 | } 213 | 214 | return null 215 | } 216 | 217 | _shouldUpdate (thisProps, nextProps) { 218 | // Make a request for new data if entity not in store or the identifier has changed 219 | const entity = this._makePropsData(nextProps) 220 | return !entity && identifier(displayName, id, nextProps) !== identifier(displayName, id, thisProps) 221 | } 222 | 223 | componentWillMount () { 224 | invariants.isValidContentType(typeConfig, contentType, `${displayName} component`) 225 | super.componentWillMount() 226 | } 227 | } 228 | 229 | return connect(KasiaConnectWpPostComponent) 230 | } 231 | } 232 | 233 | /** 234 | * Connect a component to arbitrary data from WordPress. 235 | * 236 | * The component will request new data via the given `queryFn` if `shouldUpdate` returns true. 237 | * 238 | * @example Get all posts by an author: 239 | * ```js 240 | * connectWpQuery((wpapi) => wpapi.posts().embed().author('David Bowie'), () => true) 241 | * ``` 242 | * 243 | * @example Get all pages: 244 | * ```js 245 | * connectWpQuery((wpapi) => wpapi.pages().embed(), () => true) 246 | * ``` 247 | * 248 | * @example Get custom content type by slug (content type registered at init): 249 | * ```js 250 | * connectWpQuery( 251 | * (wpapi) => wpapi.news().slug('gullible-removed-from-the-dictionary').embed(), 252 | * () => true 253 | * ) 254 | * ``` 255 | * 256 | * @example Update only when `props.id` changes: 257 | * ```js 258 | * connectWpQuery( 259 | * (wpapi, props) => wpapi.page().id(props.identifier()).embed().get(), 260 | * (thisProps, nextProps) => thisProps.id !== nextProps.id 261 | * ) 262 | * ``` 263 | * 264 | * @param {Function} queryFn Function that returns a wpapi query 265 | * @param {Function} shouldUpdate Inspect props to determine if new data request is made 266 | * @returns {Function} Decorated component 267 | */ 268 | export function connectWpQuery (queryFn, shouldUpdate) { 269 | invariants.isFunction('queryFn', queryFn) 270 | invariants.isFunction('shouldUpdate', shouldUpdate) 271 | 272 | return (target) => { 273 | const displayName = target.displayName || target.name 274 | 275 | invariants.isNotWrapped(target, displayName) 276 | 277 | class KasiaConnectWpQueryComponent extends base(target) { 278 | constructor (props, context) { 279 | super(props, context) 280 | this.dataKey = 'data' 281 | this._shouldUpdate = shouldUpdate 282 | } 283 | 284 | static preload (props, state) { 285 | debug(displayName, 'connectWpQuery preload with props:', props, 'state:', state) 286 | const wrappedQueryFn = wrapQueryFn(queryFn, props, state) 287 | const action = createQueryRequest(wrappedQueryFn) 288 | action.id = queryCounter.next() 289 | return [fetch, action] 290 | } 291 | 292 | _getRequestWpDataAction (props) { 293 | debug(displayName, 'connectWpQuery request with props:', props) 294 | const wrappedQueryFn = wrapQueryFn(queryFn, props, this.context.store.getState()) 295 | return createQueryRequest(wrappedQueryFn) 296 | } 297 | 298 | _makePropsData () { 299 | const query = this._query() 300 | const state = this.props.wordpress 301 | if (!query || !query.complete || query.error) return {} 302 | else return findEntities(state.entities, state.keyEntitiesBy, query.entities) 303 | } 304 | } 305 | 306 | return connect(KasiaConnectWpQueryComponent) 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /test/__fixtures__/wp-api-responses/page.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'id': 12, 3 | 'date': '2015-12-22T05:51:11', 4 | 'date_gmt': '2015-12-22T05:51:11', 5 | 'guid': { 6 | 'rendered': 'http://wp-api-de-elbinsta-n5pu020s13lb-1748419096.eu-west-1.elb.amazonaws.com/?page_id=12' 7 | }, 8 | 'modified': '2015-12-22T05:51:11', 9 | 'modified_gmt': '2015-12-22T05:51:11', 10 | 'slug': 'laboriosam-et-reiciendis-et-aliquam-quis', 11 | 'type': 'page', 12 | 'link': 'http://demo.wp-api.org/laboriosam-et-reiciendis-et-aliquam-quis/', 13 | 'title': { 14 | 'rendered': 'Laboriosam et reiciendis et aliquam quis' 15 | }, 16 | 'content': { 17 | 'rendered': '

Culpa nam vero doloremque quis. Sed nam magni consequatur. Laboriosam maxime cupiditate et quis. Iusto omnis ratione doloremque eos animi. Voluptas impedit velit recusandae eius

\n
Incidunt est itaque quo et labore. Maxime quis cum facilis debitis quia et laudantium
\n
    \n
  • At sequi et rerum enim
  • \n
  • Ex consectetur ullam amet
  • \n
  • Et quae consectetur itaque
  • \n
  • Mollitia est quis at quis
  • \n
  • Aut et id cum
  • \n
  • Labore unde at autem
  • \n
\n

\n

Nemo fugit aut quas et impedit. Optio et non in sapiente. Libero eum beatae maxime sed. Nulla cupiditate dolor sit excepturi sunt. Dolorum consectetur eaque consequatur enim. Et adipisci reprehenderit iure ea consequatur illo. Aperiam eum temporibus dolorem deleniti voluptatum. Eos et maxime qui iure ratione. Consequatur eaque sunt deserunt. Dolorem quia et illum dolorem aliquam quo Eos quo quaerat laborum delectus sed. Molestiae aperiam non numquam dicta. nesciunt et labore facere. Est et voluptatem architecto sapiente architecto. Minima consequatur eos rerum ut sit. accusantium et velit.

\n
Quibusdam quis ut commodi. Qui aut laboriosam et quia quia
\n

Ducimus nostrum quod reprehenderit In asperiores amet ut consequatur Et consequatur distinctio facilis. Recusandae et beatae explicabo modi. Mollitia aut quia nemo impedit voluptatibus eum. Autem velit nam maiores architecto soluta. Animi itaque vel aut. Consectetur voluptatum necessitatibus assumenda autem corporis Magnam sed molestiae ut. Autem aut ipsam est Tenetur eius vel iste velit aut. Magni enim sequi nulla molestias. perferendis quae fugiat nam fugiat modi esse. In nostrum ut eveniet et. Aut voluptates sed assumenda. Voluptates ad ducimus totam et nihil. Dolorum optio ea. Perferendis non harum ipsa aliquid similique

\n
    \n
  • Est odio exercitationem rerum earum doloribus
  • \n
  • Quaerat fugit in quia
  • \n
  • Tenetur
  • \n
\n
Voluptas accusamus provident placeat corrupti
\n

\'Facilis

\n
Doloremque temporibus omnis commodi dolor et
\n

\'Corrupti

\n

Doloribus eum et velit. Aut nemo voluptate maxime numquam autem. Veniam qui est nihil

\n
\n

Delectus pariatur sunt omnis sed soluta. Animi ducimus consectetur aperiam et. Temporibus iste qui et itaque dolores. id non tempora fuga deserunt. Perferendis laboriosam ducimus nihil saepe rem. Odit in voluptatum aspernatur. Adipisci dolore aut minima. ratione dolores eaque saepe reiciendis Explicabo eum rerum et id qui voluptatum qui. id dolores saepe fugit modi. Laboriosam libero doloribus molestiae at asperiores ut. Assumenda aut repellendus fuga animi Aut consequatur ea officia enim.

\n

Ducimus consequatur autem occaecati soluta. Molestiae voluptatum et in veniam autem. Minus aut nihil vero illo eum harum. Eveniet maxime recusandae eos. Suscipit dicta iste qui ipsa quibusdam aspernatur est.

\n

\'Quis.\'

\n' 18 | }, 19 | 'excerpt': { 20 | 'rendered': '

Culpa nam vero doloremque quis. Sed nam magni consequatur. Laboriosam maxime cupiditate et quis. Iusto omnis ratione doloremque eos animi. Voluptas impedit velit recusandae eius Incidunt est itaque quo et labore. Maxime quis cum facilis debitis quia et laudantium At sequi et rerum enim Ex consectetur ullam amet Et quae consectetur itaque Mollitia est quis […]

\n' 21 | }, 22 | 'author': 95, 23 | 'featured_media': 11, 24 | 'parent': 0, 25 | 'menu_order': 0, 26 | 'comment_status': 'closed', 27 | 'ping_status': 'open', 28 | 'template': '', 29 | '_links': { 30 | 'self': [{ 31 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/pages/12' 32 | }], 33 | 'collection': [{ 34 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/pages' 35 | }], 36 | 'about': [{ 37 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/types/page' 38 | }], 39 | 'author': [{ 40 | 'embeddable': true, 41 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users/95' 42 | }], 43 | 'replies': [{ 44 | 'embeddable': true, 45 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/comments?post=12' 46 | }], 47 | 'version-history': [{ 48 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/pages/12/revisions' 49 | }], 50 | 'wp:featuredmedia': [{ 51 | 'embeddable': true, 52 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media/11' 53 | }], 54 | 'wp:attachment': [{ 55 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media?parent=12' 56 | }], 57 | 'curies': [{ 58 | 'name': 'wp', 59 | 'href': 'https://api.w.org/{rel}', 60 | 'templated': true 61 | }] 62 | }, 63 | '_embedded': { 64 | 'author': [{ 65 | 'id': 95, 66 | 'name': 'Sonya', 67 | 'url': 'http://Beatty.info/mollitia-qui-sed-est-beatae', 68 | 'description': 'Et ut est aut rerum quis qui magnam. Vero assumenda laudantium qui autem. Saepe reprehenderit consequatur perferendis libero nam consequatur. Eius voluptate atque illum dolorem repellendus. Reprehenderit assumenda est tempore nesciunt ea.\r\n\r\nUt quia magnam sit molestiae quos. Accusamus voluptate non animi nihil esse. Labore porro quod vitae consequuntur reprehenderit saepe.\r\n\r\nMolestiae porro dolorem nihil neque sint. Reiciendis ducimus rerum suscipit labore temporibus nisi nostrum. Natus amet eum eum excepturi tempore nisi fuga. Totam aut facilis sapiente voluptatem alias unde.\r\n\r\nEaque consequatur omnis adipisci voluptatem illo. Quia neque necessitatibus ut quaerat distinctio hic molestias. Ipsum nisi molestiae vitae.\r\n\r\nAperiam sint nobis quia ut qui sint veniam. Nam animi corporis et ratione numquam est perferendis. Et dolore sit quos earum eius soluta nemo iste.\r\n\r\nVeniam incidunt voluptas reiciendis aut culpa natus. Dolor est numquam consequatur commodi.', 69 | 'link': 'http://demo.wp-api.org/author/elmira-luettgen/', 70 | 'slug': 'elmira-luettgen', 71 | 'avatar_urls': { 72 | '24': 'http://0.gravatar.com/avatar/3eb34c383eaebf4c71409ba2a95fc34a?s=24&d=mm&r=g', 73 | '48': 'http://0.gravatar.com/avatar/3eb34c383eaebf4c71409ba2a95fc34a?s=48&d=mm&r=g', 74 | '96': 'http://0.gravatar.com/avatar/3eb34c383eaebf4c71409ba2a95fc34a?s=96&d=mm&r=g' 75 | }, 76 | '_links': { 77 | 'self': [{ 78 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users/95' 79 | }], 80 | 'collection': [{ 81 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users' 82 | }] 83 | } 84 | }], 85 | 'wp:featuredmedia': [{ 86 | 'id': 11, 87 | 'date': '2015-06-03T22:18:47', 88 | 'slug': 'qui-et-ipsa-maiores', 89 | 'type': 'attachment', 90 | 'link': 'http://demo.wp-api.org/qui-et-ipsa-maiores/', 91 | 'title': { 92 | 'rendered': 'Qui et ipsa maiores' 93 | }, 94 | 'author': 72, 95 | 'alt_text': '', 96 | 'media_type': 'image', 97 | 'mime_type': 'image/jpeg', 98 | 'media_details': { 99 | 'width': 1080, 100 | 'height': 677, 101 | 'file': '2015/06/e5247ed2-2a86-3a98-8f70-d5786b1318d4.jpg', 102 | 'sizes': { 103 | 'thumbnail': { 104 | 'file': 'e5247ed2-2a86-3a98-8f70-d5786b1318d4-150x150.jpg', 105 | 'width': 150, 106 | 'height': 150, 107 | 'mime_type': 'image/jpeg', 108 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/e5247ed2-2a86-3a98-8f70-d5786b1318d4-150x150.jpg' 109 | }, 110 | 'medium': { 111 | 'file': 'e5247ed2-2a86-3a98-8f70-d5786b1318d4-300x188.jpg', 112 | 'width': 300, 113 | 'height': 188, 114 | 'mime_type': 'image/jpeg', 115 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/e5247ed2-2a86-3a98-8f70-d5786b1318d4-300x188.jpg' 116 | }, 117 | 'large': { 118 | 'file': 'e5247ed2-2a86-3a98-8f70-d5786b1318d4-1024x642.jpg', 119 | 'width': 1024, 120 | 'height': 642, 121 | 'mime_type': 'image/jpeg', 122 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/e5247ed2-2a86-3a98-8f70-d5786b1318d4-1024x642.jpg' 123 | }, 124 | 'full': { 125 | 'file': 'e5247ed2-2a86-3a98-8f70-d5786b1318d4.jpg', 126 | 'width': 1080, 127 | 'height': 677, 128 | 'mime_type': 'image/jpeg', 129 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/e5247ed2-2a86-3a98-8f70-d5786b1318d4.jpg' 130 | } 131 | }, 132 | 'image_meta': { 133 | 'aperture': 0, 134 | 'credit': '', 135 | 'camera': '', 136 | 'caption': '', 137 | 'created_timestamp': 1430426977, 138 | 'copyright': '', 139 | 'focal_length': 0, 140 | 'iso': 0, 141 | 'shutter_speed': 0, 142 | 'title': '', 143 | 'orientation': 0 144 | } 145 | }, 146 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/e5247ed2-2a86-3a98-8f70-d5786b1318d4.jpg', 147 | '_links': { 148 | 'self': [{ 149 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media/11' 150 | }], 151 | 'collection': [{ 152 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media' 153 | }], 154 | 'about': [{ 155 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/types/attachment' 156 | }], 157 | 'author': [{ 158 | 'embeddable': true, 159 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users/72' 160 | }], 161 | 'replies': [{ 162 | 'embeddable': true, 163 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/comments?post=11' 164 | }] 165 | } 166 | }] 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /test/__fixtures__/wp-api-responses/post.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'id': 16, 3 | 'date': '2015-12-31T14:51:26', 4 | 'date_gmt': '2015-12-31T14:51:26', 5 | 'guid': { 6 | 'rendered': 'http://wp-api-de-elbinsta-n5pu020s13lb-1748419096.eu-west-1.elb.amazonaws.com/?p=16' 7 | }, 8 | 'modified': '2015-12-31T14:51:26', 9 | 'modified_gmt': '2015-12-31T14:51:26', 10 | 'slug': 'architecto-enim-omnis-repellendus', 11 | 'type': 'post', 12 | 'link': 'http://demo.wp-api.org/2015/12/31/architecto-enim-omnis-repellendus/', 13 | 'title': { 14 | 'rendered': 'Architecto enim omnis repellendus' 15 | }, 16 | 'content': { 17 | 'rendered': '
Similique eaque voluptas cum voluptatem. Similique debitis quis sapiente veniam saepe. Architecto qui repellendus autem dolor autem est molestiae
\n

\'Molestiae

\n
    \n
  1. Libero ut
  2. \n
  3. Libero sunt nihil laboriosam rerum
  4. \n
  5. Rem modi
  6. \n
  7. Quod minus est nam alias velit
  8. \n
  9. Id consequatur doloribus sed et
  10. \n
  11. Maiores nulla eveniet
  12. \n
  13. Ut voluptates accusamus voluptas ea
  14. \n
  15. Quia esse id temporibus et et maiores
  16. \n
\n

Voluptatem qui eveniet quidem quia rerum voluptatem. Vel ipsa provident nesciunt at. Sed velit et placeat et et

\n
    \n
  • Est ea ut quia et
  • \n
  • Maiores nesciunt quisquam et esse aut et
  • \n
  • Reprehenderit magni neque ad
  • \n
  • Natus ex culpa non vitae
  • \n
\n

Iste velit in modi. Officia rerum error et quo molestiae. Reiciendis est est libero recusandae quia earum

\n
\n
\n
Earum harum non magni placeat rerum velit dolore. Et non facilis eum quidem vero beatae corrupti in. Repellendus ut et nobis
\n

Nobis est voluptatem ipsum sunt veniam. Iusto quia mollitia sed at non. Corrupti quibusdam et doloremque sed. similique sint Ut modi neque nostrum molestiae aut. officiis id dolores numquam. Error voluptate eum quasi cum. Rerum inventore cupiditate magni inventore magnam Magnam omnis necessitatibus veritatis modi. inventore accusamus sit sapiente expedita nam eos.

\n
Qui blanditiis et dolorum veniam alias aperiam. Hic quisquam neque ea alias nostrum voluptas
\n
    \n
  • Omnis sed consectetur voluptatem molestiae est
  • \n
  • Iste ex magnam neque quia fugiat
  • \n
  • A qui
  • \n
  • Quibusdam omnis minima in quidem
  • \n
  • Officiis sint iure omnis
  • \n
  • Voluptas nisi
  • \n
  • Voluptates at eveniet aut
  • \n
  • Natus accusantium ducimus ut
  • \n
\n

Corporis quasi ut magnam omnis. Accusantium quasi doloremque quia quas quasi modi mollitia consequatur. Eligendi rerum repellendus mollitia iste. Architecto neque eveniet laborum iste suscipit quod aut. Nam vero rerum fugit delectus qui dolores consequatur. Porro molestiae unde voluptatum laudantium. Autem distinctio ducimus enim qui. Fugit possimus eos qui aliquid accusamus magnam. Consequatur iusto non quibusdam sapiente modi deserunt. Velit et dicta iusto est natus. Eligendi ut officiis et quasi. Nihil voluptatum facere dolores quae voluptatem. Sunt sed ex provident et quam. Molestiae aut et eveniet tempore. Voluptas est quia nobis temporibus natus sint. Autem animi et accusamus quidem occaecati dolorem impedit accusantium. Quibusdam quis omnis provident alias voluptatem. Quisquam impedit quos est tenetur animi ipsam. Recusandae aut incidunt ex minima quisquam ea quia. Quod blanditiis nulla qui maxime nemo corrupti. Itaque est voluptate voluptatem voluptates rerum quasi dolor. Autem illo cumque voluptatem pariatur repellendus commodi illum dolorem. Quidem totam ut neque praesentium. Debitis ut est eveniet itaque repellat. In labore et et quia qui aut dolor. Illum rerum magni quia porro quae et fugiat. Esse dolore sint minima possimus sed ut. Ut harum sequi corrupti. Velit distinctio eum ullam corrupti dolorem earum eum. Rerum impedit iure omnis nesciunt. Quisquam voluptatem consequatur quia. Est ab quae cum ut. Nihil quidem iste est reiciendis quisquam ut. Consequatur dolorum consequatur modi. Porro cupiditate nostrum quam recusandae unde. Autem dolorem nemo nesciunt id praesentium exercitationem aut. Eos magni natus sit dolores dolorum animi assumenda aliquid. Id cupiditate nobis officiis totam. Eveniet dolores cum voluptas quam. Ut vel est velit at et aut velit. Quod sed nam in ut sit hic.

\n

Quibusdam consectetur illum quis id rerum. Ea est ut autem consequatur earum

\n

\'Nisi

\n
    \n
  1. Consequatur qui qui est modi
  2. \n
  3. Ipsam quasi ex
  4. \n
  5. Necessitatibus quia sequi non saepe
  6. \n
  7. Eum dignissimos voluptas vel qui similique
  8. \n
  9. Quo laborum
  10. \n
  11. Quidem delectus autem aut sit voluptatem
  12. \n
  13. Voluptates qui reiciendis atque minus ipsam perferendis
  14. \n
  15. Enim hic quas explicabo
  16. \n
  17. Vel explicabo suscipit laudantium
  18. \n
  19. Ut ut sed eum culpa nesciunt quis quis
  20. \n
  21. Enim laborum voluptatem dolorem est inventore
  22. \n
\n
Omnis quam in reprehenderit. Porro magni voluptatem aut adipisci quia aut ea optio. Rem qui esse dolorem porro eveniet qui
\n
    \n
  • Et molestiae velit facilis libero quo
  • \n
  • Et ad quidem id doloribus
  • \n
  • Aut ullam
  • \n
  • At velit doloribus qui iure
  • \n
\n

Voluptatibus aliquam expedita repudiandae rerum consequuntur fuga. Provident voluptatem autem vel sit

\n

Est unde voluptatum illum Est similique maiores omnis facere est molestiae. Sunt ut itaque omnis non. Dolore perspiciatis iste et veniam eius. Ipsum est similique consequatur. Magnam ea ex saepe dolor corrupti. Quia fuga qui ex. beatae placeat non Omnis possimus voluptate ea quia enim. et suscipit magni est illo. Qui totam vitae tenetur quos reprehenderit.

\n' 18 | }, 19 | 'excerpt': { 20 | 'rendered': '

Similique eaque voluptas cum voluptatem. Similique debitis quis sapiente veniam saepe. Architecto qui repellendus autem dolor autem est molestiae Libero ut Libero sunt nihil laboriosam rerum Rem modi Quod minus est nam alias velit Id consequatur doloribus sed et Maiores nulla eveniet Ut voluptates accusamus voluptas ea Quia esse id temporibus et et maiores Voluptatem […]

\n' 21 | }, 22 | 'author': 42, 23 | 'featured_media': 15, 24 | 'comment_status': 'open', 25 | 'ping_status': 'open', 26 | 'sticky': false, 27 | 'format': 'standard', 28 | 'categories': [1], 29 | 'tags': [], 30 | '_links': { 31 | 'self': [{ 32 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/posts/16' 33 | }], 34 | 'collection': [{ 35 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/posts' 36 | }], 37 | 'about': [{ 38 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/types/post' 39 | }], 40 | 'author': [{ 41 | 'embeddable': true, 42 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users/42' 43 | }], 44 | 'replies': [{ 45 | 'embeddable': true, 46 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/comments?post=16' 47 | }], 48 | 'version-history': [{ 49 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/posts/16/revisions' 50 | }], 51 | 'wp:featuredmedia': [{ 52 | 'embeddable': true, 53 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media/15' 54 | }], 55 | 'wp:attachment': [{ 56 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media?parent=16' 57 | }], 58 | 'wp:term': [{ 59 | 'taxonomy': 'category', 60 | 'embeddable': true, 61 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/categories?post=16' 62 | }, 63 | { 64 | 'taxonomy': 'post_tag', 65 | 'embeddable': true, 66 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/tags?post=16' 67 | }], 68 | 'curies': [{ 69 | 'name': 'wp', 70 | 'href': 'https://api.w.org/{rel}', 71 | 'templated': true 72 | }] 73 | }, 74 | '_embedded': { 75 | 'author': [{ 76 | 'id': 42, 77 | 'name': 'Sophia', 78 | 'url': 'http://Little.com/fugit-rem-in-consequuntur-perspiciatis-ex', 79 | 'description': 'Cumque esse consequatur facere minus. In tenetur culpa illo sint labore nostrum quis. Explicabo animi explicabo perferendis nesciunt omnis quo sunt nihil. Quod magnam necessitatibus dicta amet omnis aliquid.\r\n\r\nEt dolor consequuntur necessitatibus sequi incidunt est ratione. Unde sint iusto possimus officiis cum molestiae. Nostrum ad illo pariatur et.\r\n\r\nDoloremque neque doloremque voluptas vel voluptas placeat est. Consectetur qui et accusamus.\r\n\r\nCorrupti qui accusamus voluptatum ab architecto sapiente fugit quis. Ut et sit quod ea voluptates nobis. Enim qui eos magnam at.\r\n\r\nQuia error tempora ullam nulla illo sint. Velit perferendis amet facere quia dignissimos. Rerum nihil pariatur eum velit rerum.\r\n\r\nVelit laudantium voluptate nesciunt et cupiditate asperiores. Voluptatem recusandae numquam et neque unde molestiae voluptate. Rerum et harum occaecati quis occaecati voluptas. Sequi qui dolores cumque nostrum ab nostrum.\r\n\r\nExplicabo cum voluptates odit asperiores ut. Ipsam exercitationem eum nostrum qui dolorum sint nulla. Aut perferendis dolorem ab itaque architecto ad enim.\r\n\r\nMinima aut nobis qui enim beatae. Quis quo modi nemo est quam ea inventore. Odit consequatur vitae perferendis mollitia pariatur voluptas ea sit.\r\n\r\nIn qui saepe ducimus labore. Perferendis ducimus quia corporis perspiciatis. Autem nobis explicabo debitis laboriosam tempora aut.', 80 | 'link': 'http://demo.wp-api.org/author/meaghan-willms/', 81 | 'slug': 'meaghan-willms', 82 | 'avatar_urls': { 83 | '24': 'http://2.gravatar.com/avatar/bf432d6e226ae2f791a59ddd2b3f8462?s=24&d=mm&r=g', 84 | '48': 'http://2.gravatar.com/avatar/bf432d6e226ae2f791a59ddd2b3f8462?s=48&d=mm&r=g', 85 | '96': 'http://2.gravatar.com/avatar/bf432d6e226ae2f791a59ddd2b3f8462?s=96&d=mm&r=g' 86 | }, 87 | '_links': { 88 | 'self': [{ 89 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users/42' 90 | }], 91 | 'collection': [{ 92 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users' 93 | }] 94 | } 95 | }], 96 | 'wp:featuredmedia': [{ 97 | 'id': 15, 98 | 'date': '2015-06-03T22:18:52', 99 | 'slug': 'vero-amet-id-commodi-harum', 100 | 'type': 'attachment', 101 | 'link': 'http://demo.wp-api.org/vero-amet-id-commodi-harum/', 102 | 'title': { 103 | 'rendered': 'Vero amet id commodi harum' 104 | }, 105 | 'author': 100, 106 | 'alt_text': '', 107 | 'media_type': 'image', 108 | 'mime_type': 'image/jpeg', 109 | 'media_details': { 110 | 'width': 1080, 111 | 'height': 631, 112 | 'file': '2015/06/0845fa5d-4c5e-3c93-a054-63a976339952.jpg', 113 | 'sizes': { 114 | 'thumbnail': { 115 | 'file': '0845fa5d-4c5e-3c93-a054-63a976339952-150x150.jpg', 116 | 'width': 150, 117 | 'height': 150, 118 | 'mime_type': 'image/jpeg', 119 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952-150x150.jpg' 120 | }, 121 | 'medium': { 122 | 'file': '0845fa5d-4c5e-3c93-a054-63a976339952-300x175.jpg', 123 | 'width': 300, 124 | 'height': 175, 125 | 'mime_type': 'image/jpeg', 126 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952-300x175.jpg' 127 | }, 128 | 'large': { 129 | 'file': '0845fa5d-4c5e-3c93-a054-63a976339952-1024x598.jpg', 130 | 'width': 1024, 131 | 'height': 598, 132 | 'mime_type': 'image/jpeg', 133 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952-1024x598.jpg' 134 | }, 135 | 'full': { 136 | 'file': '0845fa5d-4c5e-3c93-a054-63a976339952.jpg', 137 | 'width': 1080, 138 | 'height': 631, 139 | 'mime_type': 'image/jpeg', 140 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952.jpg' 141 | } 142 | }, 143 | 'image_meta': { 144 | 'aperture': 0, 145 | 'credit': 'JOKINROM', 146 | 'camera': '', 147 | 'caption': '', 148 | 'created_timestamp': 1430341682, 149 | 'copyright': '', 150 | 'focal_length': 0, 151 | 'iso': 0, 152 | 'shutter_speed': 0, 153 | 'title': '', 154 | 'orientation': 0 155 | } 156 | }, 157 | 'source_url': 'http://demo.wp-api.org/content/uploads/2015/06/0845fa5d-4c5e-3c93-a054-63a976339952.jpg', 158 | '_links': { 159 | 'self': [{ 160 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media/15' 161 | }], 162 | 'collection': [{ 163 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/media' 164 | }], 165 | 'about': [{ 166 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/types/attachment' 167 | }], 168 | 'author': [{ 169 | 'embeddable': true, 170 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/users/100' 171 | }], 172 | 'replies': [{ 173 | 'embeddable': true, 174 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/comments?post=15' 175 | }] 176 | } 177 | }], 178 | 'wp:term': [[{ 179 | 'id': 1, 180 | 'link': 'http://demo.wp-api.org/category/uncategorized/', 181 | 'name': 'Uncategorized', 182 | 'slug': 'uncategorized', 183 | 'taxonomy': 'category', 184 | '_links': { 185 | 'self': [{ 186 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/categories/1' 187 | }], 188 | 'collection': [{ 189 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/categories' 190 | }], 191 | 'about': [{ 192 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/taxonomies/category' 193 | }], 194 | 'wp:post_type': [{ 195 | 'href': 'http://demo.wp-api.org/wp-json/wp/v2/posts?categories=1' 196 | }], 197 | 'curies': [{ 198 | 'name': 'wp', 199 | 'href': 'https://api.w.org/{rel}', 200 | 'templated': true 201 | }] 202 | } 203 | }], 204 | []] 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

kasia

6 | 7 |

A React Redux toolset for the WordPress API

8 | 9 |

Made with ❤ at @outlandish

10 | 11 |

12 | npm version 13 | 14 | travis ci build 15 | Coverage Status 16 |

17 | 18 |

19 | 20 |

21 | 22 |
23 | 24 | :sparkles: We welcome contributors! 25 | 26 | :vertical_traffic_light: Issues are triaged using a traffic light system: 27 | 28 |    ![#00ff00](http://placehold.it/15/00ff00/000000?text=+) [small](https://github.com/outlandishideas/kasia/issues?q=is%3Aopen+is%3Aissue+label%3Asmall) - quick tasks, great for beginner contributors
29 |    ![#ffff00](http://placehold.it/15/ffff00/000000?text=+) [medium](https://github.com/outlandishideas/kasia/issues?q=is%3Aopen+is%3Aissue+label%3Amedium) - tasks with increased complexity, may take some time to implement
30 |    ![#ff0000](http://placehold.it/15/ff0000/000000?text=+) [large](https://github.com/outlandishideas/kasia/issues?q=is%3Aopen+is%3Aissue+label%3Alarge) - big tasks that touch many parts of the library, will require commitment 31 | 32 | [Get started contributing here.](https://github.com/outlandishideas/kasia/issues) 33 | 34 |
35 | 36 | Get data from WordPress and into components with ease... 37 | 38 | ```js 39 | // e.g. Get a post by its slug 40 | @connectWpPost('post', 'spongebob-squarepants') 41 | export default class extends React.Component () { 42 | render () { 43 | const { post: spongebob } = this.props.kasia 44 | 45 | if (!spongebob) { 46 | return

{'Who lives in a pineapple under the sea?'}

47 | } 48 | 49 | return

{spongebob.title.rendered}!

50 | //=> Spongebob Squarepants! 51 | } 52 | } 53 | ``` 54 | 55 | 56 | ## Features 57 | 58 | - Declaratively connect React components to data from WordPress. 59 | - Uses [`node-wpapi`](https://github.com/WP-API/node-wpapi) in order to facilitate complex queries. 60 | - Register and consume Custom Content Types with ease. 61 | - All WP data is normalised at `store.wordpress`, e.g. `store.wordpress.pages`. 62 | - Support for universal applications. 63 | - Support for plugins, e.g. [`wp-api-menus`](https://github.com/outlandishideas/kasia/tree/master/packages/kasia-plugin-wp-api-menus). 64 | 65 | ## Glossary 66 | 67 | - [Requirements](#requirements) 68 | - [Install](#install) 69 | - [Import](#import) 70 | - [__Configure__](#configure) 71 | - [__Usage__](#usage) 72 | - [Exports](#exports) 73 | - [Plugins](#plugins) 74 | - [Universal Applications](#universal-applications) 75 | - [Contributing](#contributing) 76 | - [Author & License](#author-&-license) 77 | 78 | ## Requirements 79 | 80 | Kasia suits applications that are built using these technologies: 81 | 82 | - React 83 | - Redux 84 | - Redux Sagas (>= 0.10.0) 85 | - WordPress 86 | - [WP-API plugin](http://v2.wp-api.org/) 87 | - [`node-wpapi`](https://github.com/WP-API/node-wpapi) 88 | 89 | ## Install 90 | 91 | ```sh 92 | npm install kasia --save 93 | ``` 94 | 95 | ```sh 96 | yarn add kasia 97 | ``` 98 | 99 | ## Import 100 | 101 | ```js 102 | // ES2015 103 | import kasia from 'kasia' 104 | ``` 105 | 106 | ```js 107 | // CommonJS 108 | var kasia = require('kasia') 109 | ``` 110 | 111 | ## Configure 112 | 113 | Configure Kasia in three steps: 114 | 115 | 1. Initialise Kasia with an instance of `node-wpapi`. 116 | 117 | 2. Spread the Kasia reducer when creating the redux root reducer. 118 | 119 | 3. Run the Kasia sagas after creating the redux-saga middleware. 120 | 121 | A slimline example... 122 | 123 | ```js 124 | import { combineReducers, createStore, applyMiddleware } from 'redux' 125 | import createSagaMiddleware from 'redux-saga' 126 | import kasia from 'kasia' 127 | import wpapi from 'wpapi' 128 | 129 | const wpai = new wpapi({ endpoint: 'http://wordpress/wp-json' }) 130 | 131 | const { kasiaReducer, kasiaSagas } = kasia({ wpapi }) 132 | 133 | const rootSaga = function * () { 134 | yield [...kasiaSagas] 135 | } 136 | 137 | const rootReducer = combineReducers({ 138 | ...kasiaReducer 139 | }) 140 | 141 | const sagaMiddleware = createSagaMiddleware() 142 | 143 | export default function configureStore (initialState) { 144 | const store = createStore( 145 | rootReducer, 146 | initialState, 147 | applyMiddleware(sagaMiddleware) 148 | ) 149 | 150 | sagaMiddleware.run(rootSaga) 151 | 152 | return store 153 | } 154 | ``` 155 | 156 | ## Usage 157 | 158 | ### `kasia(options) : Object` 159 | 160 | Configure Kasia. 161 | 162 | - __options__ {Object} Options object 163 | 164 | Returns an object containing the Kasia reducer and sagas. 165 | 166 | ```js 167 | const { kasiaReducer, kasiaSagas } = kasia({ 168 | wpapi: new wpapi({ endpoint: 'http://wordpress/wp-json' }) 169 | }) 170 | ``` 171 | 172 | The `options` object accepts: 173 | 174 | - `wpapi` {wpapi} 175 | 176 | An instance of `node-wpapi`. 177 | 178 | - `keyEntitiesBy` {String} _(optional, default=`'id'`)_ 179 | 180 | Property of entities that is used to key them in the store. 181 | 182 | One of: `'slug'`, `'id'`. 183 | 184 | - `debug` {Boolean} _(optional, default=`false`)_ 185 | 186 | Log debug information to the console. 187 | 188 | - `contentTypes` {Array} _(optional)_ 189 | 190 | Array of custom content type definitions. 191 | 192 | ```js 193 | // Example custom content type definition 194 | contentTypes: [{ 195 | name: 'book', 196 | plural: 'books', 197 | slug: 'books', 198 | route, // optional, default="/{plural}/(?P)" 199 | namespace, // optional, default="wp/v2" 200 | methodName // optional, default={plural} 201 | }] 202 | ``` 203 | 204 | - `plugins` {Array} _(optional)_ 205 | 206 | Array of Kasia plugins. 207 | 208 | ```js 209 | import kasiaWpApiMenusPlugin from 'kasia-plugin-wp-api-menus' 210 | 211 | // Example passing in plugin 212 | plugins: [ 213 | [kasiaWpApiMenusPlugin, { route: 'menus' }], // with configuration 214 | kasiaWpApiMenusPlugin, // without configuration 215 | ] 216 | ``` 217 | 218 | ### Decorators 219 | 220 | Things to keep in mind: 221 | 222 | - A component will make a request for data 1) when it mounts and 2) if its props change. For `connectWpPost` a change 223 | in props will trigger Kasia to try and find entity data for the new identifier in the store. If it is found, no request 224 | is made. 225 | - Content data should be parsed before being rendered as it may contain encoded HTML entities. 226 | - In arbitrary queries with `connectWpQuery`, we suggest that you always call the `embed` method on the 227 | query chain, otherwise embedded content data will be omitted from the response. 228 | - Paging data for the request made on behalf of the component is available at `this.props.kasia.query.paging`. 229 | - The examples given assume the use of [decorators (sometimes called annotations)](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy). 230 | However decorator support is not necessary. 231 | See the end of each example for the alternative Higher Order Component approach. 232 | 233 | #### `@connectWpPost(contentType, identifier) : Component` 234 | 235 | Connect a component to a single entity in WordPress, e.g. Post, Page, or custom content type. 236 | 237 | - __contentType__ {String} The content type to fetch 238 | - __identifier__ {String|Number|Function} ID of the entity to fetch or function that derives it from `props` 239 | 240 | Returns a connected component. 241 | 242 | Example, using identifier derived from route parameter on `props`: 243 | 244 | ```js 245 | import React, { Component } from 'react' 246 | import { Route } from 'react-router' 247 | import { connectWpPost } from 'kasia/connect' 248 | import { Page } from 'kasia/types' 249 | 250 | @connectWpPost(Page, (props) => props.params.slug) 251 | export default class Page extends Component { 252 | render () { 253 | const { query, page } = this.props.kasia 254 | 255 | if (!query.complete) { 256 | return Loading... 257 | } 258 | 259 | return

{page.title}

260 | } 261 | } 262 | 263 | // Without decorator support 264 | export default connectWpPost(Page, (props) => props.params.slug)(Post) 265 | ``` 266 | 267 | #### `@connectWpQuery(queryFn, shouldUpdate) : Component` 268 | 269 | Connect a component to the result of an arbitrary WP-API query. Query is always made with `?embed` query parameter. 270 | 271 | - __queryFn__ {Function} Function that accepts args `wpapi`, `props`, `state` and should return a WP-API query 272 | - __shouldUpdate__ {Function} Called on `componentWillReceiveProps` with args `thisProps`, `nextProps`, `state` 273 | 274 | Returns a connected component. 275 | 276 | The component will request new data via `queryFn` if `shouldUpdate` returns true. 277 | 278 | Entities returned from the query will be placed on `this.props.kasia.entities` under the same 279 | normalised structure as described in [The Shape of Things](#the-shape-of-things). 280 | 281 | Example, fetching the most recent "News" entities: 282 | 283 | ```js 284 | import React, { Component } from 'react' 285 | import { Route } from 'react-router' 286 | import { connectWpPost } from 'kasia/connect' 287 | 288 | // Note the invocation of `embed` in the query chain 289 | @connectWpQuery((wpapi, props) => { 290 | return wpapi.news().month(props.month).embed().get() 291 | }, (thisProps, nextProps) => thisProps.month != nextProps.month) 292 | export default class RecentNews extends Component { 293 | render () { 294 | const { 295 | query, 296 | data: { news } 297 | } = this.props.kasia 298 | 299 | if (!query.complete) { 300 | return Loading... 301 | } 302 | 303 | return ( 304 |
305 |

Recent News Headlines

306 | {Object.keys(news).map((key) => 307 |

{news[key].title}

)} 308 |
309 | ) 310 | } 311 | } 312 | 313 | // Without decorator support 314 | export default connectWpQuery((wpapi) => { 315 | return wpapi.news().embed().get() 316 | })(Post) 317 | ``` 318 | 319 | ## Exports 320 | 321 | ### `kasia` 322 | 323 | The Kasia configurator and preload utilities. 324 | 325 | ```js 326 | import kasia, { preload, preloadQuery } from 'kasia' 327 | ``` 328 | 329 | ### `kasia/connect` 330 | 331 | The connect decorators. 332 | 333 | ```js 334 | import { connectWpPost, connectWpQuery } from 'kasia/connect' 335 | ``` 336 | 337 | ### `kasia/types` 338 | 339 | The built-in WordPress content types that can be passed to `connectWpPost` to define what content type 340 | a request should be made for. 341 | 342 | ```js 343 | import { 344 | Category, Comment, Media, Page, 345 | Post, PostStatus, PostType, 346 | PostRevision, Tag, Taxonomy, User 347 | } from 'kasia/types' 348 | ``` 349 | 350 | See [Universal Application Utilities](#Utilities) for more details. 351 | 352 | ## Plugins 353 | 354 | Kasia exposes a simple API for third-party plugins. 355 | 356 | A plugin should: 357 | 358 | - be a function that accepts these arguments: 359 | - __wpapi__ {wpapi} An instance of `wpapi` 360 | - __pluginOptions__ {Object} The user's options for the plugin 361 | - __kasiaOptions__ {Object} The user's options for Kasia 362 | 363 | - return an object containing `reducers` (Object) and `sagas` (Array). 364 | 365 | - use the `'kasia/'` action type prefix. 366 | 367 | ```js 368 | // Example definition returned by a plugin 369 | { 370 | reducer: { 371 | 'kasia/SET_DATA': function setDataReducer () {} 372 | 'kasia/REMOVE_DATA': function removeDataReducer () {} 373 | }, 374 | sagas: [function * fetchDataSaga () {}] 375 | } 376 | ``` 377 | 378 | A plugin can hook into Kasia's native action types, available at `kasia/lib/constants/ActionTypes`. 379 | All reducers for an action type are merged into a single function that calls each reducer in succession 380 | with the state returned by the previous reducer. This means the order of plugins that touch the same 381 | action type is important. 382 | 383 | ### Available plugins: 384 | 385 | - [`kasia-plugin-wp-api-menus`](https://github.com/outlandishideas/kasia/tree/master/packages/kasia-plugin-wp-api-menus) 386 | - [`kasia-plugin-wp-api-all-terms`](https://github.com/outlandishideas/kasia/tree/master/packages/kasia-plugin-wp-api-all-terms) 387 | - [`kasia-plugin-wp-api-response-modify`](https://github.com/outlandishideas/kasia/tree/master/packages/kasia-plugin-wp-api-response-modify) 388 | 389 | Please create a pull request to get your own added to the list. 390 | 391 | ## Universal Applications 392 | 393 | __Important...__ 394 | 395 | - __before calling the preloaders for SSR you must call `kasia.rewind()`__ 396 | - __or if you call `runSagas()` from the utilities then this is done for you.__ 397 | 398 | ### Utilities 399 | 400 | #### `runSagas(store, sagas) : Promise` 401 | 402 | Run a bunch of sagas against the store and wait on their completion. 403 | 404 | - __store__ {Object} Redux store enhanced with `runSaga` method 405 | - __sagas__ {Array} Array of functions that accept the store state and return a saga generator 406 | 407 | Returns a Promise resolving on completion of all the sagas. 408 | 409 | #### `preload(components[, renderProps][, state]) : Generator` 410 | 411 | Create a saga operation that will preload all data for any Kasia components in `components`. 412 | 413 | - __components__ {Array} Array of components 414 | - [__renderProps__] {Object} _(optional)_ Render props object derived from the matched route 415 | - [__state__] {Object} _(optional)_ Store state 416 | 417 | Returns a [saga operation](#saga-operation-signature). 418 | 419 | #### `preloadQuery(queryFn[, renderProps][, state]) : Generator` 420 | 421 | Create a saga operation that will preload data for an arbitrary query against the WP API. 422 | 423 | - __queryFn__ {Function} Query function that returns `node-wpapi` query 424 | - [__renderProps__] {Object} _(optional)_ Render props object 425 | - [__state__] {Object} _(optional)_ Store state 426 | 427 | Returns a [saga operation](#saga-operation-signature). 428 | 429 | #### `.preload(renderProps[, state]) : Array` 430 | 431 | Connected components expose a static method `preload` that produces an array of saga operations 432 | to facilitate the request for entity data on the server. 433 | 434 | - __renderProps__ {Object} Render props object derived from the matched route 435 | - [__state__] {Object} _(optional)_ State object (default: `null`) 436 | 437 | Returns an array of [saga operations](#saga-operation-signature). 438 | 439 | #### Saga Operation Signature 440 | 441 | A saga operation is an array of the form: 442 | 443 | ```js 444 | [ sagaGeneratorFn, action ] 445 | ``` 446 | 447 | Where: 448 | 449 | - `sagaGenerator` Function that must be called with the `action`. 450 | 451 | - `action` action Object containing information for the saga to fetch data. 452 | 453 | ### Example 454 | 455 | A somewhat contrived example using the available preloader methods. 456 | 457 | ```js 458 | import { match } from 'react-router' 459 | import { runSagas, preload, preloadQuery } from 'kasia' 460 | 461 | import routes from './routes' 462 | import store from './store' 463 | import renderToString from './render' 464 | import getAllCategories from './queries/categories' 465 | 466 | export default function renderPage (res, location) { 467 | return match({ routes, location }, (error, redirect, renderProps) => { 468 | if (error) return res.sendStatus(500) 469 | if (redirect) return res.redirect(302, redirect.pathname + redirect.search) 470 | 471 | // We are using `runSagas` which rewinds for us, but if we weren't then 472 | // we would call `kasia.rewind()` here instead: 473 | // 474 | // kasia.rewind() 475 | 476 | // Each preloader accepts the state that may/may not have been modified by 477 | // the saga before it, so the order might be important depending on your use-case! 478 | const preloaders = [ 479 | () => preload(renderProps.components, renderProps), 480 | (state) => preloadQuery(getAllCategories, renderProps, state) 481 | ] 482 | 483 | return runSagas(store, preloaders) 484 | .then(() => renderToString(renderProps.components, renderProps, store.getState())) 485 | .then((document) => res.send(document)) 486 | }) 487 | } 488 | ``` 489 | 490 | ## Testing 491 | 492 | Kasia components can be tested by: 493 | - lifting the query function from the connectWpQuery decorator and exporting it 494 | - lifting the should update function from the connectWpQuery deocrating and exporting it 495 | - making your component available as a named export prior to decoration 496 | 497 | An example: 498 | ```js 499 | export function postQuery (wpapi) {...} 500 | 501 | export function shouldUpdate (thisProps, nextProps, state) {...} 502 | 503 | export class Post extends Component {...} 504 | 505 | @connectWpQuery(postQuery, shouldUpdate) 506 | export default Post 507 | ``` 508 | 509 | You can then test the component without decoration, the query and the shouldUpdate function 510 | in isolation. 511 | 512 | ## Contributing 513 | 514 | All pull requests and issues welcome! 515 | 516 | - When submitting an issue please provide adequate steps to reproduce the problem. 517 | - PRs must be made using the `standard` code style. 518 | - PRs must update the version of the library according to [semantic versioning](http://semver.org/). 519 | 520 | If you're not sure how to contribute, check out Kent C. Dodds' 521 | [great video tutorials on egghead.io](https://egghead.io/lessons/javascript-identifying-how-to-contribute-to-an-open-source-project-on-github)! 522 | 523 | ## Author & License 524 | 525 | `kasia` was created by [Outlandish](https://twitter.com/outlandish) and is released under the MIT license. 526 | --------------------------------------------------------------------------------