├── .env.sample ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── img └── wpkanban-demo.gif ├── package-lock.json ├── package.json ├── src ├── apollo │ ├── client.js │ └── wrap-root-element.js ├── components │ └── Login.js └── pages │ └── index.js ├── wordpress-plugin └── wp-graphql-kanban │ ├── README.md │ └── wp-graphql-kanban.php └── yarn.lock /.env.sample: -------------------------------------------------------------------------------- 1 | WPGRAPHQL_URI=https://wpkanban.wpengine.com/graphql 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variables file 55 | .env 56 | .env.development 57 | .env.production 58 | 59 | # gatsby files 60 | .cache/ 61 | public 62 | 63 | 64 | # Mac files 65 | .DS_Store 66 | 67 | #Jetbrains files 68 | .idea 69 | 70 | # Yarn 71 | yarn-error.log 72 | .pnp/ 73 | .pnp.js 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "singleQuote": false, 5 | "tabWidth": 2, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 gatsbyjs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WPGraphQL + Gatsby Kanban Board 2 | 3 | This is a Kanban board created with WPGraphQL and Gatsby, making use of react-trello. 4 | 5 | This demonstrates how Gatsby is more than a static site generator, and WordPress can be used to power full applications, beyond simple blogs and marketing sites. 6 | 7 | ![GIF animation showing a Kanban board with CRUD functionality.](./img/wpkanban-demo.gif) 8 | 9 | ## Setup 10 | 11 | This is a Gatsby App that needs to communicate with a WordPress server using GraphQL. 12 | 13 | ### WordPress Environment 14 | 15 | A WordPress install must have the following plugins installed and activated: 16 | 17 | - WPGraphQL: https://github.com/wp-graphql/wp-graphql 18 | - WPGraphQL JWT Authentication https://github.com/wp-graphql/wp-graphql-jwt-authentication 19 | - WPGraphQL Kanban (found in the `wordpress-plugin` directory of this repository) 20 | 21 | ### Gatsby App 22 | 23 | - Clone this repo 24 | - From within the cloned directory, do the following: 25 | - Copy `.env.sample` to `.env.development` and replace the value of the `WPGRAPHQL_URI` with the path to your WordPress install (including the trailing `/graphql`) running the above mentioned plugins. 26 | - run `gatsby develop` 27 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Browser APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/browser-apis/ 5 | */ 6 | 7 | export { wrapRootElement } from './src/apollo/wrap-root-element'; 8 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: `Gatsby Default Starter`, 4 | description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`, 5 | author: `@gatsbyjs`, 6 | }, 7 | plugins: [ 8 | `gatsby-plugin-antd`, 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Node APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/node-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/ssr-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /img/wpkanban-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wp-graphql/wpgraphql-gatsby-kanban/8c3697b4880d8bf55491641118db9e037bad05ee/img/wpkanban-demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-starter-default", 3 | "private": true, 4 | "description": "A simple starter to get up and developing quickly with Gatsby", 5 | "version": "0.1.0", 6 | "author": "Kyle Mathews ", 7 | "dependencies": { 8 | "apollo-cache-inmemory": "^1.6.3", 9 | "apollo-client": "^2.6.4", 10 | "apollo-link-context": "^1.0.19", 11 | "apollo-link-http": "^1.5.16", 12 | "gatsby": "^2.17.4", 13 | "gatsby-plugin-antd": "^2.0.2", 14 | "graphql": "^14.5.8", 15 | "graphql-tag": "^2.10.1", 16 | "react": "^16.11.0", 17 | "react-apollo": "^3.1.3", 18 | "react-dom": "^16.11.0", 19 | "react-trello": "^2.2.3" 20 | }, 21 | "devDependencies": { 22 | "loader-utils": "^1.2.3", 23 | "prettier": "^1.18.2" 24 | }, 25 | "keywords": [ 26 | "gatsby" 27 | ], 28 | "license": "MIT", 29 | "scripts": { 30 | "build": "gatsby build", 31 | "develop": "gatsby develop", 32 | "format": "prettier --write \"**/*.{js,jsx,json,md}\"", 33 | "start": "npm run develop", 34 | "serve": "gatsby serve", 35 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/gatsbyjs/gatsby/issues" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/apollo/client.js: -------------------------------------------------------------------------------- 1 | import { ApolloClient } from 'apollo-client'; 2 | import { createHttpLink } from 'apollo-link-http'; 3 | import { setContext } from 'apollo-link-context'; 4 | import { InMemoryCache } from 'apollo-cache-inmemory'; 5 | 6 | const httpLink = createHttpLink({ 7 | uri: `${process.env.WPGRAPHQL_URI}`, 8 | }); 9 | 10 | const authLink = setContext((_, { headers }) => { 11 | // get the authentication token from local storage if it exists 12 | const token = localStorage.getItem('authToken'); 13 | // return the headers to the context so httpLink can read them 14 | return token ? { 15 | headers: { 16 | ...headers, 17 | authorization: token ? `Bearer ${token}` : "", 18 | } 19 | } : headers; 20 | }); 21 | 22 | export const client = new ApolloClient({ 23 | link: authLink.concat(httpLink), 24 | cache: new InMemoryCache() 25 | }); 26 | -------------------------------------------------------------------------------- /src/apollo/wrap-root-element.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ApolloProvider } from '@apollo/react-hooks'; 3 | import { client } from './client'; 4 | 5 | export const wrapRootElement = ({ element }) => ( 6 | {element} 7 | ); 8 | -------------------------------------------------------------------------------- /src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form, Icon, Input, Button } from 'antd'; 3 | 4 | 5 | class LoginForm extends React.Component { 6 | handleSubmit = e => { 7 | e.preventDefault(); 8 | console.log( this.props ); 9 | const { form, handleLogin } = this.props; 10 | 11 | form.validateFields((err, values) => { 12 | if (!err) { 13 | console.log('Received values of form: ', values); 14 | handleLogin(values) 15 | } 16 | }); 17 | 18 | }; 19 | 20 | render() { 21 | const { getFieldDecorator } = this.props.form; 22 | return ( 23 |
24 |

WP Kanban Login

25 |
26 | 27 | {getFieldDecorator('username', { 28 | rules: [{ required: true, message: 'Please input your username!' }], 29 | })( 30 | } 32 | placeholder="Username" 33 | />, 34 | )} 35 | 36 | 37 | {getFieldDecorator('password', { 38 | rules: [{ required: true, message: 'Please input your Password!' }], 39 | })( 40 | } 42 | type="password" 43 | placeholder="Password" 44 | />, 45 | )} 46 | 47 | 48 | 51 | 52 |
53 |
54 | ); 55 | } 56 | } 57 | export default Form.create({name: 'Login'})(LoginForm); 58 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Board from "react-trello" 3 | import gql from 'graphql-tag' 4 | import {Query} from 'react-apollo' 5 | import {client} from '../apollo/client' 6 | import Login from '../components/Login' 7 | import { Layout, Menu } from 'antd'; 8 | 9 | const { Header, Content } = Layout; 10 | 11 | /** 12 | * Query all lanes and tasks 13 | */ 14 | const GET_TASK_LANES_AND_TASKS = gql` 15 | { 16 | lanes { 17 | id 18 | title: name 19 | cards { 20 | id 21 | title 22 | description 23 | label 24 | } 25 | } 26 | } 27 | `; 28 | 29 | class KanbanBoard extends React.Component { 30 | 31 | state = { 32 | loggedIn: false, 33 | boardData: { 34 | lanes: [ 35 | { 36 | id: 'loading', 37 | title: 'loading..', 38 | cards: [] 39 | } 40 | ] 41 | } 42 | }; 43 | 44 | componentDidMount() { 45 | const authToken = localStorage.getItem( 'authToken' ); 46 | 47 | if ( authToken ) { 48 | this.setState({ loggedIn: authToken }) 49 | } 50 | } 51 | 52 | handleLogin = input => { 53 | 54 | client.mutate({ 55 | mutation: gql` 56 | mutation LOGIN( $input: LoginInput! ) { 57 | login( input: $input ) { 58 | authToken 59 | refreshToken 60 | user { 61 | id 62 | username 63 | } 64 | } 65 | } 66 | `, 67 | variables: { 68 | input: { 69 | clientMutationId: 'Login', 70 | username: input.username, 71 | password: input.password, 72 | }, 73 | }, 74 | }).then( res => { 75 | 76 | this.setState({ 77 | loggedIn: { 78 | authToken: res.data.login.authToken, 79 | refreshToken: res.data.login.refreshToken, 80 | } 81 | }); 82 | 83 | localStorage.setItem( 'authToken', res.data.login.authToken ); 84 | localStorage.setItem( 'refreshToken', res.data.login.refreshToken ); 85 | 86 | }); 87 | }; 88 | 89 | handleCardAdd = ( card, laneId ) => { 90 | console.log('handleCardAdd', { 91 | card, 92 | laneId 93 | }); 94 | 95 | let id = laneId ? atob(laneId) : null; 96 | id = id ? id.split(':') : null; 97 | 98 | const variables = { 99 | input: { 100 | clientMutationId: 'CreateCard', 101 | status: 'PUBLISH', 102 | title: card && card.title ? card.title : null, 103 | excerpt: card && card.label ? card.label : null, 104 | content: card && card.description ? card.description : null, 105 | taskLanes: laneId && id && id[1] ? { 106 | "append": false, 107 | "nodes": [ 108 | { 109 | "id": parseInt( id[1] ) 110 | } 111 | ] 112 | } : null, 113 | } 114 | }; 115 | 116 | const CREATE_CARD = gql` 117 | mutation CreateTask( $input:CreateTaskInput! ) { 118 | createTask( input: $input ) { 119 | card: task { 120 | id 121 | } 122 | } 123 | }`; 124 | 125 | client.mutate({ 126 | mutation: CREATE_CARD, 127 | variables, 128 | refetchQueries:[{query: GET_TASK_LANES_AND_TASKS}] 129 | }) 130 | 131 | }; 132 | 133 | handleCardDelete = (cardId, laneId) => { 134 | 135 | const DELETE_TASK = gql` 136 | mutation DeleteTask($input: DeleteTaskInput!) { 137 | deleteTask(input: $input) { 138 | card: task { 139 | id 140 | } 141 | } 142 | } 143 | `; 144 | 145 | const variables = { 146 | input: { 147 | clientMutationId: 'DeleteTask', 148 | id: cardId 149 | } 150 | }; 151 | 152 | client.mutate({ 153 | mutation: DELETE_TASK, 154 | variables, 155 | }) 156 | 157 | }; 158 | 159 | handleCardMoveAcrossLanes = (fromLaneId, toLaneId, cardId, index) => { 160 | console.log('handleCardMoveAcrossLanes', { 161 | fromLaneId, 162 | toLaneId, 163 | cardId, 164 | index 165 | }); 166 | 167 | let id = toLaneId ? atob(toLaneId) : null; 168 | id = id ? id.split(':') : null; 169 | 170 | const variables = { 171 | input: { 172 | clientMutationId: 'CreateCard', 173 | id: cardId ? cardId : null, 174 | taskLanes: toLaneId && id && id[1] ? { 175 | "append": false, 176 | "nodes": [ 177 | { 178 | "id": parseInt( id[1] ) 179 | } 180 | ] 181 | } : null, 182 | } 183 | }; 184 | 185 | const UPDATE_TASK_LANE = gql` 186 | mutation UpdateTaskLane( $input: UpdateTaskInput! ){ 187 | updateTask( input: $input ) { 188 | task { 189 | id 190 | } 191 | } 192 | } 193 | `; 194 | 195 | client.mutate({ 196 | mutation: UPDATE_TASK_LANE, 197 | variables 198 | }); 199 | 200 | }; 201 | 202 | handleLaneAdd = (params) => { 203 | 204 | const {title} = params; 205 | 206 | const CREATE_LANE = gql` 207 | mutation CreateTaskLane($input:CreateTaskLaneInput!){ 208 | createTaskLane(input: $input) { 209 | taskLane { 210 | id 211 | title: name 212 | } 213 | } 214 | } 215 | `; 216 | 217 | client.mutate({ 218 | mutation: CREATE_LANE, 219 | variables: { 220 | input: { 221 | clientMutationId: "Create Task Lane", 222 | name: title, 223 | } 224 | }, 225 | refetchQueries: [{ query: GET_TASK_LANES_AND_TASKS }] 226 | }); 227 | }; 228 | 229 | handleLaneDelete = (laneId) => { 230 | 231 | const DELETE_LANE = gql` 232 | mutation DeleteLane( $input: DeleteTaskLaneInput! ) { 233 | deleteTaskLane( input:$input ) { 234 | deletedId 235 | } 236 | }`; 237 | 238 | client.mutate({ 239 | mutation: DELETE_LANE, 240 | variables: { 241 | input: { 242 | clientMutationId: "DeleteLane", 243 | id: laneId, 244 | }, 245 | } 246 | }); 247 | 248 | }; 249 | 250 | handleLaneUpdate = (laneId, data) => { 251 | console.log('handleLaneUpdate', { 252 | laneId, 253 | data 254 | }) 255 | 256 | const UPDATE_LANE = gql` 257 | mutation updateTaskLane( $input: UpdateTaskLaneInput! ) { 258 | updateTaskLane( input: $input ) { 259 | taskLane { 260 | id 261 | } 262 | } 263 | } 264 | `; 265 | 266 | if ( data && data.title ) { 267 | client.mutate({ 268 | mutation: UPDATE_LANE, 269 | variables: { 270 | input: { 271 | clientMutationId: 'UpdateTaskLane', 272 | id: laneId, 273 | title: data.title 274 | } 275 | } 276 | }) 277 | } 278 | }; 279 | 280 | render() { 281 | 282 | /** 283 | * If not logged in, show the login page 284 | */ 285 | if (!this.state.loggedIn) { 286 | return ; 287 | } else { 288 | return ( 289 | 290 | {({loading, error, data}) => { 291 | 292 | if (loading) return ; 293 | if (error) return `Error! ${error.message}`; 294 | return ( 295 | 296 |
301 | 307 | { 308 | localStorage.clear(); 309 | this.setState({loggedIn:false}); 310 | }}> 311 | Logout 312 | 313 | 314 |
315 | 316 | 331 | 332 |
333 | ); 334 | }} 335 |
336 | ); 337 | } 338 | } 339 | } 340 | 341 | export default KanbanBoard 342 | -------------------------------------------------------------------------------- /wordpress-plugin/wp-graphql-kanban/README.md: -------------------------------------------------------------------------------- 1 | # WPGraphQL Kanban 2 | 3 | This is a WordPress plugin. It is an extension for WPGraphQL that extends the WPGraphQL 4 | Schema for use with the WPGraphQL + Gatsby Kanban project. 5 | 6 | This plugin requires WPGraphQL and WPGraphQL for JWT Authentication 7 | to be installed as well. 8 | -------------------------------------------------------------------------------- /wordpress-plugin/wp-graphql-kanban/wp-graphql-kanban.php: -------------------------------------------------------------------------------- 1 | __( 'Tasks', 'wp-graphql-kanban' ), 16 | 'show_ui' => true, 17 | 'show_in_graphql' => true, 18 | 'graphql_single_name' => 'Task', 19 | 'graphql_plural_name' => 'Tasks', 20 | 'taxonomies' => [ 'task-lane', 'post_tag' ], 21 | 'supports' => [ 'title', 'excerpt', 'editor' ] 22 | ] ); 23 | register_taxonomy( 'task-lane', [ 'task' ], [ 24 | 'label' => __( 'Task Lanes', 'wp-graphql-kanban' ), 25 | 'show_ui' => true, 26 | 'show_admin_column' => true, 27 | 'show_in_graphql' => true, 28 | 'graphql_single_name' => 'TaskLane', 29 | 'graphql_plural_name' => 'TaskLanes' 30 | ] ); 31 | } ); 32 | 33 | add_action( 'graphql_register_types', function( $type_registry ) { 34 | 35 | register_graphql_field( 'RootQuery', 'lanes', [ 36 | 'type' => [ 37 | 'list_of' => 'TaskLane', 38 | ], 39 | 'description' => __( 'All Task Lanes', 'wp-graphql-kanban' ), 40 | 'resolve' => function() { 41 | $terms = get_terms([ 42 | 'taxonomy' => 'task-lane', 43 | 'hide_empty' => false, 44 | ]); 45 | 46 | return $terms ? array_map( function( $term ) { return new \WPGraphQL\Model\Term( $term ); }, $terms ) : []; 47 | } 48 | ] ); 49 | 50 | register_graphql_fields( 'TaskLane', [ 51 | 'cards' => [ 52 | 'type' => [ 53 | 'list_of' => 'Task', 54 | ], 55 | 'description' => __( 'All tasks in the task lane', 'wp-graphql-kanban' ), 56 | 'resolve' => function( $lane ) { 57 | 58 | $cards = new WP_Query([ 59 | 'post_type' => 'task', 60 | 'post_status' => 'publish', 61 | 'posts_per_page' => 1000, 62 | 'task-lane' => $lane->slug, 63 | 'fields' => 'id' 64 | ]); 65 | 66 | return $cards->posts ? array_map( function( $task ) { return new \WPGraphQL\Model\Post( $task ); }, $cards->posts ) : []; 67 | } 68 | ], 69 | 'label' => [ 70 | 'type' => 'String', 71 | 'description' => __( 'Label of the task (the post excerpt)', 'wp-graphql-kanban' ), 72 | 'resolve' => function( $lane ) { 73 | return $lane->description ? $lane->description : null; 74 | } 75 | ], 76 | 77 | ]); 78 | 79 | register_graphql_fields( 'Task', [ 80 | 'description' => [ 81 | 'type' => 'String', 82 | 'description' => __( 'Short description of the task (the post content)', 'wp-graphql-kanban' ), 83 | 'resolve' => function( $post ) { 84 | return $post->post_content ? apply_filters( 'the_content', $post->post_content ) : null; 85 | } 86 | ], 87 | 'label' => [ 88 | 'type' => 'String', 89 | 'description' => __( 'Label of the task (the post excerpt)', 'wp-graphql-kanban' ), 90 | 'resolve' => function( $post ) { 91 | return $post->post_excerpt ? apply_filters( 'the_content', $post->post_excerpt ) : null; 92 | } 93 | ], 94 | ] ); 95 | 96 | register_graphql_fields( 'Tag', [ 97 | 'color' => [ 98 | 'type' => 'String', 99 | 'description' => __( 'The color of the tag', 'wp-graphql-kanban' ), 100 | 'resolve' => function( $tag ) { 101 | $color = get_term_meta( $tag->term_id, 'color', true ); 102 | return $color ? $color : '#222222'; 103 | } 104 | ], 105 | 'bgColor' => [ 106 | 'type' => 'String', 107 | 'description' => __( 'Background Color of the tag', 'wp-graphql-kanban' ), 108 | 'resolve' => function( $tag ) { 109 | $bgcolor = get_term_meta( $tag->term_id, 'bg_color', true ); 110 | return $bgcolor ? $bgcolor : '#222222'; 111 | } 112 | ] 113 | ]); 114 | 115 | } ); 116 | --------------------------------------------------------------------------------