diff --git a/enact-all-samples/package.json b/enact-all-samples/package.json index f0ea8015..c4e43024 100644 --- a/enact-all-samples/package.json +++ b/enact-all-samples/package.json @@ -36,8 +36,12 @@ "@enact/spotlight": "next", "@enact/ui": "next", "@enact/webos": "next", + "apollo-boost": "^0.1.8", + "graphql": "^0.13.2", + "graphql-tag": "^2.9.2", "prop-types": "^15.6.0", "react": "^16.3.2", + "react-apollo": "^2.1.5", "react-dom": "^16.3.2", "react-redux": "^5.0.3", "react-router-dom": "^4.1.1", diff --git a/enact-all-samples/src/index.js b/enact-all-samples/src/index.js index 3ebe8879..d5001b3d 100644 --- a/enact-all-samples/src/index.js +++ b/enact-all-samples/src/index.js @@ -7,6 +7,7 @@ import PatternActivityPanelsDeepLinking from '../../pattern-activity-panels-deep import PatternActivityPanelsRedux from '../../pattern-activity-panels-redux/src/main'; import PatternDynamicPanel from '../../pattern-dynamic-panel/src/App'; import PatternExpandableList from '../../pattern-expandablelist-object/src/App'; +import PatternGraphQL from '../../pattern-graphql/src/App'; import PatternListDetails from '../../pattern-list-details/src/App'; import PatternListDetailsRedux from '../../pattern-list-details-redux/src/main'; import PatternLocaleSwitching from '../../pattern-locale-switching/src/main'; @@ -28,6 +29,7 @@ export const routes = [ {path: '/PatternActivityPanelsRedux', component: PatternActivityPanelsRedux}, {path: '/PatternDynamicPanel', component: PatternDynamicPanel}, {path: '/PatternExpandableList', component: PatternExpandableList}, + {path: '/PatternGraphQL', component: PatternGraphQL}, {path: '/PatternListDetails', component: PatternListDetails}, {path: '/PatternListDetailsRedux', component: PatternListDetailsRedux}, {path: '/PatternLocaleSwitching', component: PatternLocaleSwitching}, diff --git a/pattern-graphql/.gitignore b/pattern-graphql/.gitignore new file mode 100644 index 00000000..0834513f --- /dev/null +++ b/pattern-graphql/.gitignore @@ -0,0 +1,16 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules + +# testing +coverage + +# production +build +dist + +# misc +.DS_Store +npm-debug.log +.env diff --git a/pattern-graphql/README.md b/pattern-graphql/README.md new file mode 100644 index 00000000..1e7e3bb3 --- /dev/null +++ b/pattern-graphql/README.md @@ -0,0 +1,107 @@ +## Use Apollo with Enact and GitHub GraphqQL API + +A sample application that demonstrates how to use GraphqQL with Enact components. It uses [Apollo Client](https://github.com/apollographql/apollo-client), (GraphqQL client.) + +Run `npm install` then `npm run serve` to have the app running on [http://localhost:8080](http://localhost:8080), where you can view it in your browser. + +#### Enact Components Used +- `moonstone/Panels/ActivityPanels` +- `moonstone/Panels/Panel` +- `moonstone/Panels/Header` +- `moonstone/Button` +- `moonstone/VirtualList` +- `moonstone/Item` +- `moonstone/Divider` +- `moonstone/Image` +- `moonstone/Input` +- `ui/Layout/Column` +- `ui/Layout/Cell` +- `ui/resolution/scale` + +## Setup and use +1. Get a personal access token for the [GitHub API](https://github.com/settings/tokens/new). + - Assign a name to the token and select the "read:org" under "admin:org" scope. +2. Replace the dummy token in [src/config.json](src/config.json) with the token generated above. +3. Install npm modules. + +```bash +npm install +``` +4. Serve. + +```bash +npm run serve +``` + +5. Open [localhost](http://localhost:8080/). +6. Search for a GitHub id, selecting which information you wish to retrieve. + + +### How Apollo is used. + +In **App.js**, a new `ApolloClient` is created with GitHub URI and request setup: + +[src/App/App.js] +```javascript +import ApolloClient from "apollo-boost"; + +const client = new ApolloClient({ + uri: 'https://api.github.com/graphql', + request: operation => { + operation.setContext({ + headers: { + authorization: `Bearer ${config.token}` + } + }); + } +}); +``` + +Next, the `client` prop is passed to the `ApolloProvider` component, which wraps the components that need the queried data: +[src/App/App.js](src/App/App.js) +```javascript +import { ApolloProvider } from "react-apollo"; + + + ... + +``` + +Finally, a GraphQL query (`GET_USER`) is created using `gql` and is passed as the `query` prop to the `Query` component: + +[src/views/Detail.js](src/views/Detail.js) +```javascript +import { Query } from "react-apollo"; +import gql from "graphql-tag"; + +const GET_USER = gql` + query($login: String!) { + user(login: $login) { + name + avatarUrl + organizations(first: 10) { + nodes { + name + } + } + repositories(first: 10) { + nodes { + name + url + } + } + followers(first: 10) { + nodes { + name + } + } + } + } +`; + + + {({loading, data}) => { + ... + }} + +``` diff --git a/pattern-graphql/package.json b/pattern-graphql/package.json new file mode 100644 index 00000000..5c125e5e --- /dev/null +++ b/pattern-graphql/package.json @@ -0,0 +1,46 @@ +{ + "name": "pattern-graphql", + "version": "1.0.0", + "description": "An Enact Moonstone GraphQL application.", + "author": "Hailey HanGyeol Ryu", + "main": "src/index.js", + "scripts": { + "serve": "enact serve", + "pack": "enact pack", + "pack-p": "enact pack -p", + "watch": "enact pack --watch", + "clean": "enact clean", + "lint": "enact lint --strict .", + "license": "enact license", + "test": "enact test start --single-run", + "test-watch": "enact test start" + }, + "license": "Apache-2.0", + "private": true, + "repository": "https://github.com/enactjs/samples", + "enact": { + "theme": "moonstone" + }, + "eslintConfig": { + "extends": "enact/strict" + }, + "eslintIgnore": [ + "node_modules/*", + "build/*", + "dist/*" + ], + "dependencies": { + "@enact/core": "next", + "@enact/i18n": "next", + "@enact/moonstone": "next", + "@enact/spotlight": "next", + "@enact/ui": "next", + "apollo-boost": "^0.1.8", + "graphql": "^0.13.2", + "graphql-tag": "^2.9.2", + "prop-types": "^15.6.0", + "react": "^16.4.0", + "react-apollo": "^2.1.5", + "react-dom": "^16.4.0" + } +} diff --git a/pattern-graphql/resources/ilibmanifest.json b/pattern-graphql/resources/ilibmanifest.json new file mode 100644 index 00000000..5916671d --- /dev/null +++ b/pattern-graphql/resources/ilibmanifest.json @@ -0,0 +1,3 @@ +{ + "files": [] +} \ No newline at end of file diff --git a/pattern-graphql/src/App/App.js b/pattern-graphql/src/App/App.js new file mode 100644 index 00000000..11747b23 --- /dev/null +++ b/pattern-graphql/src/App/App.js @@ -0,0 +1,80 @@ +import {ActivityPanels} from '@enact/moonstone/Panels'; +import ApolloClient from 'apollo-boost'; +import {ApolloProvider} from 'react-apollo'; +import MoonstoneDecorator from '@enact/moonstone/MoonstoneDecorator'; +import Notification from '@enact/moonstone/Notification'; +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; + +import Detail from '../views/Detail'; +import Search from '../views/Search'; +import config from '../config.json'; + + +const client = new ApolloClient({ + uri: 'https://api.github.com/graphql', + request: operation => { + operation.setContext({ + headers: { + authorization: `Bearer ${config.token}` + } + }); + } +}); + +class AppBase extends Component { + static propTypes = { + index: PropTypes.number, + onSearch: PropTypes.func + }; + + static defaultProps = { + index: 0 + }; + + constructor (props) { + super(props); + + this.state = { + index: this.props.index, + fol: false, + org: false, + repo: true, + userId: '' + }; + } + + handleSelectBreadcrumb = ({index}) => { + this.setState({index}); + }; + + onSearch = ({userId, repo, fol, org}) => { + this.setState({ + index: 1, + userId, + repo, + fol, + org + }); + }; + + render () { + const {index, userId, repo, org, fol} = this.state; + + return ( + + {!config.token &&

Please set your github token in src/config.json.

} + + + + +
+ ); + } +} + +const App = MoonstoneDecorator(AppBase); + +export default App; diff --git a/pattern-graphql/src/App/package.json b/pattern-graphql/src/App/package.json new file mode 100644 index 00000000..add0ba2a --- /dev/null +++ b/pattern-graphql/src/App/package.json @@ -0,0 +1,3 @@ +{ + "main": "App.js" +} \ No newline at end of file diff --git a/pattern-graphql/src/components/List.js b/pattern-graphql/src/components/List.js new file mode 100644 index 00000000..7919943c --- /dev/null +++ b/pattern-graphql/src/components/List.js @@ -0,0 +1,43 @@ +import {Cell} from '@enact/ui/Layout'; +import Divider from '@enact/moonstone/Divider'; +import Item from '@enact/moonstone/Item'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; +import {scale} from '@enact/ui/resolution'; +import VirtualList from '@enact/moonstone/VirtualList'; + +class List extends Component { + static propTypes = { + list: PropTypes.arrayOf(PropTypes.object).isRequired, + title: PropTypes.string, + userId: PropTypes.string + } + + renderItem = ({index, ...rest}) => ( + + {this.props.list[index].name} + + ); + + render () { + const {list, title} = this.props; + + return [ + {title}, + ]; + } +} + +export default List; diff --git a/pattern-graphql/src/config.json b/pattern-graphql/src/config.json new file mode 100644 index 00000000..d81c8a42 --- /dev/null +++ b/pattern-graphql/src/config.json @@ -0,0 +1,3 @@ +{ + "token":"" +} diff --git a/pattern-graphql/src/index.js b/pattern-graphql/src/index.js new file mode 100644 index 00000000..b4e758df --- /dev/null +++ b/pattern-graphql/src/index.js @@ -0,0 +1,13 @@ +import React from 'react'; +import {render} from 'react-dom'; + +import App from './App'; + +const appElement = (); + +// In a browser environment, render instead of exporting +if (typeof window !== 'undefined') { + render(appElement, document.getElementById('root')); +} + +export default appElement; diff --git a/pattern-graphql/src/views/Detail.js b/pattern-graphql/src/views/Detail.js new file mode 100644 index 00000000..08825100 --- /dev/null +++ b/pattern-graphql/src/views/Detail.js @@ -0,0 +1,67 @@ +import {Column} from '@enact/ui/Layout'; +import gql from 'graphql-tag'; +import {Header, Panel} from '@enact/moonstone/Panels'; +import Image from '@enact/moonstone/Image'; +import Spinner from '@enact/moonstone/Spinner'; +import kind from '@enact/core/kind'; +import List from '../components/List'; +import PropTypes from 'prop-types'; +import {Query} from 'react-apollo'; +import React from 'react'; + +const GET_USER = gql` + query ($login: String!) { + user (login: $login) { + name + login + avatarUrl + organizations (first: 100) { + nodes { + name + } + } + repositories (first: 100) { + nodes { + name + } + } + followers (first: 100) { + nodes { + name + } + } + } + } +`; + +const Detail = kind({ + name: 'Detail', + + propTypes: { + userId: PropTypes.string.isRequired, + fol: PropTypes.bool, + org: PropTypes.bool, + repo: PropTypes.bool + }, + + render: ({userId, repo, org, fol, ...rest}) => ( + + {({loading, error, data}) => { + if (loading) return Loading...; + if (error) return

{error.message}

; + + return +
+ +
+ + {repo && } + {org && } + {fol && } + +
; + }} +
+ ) +}); +export default Detail; diff --git a/pattern-graphql/src/views/Search.js b/pattern-graphql/src/views/Search.js new file mode 100644 index 00000000..bb972d34 --- /dev/null +++ b/pattern-graphql/src/views/Search.js @@ -0,0 +1,74 @@ +import FormCheckboxItem from '@enact/moonstone/FormCheckboxItem'; +import {Header, Panel} from '@enact/moonstone/Panels'; +import IconButton from '@enact/moonstone/IconButton'; +import Input from '@enact/moonstone/Input'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; + +class Search extends Component { + static propTypes = { + apiToken: PropTypes.string, + handleIdChange: PropTypes.func, + onSearch: PropTypes.func, + userId: PropTypes.string + }; + + constructor (props) { + super(props); + this.state = { + fol: false, + org: false, + repo: true, + userId: '' + }; + } + + handleIdChange = (ev) => { + this.setState({userId: ev.value}); + }; + + onSearch = () => { + this.props.onSearch(this.state); + } + + onRepoToggle = () => { + this.setState(prevState => ({repo: !prevState.repo})); + } + + onFolToggle = () => { + this.setState(prevState => ({fol: !prevState.fol})); + } + + onOrgToggle = () => { + this.setState(prevState => ({org: !prevState.org})); + } + + onKeyUp = (ev) => { + if (ev.keyCode === 13) { + this.props.onSearch(this.state); + } + } + + render () { + const {...rest} = this.props; + const {repo, org, fol} = this.state; + delete rest.onSearch; + delete rest.handleIdChange; + + return ( + +
+ + search + Repositories + Organizations + Followers + + ); + } +} + +export default Search; diff --git a/pattern-graphql/webos-meta/appinfo.json b/pattern-graphql/webos-meta/appinfo.json new file mode 100644 index 00000000..0be50d78 --- /dev/null +++ b/pattern-graphql/webos-meta/appinfo.json @@ -0,0 +1,12 @@ +{ + "id": "com.enactjs.graphql", + "version": "1.0.0", + "vendor": "LGE-SVL", + "type": "web", + "main": "index.html", + "title": "Enact GraphQL Sample", + "icon": "icon.png", + "miniicon": "icon-mini.png", + "largeIcon": "icon-large.png", + "uiRevision": 2 +} diff --git a/pattern-graphql/webos-meta/icon-large.png b/pattern-graphql/webos-meta/icon-large.png new file mode 100644 index 00000000..d237e9fb Binary files /dev/null and b/pattern-graphql/webos-meta/icon-large.png differ diff --git a/pattern-graphql/webos-meta/icon-mini.png b/pattern-graphql/webos-meta/icon-mini.png new file mode 100644 index 00000000..9771fac4 Binary files /dev/null and b/pattern-graphql/webos-meta/icon-mini.png differ diff --git a/pattern-graphql/webos-meta/icon.png b/pattern-graphql/webos-meta/icon.png new file mode 100644 index 00000000..e616273a Binary files /dev/null and b/pattern-graphql/webos-meta/icon.png differ