├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── issue-tracker
├── .babelrc
├── .eslintrc
├── .gitignore
├── .prettierrc
├── README.md
├── config-overrides.js
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── schema
│ └── schema.graphql
├── src
│ ├── ErrorBoundary.tsx
│ ├── HomeRoot.tsx
│ ├── IssueActions.tsx
│ ├── IssueDetailComments.tsx
│ ├── IssueDetailRoot.tsx
│ ├── IssueListItem.tsx
│ ├── Issues.tsx
│ ├── JSResource.ts
│ ├── RelayEnvironment.ts
│ ├── Root.tsx
│ ├── SuspenseImage.tsx
│ ├── __generated__
│ │ ├── HomeRootIssuesQuery.graphql.ts
│ │ ├── IssueActionsAddCommentMutation.graphql.ts
│ │ ├── IssueActionsCloseIssueMutation.graphql.ts
│ │ ├── IssueActionsReopenIssueMutation.graphql.ts
│ │ ├── IssueActions_issue.graphql.ts
│ │ ├── IssueDetailCommentsQuery.graphql.ts
│ │ ├── IssueDetailComments_issue.graphql.ts
│ │ ├── IssueDetailRootQuery.graphql.ts
│ │ ├── IssueListItem_issue.graphql.ts
│ │ ├── IssuesPaginationQuery.graphql.ts
│ │ ├── Issues_repository.graphql.ts
│ │ └── RootQuery.graphql.ts
│ ├── index.css
│ ├── index.tsx
│ ├── react-app-env.d.ts
│ ├── routes.ts
│ ├── routing
│ │ ├── Link.tsx
│ │ ├── RouteRenderer.css
│ │ ├── RouteRenderer.tsx
│ │ ├── RoutingContext.ts
│ │ └── createRouter.ts
│ └── useMutation.ts
├── tsconfig.json
└── yarn.lock
└── todo
├── .babelrc
├── .editorconfig
├── .gitignore
├── .prettierrc
├── README.md
├── data
├── database.js
├── schema.graphql
└── schema.js
├── package.json
├── public
├── base.css
├── index.css
├── index.html
└── learn.json
├── scripts
└── updateSchema.js
├── server.js
├── ts
├── ErrorBoundaryWithRetry.tsx
├── __relay_artifacts__
│ ├── AddTodoMutation.graphql.ts
│ ├── ChangeTodoStatusMutation.graphql.ts
│ ├── MarkAllTodosMutation.graphql.ts
│ ├── RemoveCompletedTodosMutation.graphql.ts
│ ├── RemoveTodoMutation.graphql.ts
│ ├── RenameTodoMutation.graphql.ts
│ ├── TodoApp_viewer.graphql.ts
│ ├── TodoListFooter_viewer.graphql.ts
│ ├── TodoList_viewer.graphql.ts
│ ├── TodoRootQuery.graphql.ts
│ ├── Todo_todo.graphql.ts
│ └── Todo_viewer.graphql.ts
├── app.tsx
├── components
│ ├── Todo.tsx
│ ├── TodoApp.tsx
│ ├── TodoList.tsx
│ ├── TodoListFooter.tsx
│ ├── TodoRoot.tsx
│ └── TodoTextInput.tsx
├── definitions.d.ts
└── mutations
│ ├── AddTodoMutation.ts
│ ├── ChangeTodoStatusMutation.ts
│ ├── MarkAllTodosMutation.ts
│ ├── RemoveCompletedTodosMutation.ts
│ ├── RemoveTodoMutation.ts
│ └── RenameTodoMutation.ts
├── tsconfig.json
├── tslint.json
└── yarn.lock
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to.
4 | Please read the [full text](https://code.fb.com/codeofconduct/)
5 | so that you can understand what actions will and will not be tolerated.
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | We want to make contributing to this project as easy and transparent as possible.
4 |
5 | ## Our Development Process
6 |
7 | We actively welcome your input via pull requests or issues.
8 |
9 | ## Pull Requests
10 |
11 | 1. Fork the repo and create your branch from `master`.
12 | 2. If you haven't already, complete the Contributor License Agreement ("CLA").
13 |
14 | ## Contributor License Agreement ("CLA")
15 |
16 | In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects.
17 |
18 | Complete your CLA [here](https://code.facebook.com/cla).
19 |
20 | ## Issues
21 |
22 | Ask questions, provide feedback, or open new topics for discussion by [opening an issue](https://github.com/relayjs/relay-examples/issues).
23 |
24 | ## License
25 |
26 | By contributing to relay-examples, you agree that your contributions will be licensed under the terms described in [LICENSE.md](https://github.com/relayjs/relay-examples/blob/master/LICENSE.md).
27 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016-present, Facebook, Inc. All rights reserved.
2 |
3 | The examples provided by Facebook are for non-commercial testing and evaluation purposes only. Facebook reserves all rights not expressly granted.
4 |
5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Relay Examples [](https://travis-ci.org/relayjs/relay-examples)
2 |
3 | A collection of example applications using [Relay](https://github.com/facebook/relay).
4 |
5 | # Contributing
6 |
7 | See [CONTRIBUTING.md](https://github.com/relayjs/relay-examples/blob/master/CONTRIBUTING.md).
8 |
9 | # License
10 |
11 | See [LICENSE.md](https://github.com/relayjs/relay-examples/blob/master/LICENSE.md).
12 |
--------------------------------------------------------------------------------
/issue-tracker/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["@babel/plugin-proposal-optional-chaining"]
3 | }
4 |
--------------------------------------------------------------------------------
/issue-tracker/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "extends": "react-app",
7 | "globals": {
8 | "Atomics": "readonly",
9 | "SharedArrayBuffer": "readonly"
10 | },
11 | "parser": "@typescript-eslint/parser",
12 | "parserOptions": {
13 | "ecmaFeatures": {
14 | "jsx": true
15 | },
16 | "ecmaVersion": 2018,
17 | "sourceType": "module"
18 | },
19 | "plugins": ["react", "@typescript-eslint/eslint-plugin"],
20 | "rules": {
21 | "indent": ["error", 2],
22 | "linebreak-style": ["error", "unix"],
23 | "quotes": ["error", "single"],
24 | "semi": ["error", "never"]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/issue-tracker/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .vscode
26 |
--------------------------------------------------------------------------------
/issue-tracker/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "semi": false
5 | }
6 |
--------------------------------------------------------------------------------
/issue-tracker/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `yarn test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `yarn build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `yarn eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
--------------------------------------------------------------------------------
/issue-tracker/config-overrides.js:
--------------------------------------------------------------------------------
1 | const { useBabelRc, override } = require('customize-cra')
2 | module.exports = override(useBabelRc())
3 |
--------------------------------------------------------------------------------
/issue-tracker/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "issue-tracker",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "yarn run relay; concurrently --kill-others --names \"react-app-rewired,relay\" \"react-app-rewired start\" \"yarn run relay --watch\"",
7 | "build": "yarn run relay && react-app-rewired build",
8 | "test": "react-app-rewired test",
9 | "eject": "react-scripts eject",
10 | "lint": "eslint --fix --ext .js,.ts,.tsx",
11 | "update-schema": "yarn get-graphql-schema -h \"Authorization=bearer $REACT_APP_GITHUB_AUTH_TOKEN\" https://api.github.com/graphql > schema/schema.graphql",
12 | "relay": "yarn run relay-compiler --schema schema/schema.graphql --src ./src/ $@ --language typescript"
13 | },
14 | "dependencies": {
15 | "history": "^4.10.1",
16 | "react": "^0.0.0-experimental-38dd17ab9",
17 | "react-dom": "^0.0.0-experimental-38dd17ab9",
18 | "react-markdown": "^4.2.2",
19 | "react-relay": "^0.0.0-experimental-a1a40b68",
20 | "react-router": "^5.1.2",
21 | "react-router-config": "^5.1.1",
22 | "relay-runtime": "7.0.0"
23 | },
24 | "devDependencies": {
25 | "@babel/plugin-proposal-optional-chaining": "^7.6.0",
26 | "@types/history": "^4.7.3",
27 | "@types/jest": "24.0.21",
28 | "@types/node": "12.12.5",
29 | "@types/react": "16.9.11",
30 | "@types/react-dom": "16.9.3",
31 | "@types/react-relay": "^7.0.0",
32 | "@types/react-router-config": "^5.0.1",
33 | "@types/relay-runtime": "^6.0.9",
34 | "@typescript-eslint/eslint-plugin": "^2.6.1",
35 | "@typescript-eslint/parser": "^2.6.1",
36 | "babel-plugin-relay": "7.0.0",
37 | "concurrently": "^5.0.0",
38 | "customize-cra": "^0.8.0",
39 | "eslint": "^6.6.0",
40 | "eslint-config-react-app": "^5.0.2",
41 | "eslint-plugin-flowtype": "^4.3.0",
42 | "eslint-plugin-import": "^2.18.2",
43 | "eslint-plugin-jsx-a11y": "^6.2.3",
44 | "eslint-plugin-react": "^7.16.0",
45 | "eslint-plugin-react-hooks": "^2.2.0",
46 | "get-graphql-schema": "^2.1.2",
47 | "graphql": "^14.5.8",
48 | "husky": "^3.0.9",
49 | "lint-staged": "^9.4.2",
50 | "prettier": "^1.19.1",
51 | "react-app-rewired": "^2.1.5",
52 | "react-scripts": "3.2.0",
53 | "relay-compiler": "7.0.0",
54 | "relay-compiler-language-typescript": "^10.1.0",
55 | "typescript": "^3.7.2"
56 | },
57 | "browserslist": {
58 | "production": [
59 | ">0.2%",
60 | "not dead",
61 | "not op_mini all"
62 | ],
63 | "development": [
64 | "last 1 chrome version",
65 | "last 1 firefox version",
66 | "last 1 safari version"
67 | ]
68 | },
69 | "husky": {
70 | "hooks": {
71 | "pre-commit": "lint-staged"
72 | }
73 | },
74 | "lint-staged": {
75 | "src/**/*.{js,ts,jsx,tsx}": [
76 | "yarn prettier --write",
77 | "yarn lint",
78 | "git add"
79 | ]
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/issue-tracker/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renanmav/relay-examples-typescript/aa6b3a97af23ab188ed2ae2b7637c17519b00450/issue-tracker/public/favicon.ico
--------------------------------------------------------------------------------
/issue-tracker/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
25 | Relay Example App
26 |
27 |
28 |
29 | You need to enable JavaScript to run this app.
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/issue-tracker/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renanmav/relay-examples-typescript/aa6b3a97af23ab188ed2ae2b7637c17519b00450/issue-tracker/public/logo192.png
--------------------------------------------------------------------------------
/issue-tracker/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renanmav/relay-examples-typescript/aa6b3a97af23ab188ed2ae2b7637c17519b00450/issue-tracker/public/logo512.png
--------------------------------------------------------------------------------
/issue-tracker/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Relay Example App",
3 | "name": "Relay Example App - GitHub Issues Clone",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
--------------------------------------------------------------------------------
/issue-tracker/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/issue-tracker/src/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | interface State {
4 | error: {
5 | message: string
6 | source: {}
7 | } | null
8 | }
9 |
10 | /**
11 | * A reusable component for handling errors in a React (sub)tree.
12 | */
13 | export default class ErrorBoundary extends Component {
14 | state: State = { error: null }
15 |
16 | static getDerivedStateFromError(error: Error) {
17 | return {
18 | error,
19 | }
20 | }
21 |
22 | render() {
23 | const { error } = this.state
24 | if (error) {
25 | return (
26 |
27 |
Error: {error.message}
28 |
29 |
{JSON.stringify(error.source, null, 2)}
30 |
31 |
32 | )
33 | }
34 | return this.props.children
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/issue-tracker/src/HomeRoot.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { usePreloadedQuery } from 'react-relay/hooks'
3 | import graphql from 'babel-plugin-relay/macro'
4 |
5 | import { HomeRootIssuesQuery } from './__generated__/HomeRootIssuesQuery.graphql'
6 | import { PreloadedQuery } from 'react-relay/lib/relay-experimental/EntryPointTypes'
7 | import Issues from './Issues'
8 |
9 | interface Props {
10 | prepared: {
11 | issuesQuery: PreloadedQuery
12 | }
13 | }
14 |
15 | const HomeRoot = (props: Props) => {
16 | // Defines *what* data the component needs via a query. The responsibility of
17 | // actually fetching this data belongs to the route definition: it calls
18 | // preloadQuery() with the query and variables, and the result is passed
19 | // on props.prepared.issuesQuery - see src/routes.js
20 | const data = usePreloadedQuery(
21 | graphql`
22 | query HomeRootIssuesQuery($owner: String!, $name: String!) {
23 | repository(owner: $owner, name: $name) {
24 | # Compose the data dependencies of child components
25 | # by spreading their fragments:
26 | ...Issues_repository
27 | }
28 | }
29 | `,
30 | props.prepared.issuesQuery,
31 | )
32 | const { repository } = data
33 |
34 | return
35 | }
36 |
37 | export default HomeRoot
38 |
--------------------------------------------------------------------------------
/issue-tracker/src/IssueActions.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from 'react'
2 | import { ConnectionHandler } from 'relay-runtime'
3 | import { useFragment } from 'react-relay/hooks'
4 | import graphql from 'babel-plugin-relay/macro'
5 |
6 | import useMutation from './useMutation'
7 |
8 | import { IssueActions_issue$key } from './__generated__/IssueActions_issue.graphql'
9 |
10 | import { IssueActionsAddCommentMutation as AddComment } from './__generated__/IssueActionsAddCommentMutation.graphql'
11 | import { IssueActionsCloseIssueMutation as CloseIssue } from './__generated__/IssueActionsCloseIssueMutation.graphql'
12 | import { IssueActionsReopenIssueMutation as ReopenIssue } from './__generated__/IssueActionsReopenIssueMutation.graphql'
13 |
14 | const AddCommentMutation = graphql`
15 | mutation IssueActionsAddCommentMutation($input: AddCommentInput!) {
16 | addComment(input: $input) {
17 | subject {
18 | id
19 | }
20 | commentEdge {
21 | __id
22 | node {
23 | id
24 | author {
25 | login
26 | avatarUrl
27 | }
28 | body
29 | }
30 | }
31 | }
32 | }
33 | `
34 |
35 | const CloseIssueMutation = graphql`
36 | mutation IssueActionsCloseIssueMutation($input: CloseIssueInput!) {
37 | closeIssue(input: $input) {
38 | issue {
39 | closed
40 | }
41 | }
42 | }
43 | `
44 |
45 | const ReopenIssueMutation = graphql`
46 | mutation IssueActionsReopenIssueMutation($input: ReopenIssueInput!) {
47 | reopenIssue(input: $input) {
48 | issue {
49 | closed
50 | }
51 | }
52 | }
53 | `
54 |
55 | interface Props {
56 | issue: IssueActions_issue$key
57 | }
58 |
59 | export default function IssueActions(props: Props) {
60 | // Track the current comment text - this is used as the value of the comment textarea
61 | const [commentText, setCommentText] = useState('')
62 |
63 | const [isCommentPending, addComment] = useMutation(
64 | AddCommentMutation,
65 | )
66 | const [isClosePending, closeIssue] = useMutation(
67 | CloseIssueMutation,
68 | )
69 | const [isReopenPending, reopenIssue] = useMutation(
70 | ReopenIssueMutation,
71 | )
72 | const isPending = isCommentPending || isClosePending || isReopenPending
73 |
74 | // Get the data we need about the issue in order to execute the mutation. Right now that's just
75 | // the id, but in the future this component might neeed more information.
76 | const data = useFragment(
77 | graphql`
78 | fragment IssueActions_issue on Issue {
79 | id
80 | closed
81 | }
82 | `,
83 | props.issue,
84 | )
85 | const issueId = data.id
86 |
87 | const onChange = useCallback((e: React.ChangeEvent) => {
88 | setCommentText(e.target.value)
89 | }, [])
90 |
91 | const onSubmit = useCallback(
92 | (e: React.FormEvent) => {
93 | e.preventDefault()
94 | addComment({
95 | variables: {
96 | input: {
97 | body: commentText,
98 | subjectId: issueId,
99 | },
100 | },
101 | /**
102 | * Relay merges data from the mutation result based on each response object's `id` value.
103 | * In this case, however, we also want to add the new comment to the list of issues: Relay
104 | * doesn't magically know where addComment.commentEdge should be added into the data graph.
105 | * So we define an `updater` function to imperatively update thee store.
106 | */
107 | updater: store => {
108 | // Get a reference to the issue
109 | const issue = store.get(issueId)
110 | if (issue == null) return
111 | // Get the list of comments using the same 'key' value as defined in
112 | // IssueDetailComments
113 | const comments = ConnectionHandler.getConnection(
114 | issue,
115 | 'IssueDetailComments_comments', // See IssueDetailsComments @connection
116 | )
117 | if (comments == null) return
118 | // Insert the edge at the end of the list
119 | ConnectionHandler.insertEdgeAfter(
120 | comments,
121 | store.getRootField('addComment').getLinkedRecord('commentEdge'),
122 | null, // we can specify a cursor value here to insert the new edge after that cursor
123 | )
124 | },
125 | })
126 | // Reset the comment text
127 | setCommentText('')
128 | },
129 | [addComment, commentText, issueId],
130 | )
131 |
132 | const onToggleOpen = useCallback(
133 | (e: React.MouseEvent) => {
134 | e.preventDefault()
135 |
136 | // Switch mutation based on the current open/close status
137 | const config = {
138 | variables: {
139 | input: {
140 | issueId,
141 | },
142 | },
143 | }
144 | if (data.closed) {
145 | reopenIssue(config)
146 | } else {
147 | closeIssue(config)
148 | }
149 | },
150 | [closeIssue, data.closed, issueId, reopenIssue],
151 | )
152 |
153 | return (
154 |
176 | )
177 | }
178 |
--------------------------------------------------------------------------------
/issue-tracker/src/IssueDetailComments.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useTransition,
3 | SuspenseList,
4 | useCallback,
5 | Suspense,
6 | } from 'react'
7 | import { usePaginationFragment } from 'react-relay/hooks'
8 | import graphql from 'babel-plugin-relay/macro'
9 |
10 | import SuspenseImage from './SuspenseImage'
11 |
12 | import { IssueDetailComments_issue$key } from './__generated__/IssueDetailComments_issue.graphql'
13 | import ReactMarkdown from 'react-markdown'
14 |
15 | interface Props {
16 | issue: IssueDetailComments_issue$key
17 | }
18 |
19 | const SUSPENSE_CONFIG = { timeoutMs: 2000 }
20 |
21 | /**
22 | * Renders a list of comments for a given issue.
23 | */
24 | export default function IssueDetailComments(props: Props) {
25 | // Given a reference to an issue in props.issue, defines *what*
26 | // data the component needs about that repository. In this case we fetch
27 | // the list of comments starting at a given cursor (initially null to start
28 | // at the beginning of the issues list). See the usePaginationFragment()
29 | // docs: https://relay.dev/docs/en/experimental/api-reference#usepaginationfragment
30 | // for more details about how to use this hook to paginate over lists.
31 | const { data, hasNext, loadNext, isLoadingNext } = usePaginationFragment(
32 | graphql`
33 | fragment IssueDetailComments_issue on Issue
34 | @argumentDefinitions(
35 | cursor: { type: "String" }
36 | count: { type: "Int", defaultValue: 10 }
37 | )
38 | @refetchable(queryName: "IssueDetailCommentsQuery") {
39 | comments(after: $cursor, first: $count)
40 | @connection(key: "IssueDetailComments_comments") {
41 | edges {
42 | __id
43 | node {
44 | id
45 | author {
46 | login
47 | avatarUrl
48 | }
49 | body
50 | }
51 | }
52 | }
53 | }
54 | `,
55 | props.issue,
56 | )
57 | // Individual comments may suspend while any images are loading (for the
58 | // author avatar or content within the comment body). Using `useTransition()`
59 | // allows us to continue showing existing comments while the next page of
60 | // results is still loading in the background.
61 | const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG)
62 |
63 | // Callback to paginate the issues list
64 | const loadMore = useCallback(() => {
65 | // Don't fetch again if we're already loading the next page
66 | if (isLoadingNext) {
67 | return
68 | }
69 | startTransition(() => {
70 | loadNext(10)
71 | })
72 | }, [isLoadingNext, loadNext, startTransition])
73 |
74 | const comments = data?.comments.edges
75 | if (comments == null || comments.length === 0) {
76 | return No comments
77 | }
78 |
79 | // Per above, individual comments may suspend while images load. Using
80 | // allows us to render comments as they are ready, while avoiding showing them out of
81 | // order, as could happen if images for a later comment resolved before images for
82 | // an earlier comment.
83 | return (
84 | <>
85 |
86 | {comments.map(edge => {
87 | if (edge == null || edge.node == null) return null
88 | const { node: comment } = edge
89 | return (
90 | // Wrap each comment in a separate suspense fallback to allow them to commit
91 | // individually; SuspenseList ensures they'll reveal in-order.
92 |
93 |
94 |
99 |
100 | {comment.author?.login}
101 |
102 |
103 |
107 |
108 |
109 |
110 | )
111 | })}
112 |
113 | {hasNext ? (
114 |
120 | {isPending || isLoadingNext ? 'Loading...' : 'Load More'}
121 |
122 | ) : null}
123 | >
124 | )
125 | }
126 |
--------------------------------------------------------------------------------
/issue-tracker/src/IssueDetailRoot.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { usePreloadedQuery } from 'react-relay/hooks'
3 | import ReactMarkdown from 'react-markdown'
4 | import graphql from 'babel-plugin-relay/macro'
5 |
6 | import { IssueDetailRootQuery } from './__generated__/IssueDetailRootQuery.graphql'
7 | import { PreloadedQuery } from 'react-relay/lib/relay-experimental/EntryPointTypes'
8 | import SuspenseImage from './SuspenseImage'
9 | import IssueDetailComments from './IssueDetailComments'
10 | import IssueActions from './IssueActions'
11 |
12 | interface Props {
13 | prepared: {
14 | issueDetailQuery: PreloadedQuery
15 | }
16 | }
17 |
18 | /**
19 | * The root component for the issue detail route.
20 | */
21 | export default function IssueDetailRoot(props: Props) {
22 | // Defines *what* data the component needs via a query. The responsibility of
23 | // actually fetching this data belongs to the route definition: it calls
24 | // preloadQuery() with the query and variables, and the result is passed
25 | // on props.prepared.issueDetailQuery - see src/routes.js
26 | const { node: issue } = usePreloadedQuery(
27 | graphql`
28 | query IssueDetailRootQuery($id: ID!) {
29 | node(id: $id) {
30 | ... on Issue {
31 | title
32 | number
33 | author {
34 | login
35 | avatarUrl
36 | }
37 | body
38 | closed
39 | url
40 | ...IssueDetailComments_issue
41 | ...IssueActions_issue
42 | }
43 | }
44 | }
45 | `,
46 | props.prepared.issueDetailQuery,
47 | )
48 | if (issue == null) {
49 | return 'Issue not found'
50 | }
51 |
52 | return (
53 |
54 |
64 |
65 |
70 |
{issue.author?.login}
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/issue-tracker/src/IssueListItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useFragment } from 'react-relay/hooks'
3 | import graphql from 'babel-plugin-relay/macro'
4 |
5 | import Link from './routing/Link'
6 |
7 | import { IssueListItem_issue$key } from './__generated__/IssueListItem_issue.graphql'
8 |
9 | interface Props {
10 | issue: IssueListItem_issue$key
11 | }
12 |
13 | /**
14 | * Renders a single item (issue) in the issues list.
15 | */
16 | export default function IssueListItem(props: Props) {
17 | // Given a reference to a specific issue - props.issue - define *what*
18 | // data the component needs about the issue in order to render it.
19 | // Note that Relay will only give the component access to the exact fields
20 | // defined in the fragment. Relay will also subscribe for updates to just
21 | // those fields, updating the component if the values change due to a mutation
22 | // or the data being refetched.
23 | const issue = useFragment(
24 | graphql`
25 | fragment IssueListItem_issue on Issue {
26 | id
27 | title
28 | }
29 | `,
30 | props.issue,
31 | )
32 |
33 | // Describe how to render the data:
34 | return {issue.title}
35 | }
36 |
--------------------------------------------------------------------------------
/issue-tracker/src/Issues.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 | import { usePaginationFragment } from 'react-relay/hooks'
3 | import graphql from 'babel-plugin-relay/macro'
4 |
5 | import IssueListItem from './IssueListItem'
6 |
7 | import { Issues_repository$key } from './__generated__/Issues_repository.graphql'
8 |
9 | interface Props {
10 | repository: Issues_repository$key
11 | }
12 |
13 | /**
14 | * Renders a list of issues for a given repository.
15 | */
16 | export default function Issues(props: Props) {
17 | // Given a reference to a repository in props.repository, defines *what*
18 | // data the component needs about that repository. In this case we fetch
19 | // the list of issues starting at a given cursor (initially null to start
20 | // at the beginning of the issues list). See the usePaginationFragment()
21 | // docs: https://relay.dev/docs/en/experimental/api-reference#usepaginationfragment
22 | // for more details about how to use this hook to paginate over lists.
23 | const { data, loadNext, isLoadingNext } = usePaginationFragment(
24 | graphql`
25 | fragment Issues_repository on Repository
26 | @argumentDefinitions(
27 | cursor: { type: "String" }
28 | count: { type: "Int", defaultValue: 10 }
29 | states: { type: "[IssueState!]", defaultValue: ["OPEN"] }
30 | )
31 | @refetchable(queryName: "IssuesPaginationQuery") {
32 | issues(after: $cursor, first: $count, states: $states)
33 | @connection(key: "Issues_issues") {
34 | edges {
35 | __id
36 | node {
37 | # Compose the data dependencies of child components
38 | # by spreading their fragments:
39 | ...IssueListItem_issue
40 | }
41 | }
42 | }
43 | }
44 | `,
45 | props.repository,
46 | )
47 |
48 | // Callback to paginate the issues list
49 | const loadMore = useCallback(() => {
50 | // Don't fetch again if we're already loading the next page
51 | if (isLoadingNext) return
52 | loadNext(10)
53 | }, [isLoadingNext, loadNext])
54 |
55 | return (
56 |
57 | {data?.issues.edges?.map(edge => {
58 | if (edge == null || edge.node == null) return null
59 |
60 | return (
61 |
62 | {/* Note how we also spread IssuesListItem's fragment above */}
63 |
64 |
65 | )
66 | })}
67 |
73 | Load More
74 |
75 |
76 | )
77 | }
78 |
--------------------------------------------------------------------------------
/issue-tracker/src/JSResource.ts:
--------------------------------------------------------------------------------
1 | declare const UniqueId: string
2 | type Loader = () => Promise
3 |
4 | export type Result = any
5 |
6 | /**
7 | * A cache of resources to avoid loading the same module twice. This is important
8 | * because Webpack dynamic imports only expose an asynchronous API for loading
9 | * modules, so to be able to access already-loaded modules synchronously we
10 | * must have stored the previous result somewhere.
11 | */
12 | const resourceMap = new Map()
13 |
14 | /**
15 | * A generic resource: given some method to asynchronously load a value - the loader()
16 | * argument - it allows accessing the state of the resource.
17 | */
18 | export class Resource {
19 | private error: Error | null
20 | private loader: Loader
21 | private promise: Promise | null
22 | private result: Result | null
23 |
24 | constructor(loader: Loader) {
25 | this.error = null
26 | this.loader = loader
27 | this.promise = null
28 | this.result = null
29 | }
30 |
31 | /**
32 | * Loads the resource if necessary.
33 | */
34 | load() {
35 | let promise = this.promise
36 | if (promise == null) {
37 | promise = this.loader()
38 | .then(result => {
39 | if (result.default) {
40 | result = result.default
41 | }
42 | this.result = result
43 | return result
44 | })
45 | .catch(error => {
46 | this.error = error
47 | throw error
48 | })
49 | this.promise = promise
50 | }
51 | return promise
52 | }
53 |
54 | /**
55 | * Returns the result, if available. This can be useful to check if the value
56 | * is resolved yet.
57 | */
58 | get() {
59 | if (this.result != null) {
60 | return this.result
61 | }
62 | }
63 |
64 | /**
65 | * This is the key method for integrating with React Suspense. Read will:
66 | * - "Suspend" if the resource is still pending (currently implemented as
67 | * throwing a Promise, though this is subject to change in future
68 | * versions of React)
69 | * - Throw an error if the resource failed to load.
70 | * - Return the data of the resource if available.
71 | */
72 | read() {
73 | if (this.result != null) {
74 | return this.result
75 | } else if (this.error != null) {
76 | throw this.error
77 | } else {
78 | throw this.promise
79 | }
80 | }
81 | }
82 |
83 | /**
84 | * A helper method to create a resource, intended for dynamically loading code.
85 | *
86 | * Example:
87 | * ```
88 | * // Before rendering, ie in an event handler:
89 | * const resource = JSResource('Foo', () => import('./Foo.js));
90 | * resource.load();
91 | *
92 | * // in a React component:
93 | * const Foo = resource.read();
94 | * return ;
95 | * ```
96 | *
97 | * @param {*} moduleId A globally unique identifier for the resource used for caching
98 | * @param {*} loader A method to load the resource's data if necessary
99 | */
100 | export default function JSResource(moduleId: typeof UniqueId, loader: Loader) {
101 | let resource = resourceMap.get(moduleId)
102 | if (resource == null) {
103 | resource = new Resource(loader)
104 | resourceMap.set(moduleId, resource)
105 | }
106 | return resource
107 | }
108 |
--------------------------------------------------------------------------------
/issue-tracker/src/RelayEnvironment.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Environment,
3 | Network,
4 | RecordSource,
5 | Store,
6 | RequestParameters,
7 | Variables,
8 | CacheConfig,
9 | } from 'relay-runtime'
10 |
11 | /**
12 | * Relay requires developers to configure a "fetch" function that tells Relay how to load
13 | * the results of GraphQL queries from your server (or other data source). See more at
14 | * https://relay.dev/docs/en/quick-start-guide#relay-environment.
15 | */
16 | async function fetchRelay(
17 | params: RequestParameters,
18 | variables: Variables,
19 | _cacheConfig: CacheConfig,
20 | ) {
21 | // Check that the auth token is configured
22 | const REACT_APP_GITHUB_AUTH_TOKEN = process.env.REACT_APP_GITHUB_AUTH_TOKEN
23 | if (REACT_APP_GITHUB_AUTH_TOKEN == null) {
24 | throw new Error(
25 | 'This app requires a GitHub authentication token to be configured. See readme.md for setup details.',
26 | )
27 | }
28 |
29 | // Fetch data from GitHub's GraphQL API:
30 | const response = await fetch('https://api.github.com/graphql', {
31 | method: 'POST',
32 | headers: {
33 | Authorization: `bearer ${REACT_APP_GITHUB_AUTH_TOKEN}`,
34 | 'Content-Type': 'application/json',
35 | },
36 | body: JSON.stringify({
37 | query: params.text,
38 | variables,
39 | }),
40 | })
41 |
42 | // Get the response as JSON
43 | const json = await response.json()
44 |
45 | // GraphQL returns exceptions (for example, a missing required variable) in the "errors"
46 | // property of the response. If any exceptions occurred when processing the request,
47 | // throw an error to indicate to the developer what went wrong.
48 | if (Array.isArray(json.errors)) {
49 | console.log(json.errors)
50 | throw new Error(`
51 | Error fetching GraphQL query '${
52 | params.name
53 | }' with variables '${JSON.stringify(variables)}': ${JSON.stringify(
54 | json.errors,
55 | )}
56 | `)
57 | }
58 |
59 | // Otherwise, return the full payload.
60 | return json
61 | }
62 |
63 | export default new Environment({
64 | network: Network.create(fetchRelay),
65 | store: new Store(new RecordSource(), {
66 | // This property tells Relay to not immediately clear its cache when the user
67 | // navigates around the app. Relay will hold onto the specified number of
68 | // query results, allowing the user to return to recently visited pages
69 | // and reusing cached data if its available/fresh.
70 |
71 | // TODO: RelayModernStore needs to be updated on @types/relay-runtime in order to allow Scheduler config such as gcReleaseBufferSize
72 | // @ts-ignore
73 | gcReleaseBufferSize: 10,
74 | }),
75 | })
76 |
--------------------------------------------------------------------------------
/issue-tracker/src/Root.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react'
2 | import { usePreloadedQuery } from 'react-relay/hooks'
3 | import graphql from 'babel-plugin-relay/macro'
4 |
5 | import { RootQuery } from './__generated__/RootQuery.graphql'
6 | import { PreloadedQuery } from 'react-relay/lib/relay-experimental/EntryPointTypes'
7 |
8 | interface Props {
9 | prepared: {
10 | rootQuery: PreloadedQuery
11 | }
12 | children: React.ReactChildren
13 | }
14 |
15 | const Root = (props: Props) => {
16 | // Defines *what* data the component needs via a query. The responsibility of
17 | // actually fetching this data belongs to the route definition: it calls
18 | // preloadQuery() with the query and variables, and the result is passed
19 | // on props.prepared.issuesQuery - see src/routes.js
20 | const data = usePreloadedQuery(
21 | graphql`
22 | query RootQuery($owner: String!, $name: String!) {
23 | repository(owner: $owner, name: $name) {
24 | owner {
25 | login
26 | }
27 | name
28 | }
29 | }
30 | `,
31 | props.prepared.rootQuery,
32 | )
33 | const { repository } = data
34 |
35 | return (
36 |
37 |
38 | {repository!.owner.login}/{repository!.name}: Issues
39 |
40 |
41 | {/* Wrap the child in a Suspense boundary to allow rendering the
42 | layout even if the main content isn't ready */}
43 | {props.children}
44 |
45 |
46 | )
47 | }
48 |
49 | export default Root
50 |
--------------------------------------------------------------------------------
/issue-tracker/src/SuspenseImage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import JSResource from './JSResource'
3 |
4 | type Props = React.DetailedHTMLProps<
5 | React.ImgHTMLAttributes,
6 | HTMLImageElement
7 | >
8 |
9 | export default function SuspenseImage(props: Props) {
10 | const { src, alt } = props
11 |
12 | if (src != null) {
13 | // JSResource is meant for loading resources, but the implementation is
14 | // just cached loading of promises. So we reuse that here as a quick
15 | // way to suspend while images are loading, with caching in case
16 | // we encouter the same image twice (in that case, we'll create
17 | // new loader *functions*, but JSResource will return a cached
18 | // value and only load the image once.
19 | const resource = JSResource(src, () => {
20 | return new Promise(resolve => {
21 | const img = new Image()
22 | img.onload = () => {
23 | resolve(src)
24 | }
25 | img.onerror = error => {
26 | console.error(error)
27 | resolve(src)
28 | }
29 | img.src = src
30 | })
31 | })
32 | resource.load() // TODO: JSResource::read() should call load() if necessary
33 | resource.read() // suspends while the image is pending
34 | }
35 |
36 | return
37 | }
38 |
--------------------------------------------------------------------------------
/issue-tracker/src/__generated__/HomeRootIssuesQuery.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ConcreteRequest } from 'relay-runtime'
4 | import { FragmentRefs } from 'relay-runtime'
5 | export type HomeRootIssuesQueryVariables = {
6 | owner: string
7 | name: string
8 | }
9 | export type HomeRootIssuesQueryResponse = {
10 | readonly repository: {
11 | readonly ' $fragmentRefs': FragmentRefs<'Issues_repository'>
12 | } | null
13 | }
14 | export type HomeRootIssuesQuery = {
15 | readonly response: HomeRootIssuesQueryResponse
16 | readonly variables: HomeRootIssuesQueryVariables
17 | }
18 |
19 | /*
20 | query HomeRootIssuesQuery(
21 | $owner: String!
22 | $name: String!
23 | ) {
24 | repository(owner: $owner, name: $name) {
25 | ...Issues_repository
26 | id
27 | }
28 | }
29 |
30 | fragment IssueListItem_issue on Issue {
31 | id
32 | title
33 | }
34 |
35 | fragment Issues_repository on Repository {
36 | issues(first: 10, states: [OPEN]) {
37 | edges {
38 | node {
39 | ...IssueListItem_issue
40 | id
41 | __typename
42 | }
43 | cursor
44 | }
45 | pageInfo {
46 | endCursor
47 | hasNextPage
48 | }
49 | }
50 | id
51 | }
52 | */
53 |
54 | const node: ConcreteRequest = (function() {
55 | var v0 = [
56 | {
57 | kind: 'LocalArgument',
58 | name: 'owner',
59 | type: 'String!',
60 | defaultValue: null,
61 | },
62 | {
63 | kind: 'LocalArgument',
64 | name: 'name',
65 | type: 'String!',
66 | defaultValue: null,
67 | },
68 | ],
69 | v1 = [
70 | {
71 | kind: 'Variable',
72 | name: 'name',
73 | variableName: 'name',
74 | },
75 | {
76 | kind: 'Variable',
77 | name: 'owner',
78 | variableName: 'owner',
79 | },
80 | ],
81 | v2 = [
82 | {
83 | kind: 'Literal',
84 | name: 'first',
85 | value: 10,
86 | },
87 | {
88 | kind: 'Literal',
89 | name: 'states',
90 | value: ['OPEN'],
91 | },
92 | ],
93 | v3 = {
94 | kind: 'ScalarField',
95 | alias: null,
96 | name: 'id',
97 | args: null,
98 | storageKey: null,
99 | }
100 | return {
101 | kind: 'Request',
102 | fragment: {
103 | kind: 'Fragment',
104 | name: 'HomeRootIssuesQuery',
105 | type: 'Query',
106 | metadata: null,
107 | argumentDefinitions: v0 /*: any*/,
108 | selections: [
109 | {
110 | kind: 'LinkedField',
111 | alias: null,
112 | name: 'repository',
113 | storageKey: null,
114 | args: v1 /*: any*/,
115 | concreteType: 'Repository',
116 | plural: false,
117 | selections: [
118 | {
119 | kind: 'FragmentSpread',
120 | name: 'Issues_repository',
121 | args: null,
122 | },
123 | ],
124 | },
125 | ],
126 | },
127 | operation: {
128 | kind: 'Operation',
129 | name: 'HomeRootIssuesQuery',
130 | argumentDefinitions: v0 /*: any*/,
131 | selections: [
132 | {
133 | kind: 'LinkedField',
134 | alias: null,
135 | name: 'repository',
136 | storageKey: null,
137 | args: v1 /*: any*/,
138 | concreteType: 'Repository',
139 | plural: false,
140 | selections: [
141 | {
142 | kind: 'LinkedField',
143 | alias: null,
144 | name: 'issues',
145 | storageKey: 'issues(first:10,states:["OPEN"])',
146 | args: v2 /*: any*/,
147 | concreteType: 'IssueConnection',
148 | plural: false,
149 | selections: [
150 | {
151 | kind: 'LinkedField',
152 | alias: null,
153 | name: 'edges',
154 | storageKey: null,
155 | args: null,
156 | concreteType: 'IssueEdge',
157 | plural: true,
158 | selections: [
159 | {
160 | kind: 'LinkedField',
161 | alias: null,
162 | name: 'node',
163 | storageKey: null,
164 | args: null,
165 | concreteType: 'Issue',
166 | plural: false,
167 | selections: [
168 | v3 /*: any*/,
169 | {
170 | kind: 'ScalarField',
171 | alias: null,
172 | name: 'title',
173 | args: null,
174 | storageKey: null,
175 | },
176 | {
177 | kind: 'ScalarField',
178 | alias: null,
179 | name: '__typename',
180 | args: null,
181 | storageKey: null,
182 | },
183 | ],
184 | },
185 | {
186 | kind: 'ScalarField',
187 | alias: null,
188 | name: 'cursor',
189 | args: null,
190 | storageKey: null,
191 | },
192 | {
193 | kind: 'ClientExtension',
194 | selections: [
195 | {
196 | kind: 'ScalarField',
197 | alias: null,
198 | name: '__id',
199 | args: null,
200 | storageKey: null,
201 | },
202 | ],
203 | },
204 | ],
205 | },
206 | {
207 | kind: 'LinkedField',
208 | alias: null,
209 | name: 'pageInfo',
210 | storageKey: null,
211 | args: null,
212 | concreteType: 'PageInfo',
213 | plural: false,
214 | selections: [
215 | {
216 | kind: 'ScalarField',
217 | alias: null,
218 | name: 'endCursor',
219 | args: null,
220 | storageKey: null,
221 | },
222 | {
223 | kind: 'ScalarField',
224 | alias: null,
225 | name: 'hasNextPage',
226 | args: null,
227 | storageKey: null,
228 | },
229 | ],
230 | },
231 | ],
232 | },
233 | {
234 | kind: 'LinkedHandle',
235 | alias: null,
236 | name: 'issues',
237 | args: v2 /*: any*/,
238 | handle: 'connection',
239 | key: 'Issues_issues',
240 | filters: ['states'],
241 | },
242 | v3 /*: any*/,
243 | ],
244 | },
245 | ],
246 | },
247 | params: {
248 | operationKind: 'query',
249 | name: 'HomeRootIssuesQuery',
250 | id: null,
251 | text:
252 | 'query HomeRootIssuesQuery(\n $owner: String!\n $name: String!\n) {\n repository(owner: $owner, name: $name) {\n ...Issues_repository\n id\n }\n}\n\nfragment IssueListItem_issue on Issue {\n id\n title\n}\n\nfragment Issues_repository on Repository {\n issues(first: 10, states: [OPEN]) {\n edges {\n node {\n ...IssueListItem_issue\n id\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n}\n',
253 | metadata: {},
254 | },
255 | }
256 | })()
257 | ;(node as any).hash = '123ee85bfef2bb303a99a7320127372f'
258 | export default node
259 |
--------------------------------------------------------------------------------
/issue-tracker/src/__generated__/IssueActionsAddCommentMutation.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ConcreteRequest } from 'relay-runtime'
4 | export type AddCommentInput = {
5 | readonly subjectId: string
6 | readonly body: string
7 | readonly clientMutationId?: string | null
8 | }
9 | export type IssueActionsAddCommentMutationVariables = {
10 | input: AddCommentInput
11 | }
12 | export type IssueActionsAddCommentMutationResponse = {
13 | readonly addComment: {
14 | readonly subject: {
15 | readonly id: string
16 | } | null
17 | readonly commentEdge: {
18 | readonly __id: string
19 | readonly node: {
20 | readonly id: string
21 | readonly author: {
22 | readonly login: string
23 | readonly avatarUrl: unknown
24 | } | null
25 | readonly body: string
26 | } | null
27 | } | null
28 | } | null
29 | }
30 | export type IssueActionsAddCommentMutation = {
31 | readonly response: IssueActionsAddCommentMutationResponse
32 | readonly variables: IssueActionsAddCommentMutationVariables
33 | }
34 |
35 | /*
36 | mutation IssueActionsAddCommentMutation(
37 | $input: AddCommentInput!
38 | ) {
39 | addComment(input: $input) {
40 | subject {
41 | __typename
42 | id
43 | }
44 | commentEdge {
45 | node {
46 | id
47 | author {
48 | __typename
49 | login
50 | avatarUrl
51 | ... on Node {
52 | id
53 | }
54 | }
55 | body
56 | }
57 | }
58 | }
59 | }
60 | */
61 |
62 | const node: ConcreteRequest = (function() {
63 | var v0 = [
64 | {
65 | kind: 'LocalArgument',
66 | name: 'input',
67 | type: 'AddCommentInput!',
68 | defaultValue: null,
69 | },
70 | ],
71 | v1 = [
72 | {
73 | kind: 'Variable',
74 | name: 'input',
75 | variableName: 'input',
76 | },
77 | ],
78 | v2 = {
79 | kind: 'ScalarField',
80 | alias: null,
81 | name: 'id',
82 | args: null,
83 | storageKey: null,
84 | },
85 | v3 = {
86 | kind: 'ScalarField',
87 | alias: null,
88 | name: 'login',
89 | args: null,
90 | storageKey: null,
91 | },
92 | v4 = {
93 | kind: 'ScalarField',
94 | alias: null,
95 | name: 'avatarUrl',
96 | args: null,
97 | storageKey: null,
98 | },
99 | v5 = {
100 | kind: 'ScalarField',
101 | alias: null,
102 | name: 'body',
103 | args: null,
104 | storageKey: null,
105 | },
106 | v6 = {
107 | kind: 'ClientExtension',
108 | selections: [
109 | {
110 | kind: 'ScalarField',
111 | alias: null,
112 | name: '__id',
113 | args: null,
114 | storageKey: null,
115 | },
116 | ],
117 | },
118 | v7 = {
119 | kind: 'ScalarField',
120 | alias: null,
121 | name: '__typename',
122 | args: null,
123 | storageKey: null,
124 | }
125 | return {
126 | kind: 'Request',
127 | fragment: {
128 | kind: 'Fragment',
129 | name: 'IssueActionsAddCommentMutation',
130 | type: 'Mutation',
131 | metadata: null,
132 | argumentDefinitions: v0 /*: any*/,
133 | selections: [
134 | {
135 | kind: 'LinkedField',
136 | alias: null,
137 | name: 'addComment',
138 | storageKey: null,
139 | args: v1 /*: any*/,
140 | concreteType: 'AddCommentPayload',
141 | plural: false,
142 | selections: [
143 | {
144 | kind: 'LinkedField',
145 | alias: null,
146 | name: 'subject',
147 | storageKey: null,
148 | args: null,
149 | concreteType: null,
150 | plural: false,
151 | selections: [v2 /*: any*/],
152 | },
153 | {
154 | kind: 'LinkedField',
155 | alias: null,
156 | name: 'commentEdge',
157 | storageKey: null,
158 | args: null,
159 | concreteType: 'IssueCommentEdge',
160 | plural: false,
161 | selections: [
162 | {
163 | kind: 'LinkedField',
164 | alias: null,
165 | name: 'node',
166 | storageKey: null,
167 | args: null,
168 | concreteType: 'IssueComment',
169 | plural: false,
170 | selections: [
171 | v2 /*: any*/,
172 | {
173 | kind: 'LinkedField',
174 | alias: null,
175 | name: 'author',
176 | storageKey: null,
177 | args: null,
178 | concreteType: null,
179 | plural: false,
180 | selections: [v3 /*: any*/, v4 /*: any*/],
181 | },
182 | v5 /*: any*/,
183 | ],
184 | },
185 | v6 /*: any*/,
186 | ],
187 | },
188 | ],
189 | },
190 | ],
191 | },
192 | operation: {
193 | kind: 'Operation',
194 | name: 'IssueActionsAddCommentMutation',
195 | argumentDefinitions: v0 /*: any*/,
196 | selections: [
197 | {
198 | kind: 'LinkedField',
199 | alias: null,
200 | name: 'addComment',
201 | storageKey: null,
202 | args: v1 /*: any*/,
203 | concreteType: 'AddCommentPayload',
204 | plural: false,
205 | selections: [
206 | {
207 | kind: 'LinkedField',
208 | alias: null,
209 | name: 'subject',
210 | storageKey: null,
211 | args: null,
212 | concreteType: null,
213 | plural: false,
214 | selections: [v7 /*: any*/, v2 /*: any*/],
215 | },
216 | {
217 | kind: 'LinkedField',
218 | alias: null,
219 | name: 'commentEdge',
220 | storageKey: null,
221 | args: null,
222 | concreteType: 'IssueCommentEdge',
223 | plural: false,
224 | selections: [
225 | {
226 | kind: 'LinkedField',
227 | alias: null,
228 | name: 'node',
229 | storageKey: null,
230 | args: null,
231 | concreteType: 'IssueComment',
232 | plural: false,
233 | selections: [
234 | v2 /*: any*/,
235 | {
236 | kind: 'LinkedField',
237 | alias: null,
238 | name: 'author',
239 | storageKey: null,
240 | args: null,
241 | concreteType: null,
242 | plural: false,
243 | selections: [
244 | v7 /*: any*/,
245 | v3 /*: any*/,
246 | v4 /*: any*/,
247 | v2 /*: any*/,
248 | ],
249 | },
250 | v5 /*: any*/,
251 | ],
252 | },
253 | v6 /*: any*/,
254 | ],
255 | },
256 | ],
257 | },
258 | ],
259 | },
260 | params: {
261 | operationKind: 'mutation',
262 | name: 'IssueActionsAddCommentMutation',
263 | id: null,
264 | text:
265 | 'mutation IssueActionsAddCommentMutation(\n $input: AddCommentInput!\n) {\n addComment(input: $input) {\n subject {\n __typename\n id\n }\n commentEdge {\n node {\n id\n author {\n __typename\n login\n avatarUrl\n ... on Node {\n id\n }\n }\n body\n }\n }\n }\n}\n',
266 | metadata: {},
267 | },
268 | }
269 | })()
270 | ;(node as any).hash = 'f777dc7f1873d5cdab111f485ef8f404'
271 | export default node
272 |
--------------------------------------------------------------------------------
/issue-tracker/src/__generated__/IssueActionsCloseIssueMutation.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ConcreteRequest } from 'relay-runtime'
4 | export type CloseIssueInput = {
5 | readonly issueId: string
6 | readonly clientMutationId?: string | null
7 | }
8 | export type IssueActionsCloseIssueMutationVariables = {
9 | input: CloseIssueInput
10 | }
11 | export type IssueActionsCloseIssueMutationResponse = {
12 | readonly closeIssue: {
13 | readonly issue: {
14 | readonly closed: boolean
15 | } | null
16 | } | null
17 | }
18 | export type IssueActionsCloseIssueMutation = {
19 | readonly response: IssueActionsCloseIssueMutationResponse
20 | readonly variables: IssueActionsCloseIssueMutationVariables
21 | }
22 |
23 | /*
24 | mutation IssueActionsCloseIssueMutation(
25 | $input: CloseIssueInput!
26 | ) {
27 | closeIssue(input: $input) {
28 | issue {
29 | closed
30 | id
31 | }
32 | }
33 | }
34 | */
35 |
36 | const node: ConcreteRequest = (function() {
37 | var v0 = [
38 | {
39 | kind: 'LocalArgument',
40 | name: 'input',
41 | type: 'CloseIssueInput!',
42 | defaultValue: null,
43 | },
44 | ],
45 | v1 = [
46 | {
47 | kind: 'Variable',
48 | name: 'input',
49 | variableName: 'input',
50 | },
51 | ],
52 | v2 = {
53 | kind: 'ScalarField',
54 | alias: null,
55 | name: 'closed',
56 | args: null,
57 | storageKey: null,
58 | }
59 | return {
60 | kind: 'Request',
61 | fragment: {
62 | kind: 'Fragment',
63 | name: 'IssueActionsCloseIssueMutation',
64 | type: 'Mutation',
65 | metadata: null,
66 | argumentDefinitions: v0 /*: any*/,
67 | selections: [
68 | {
69 | kind: 'LinkedField',
70 | alias: null,
71 | name: 'closeIssue',
72 | storageKey: null,
73 | args: v1 /*: any*/,
74 | concreteType: 'CloseIssuePayload',
75 | plural: false,
76 | selections: [
77 | {
78 | kind: 'LinkedField',
79 | alias: null,
80 | name: 'issue',
81 | storageKey: null,
82 | args: null,
83 | concreteType: 'Issue',
84 | plural: false,
85 | selections: [v2 /*: any*/],
86 | },
87 | ],
88 | },
89 | ],
90 | },
91 | operation: {
92 | kind: 'Operation',
93 | name: 'IssueActionsCloseIssueMutation',
94 | argumentDefinitions: v0 /*: any*/,
95 | selections: [
96 | {
97 | kind: 'LinkedField',
98 | alias: null,
99 | name: 'closeIssue',
100 | storageKey: null,
101 | args: v1 /*: any*/,
102 | concreteType: 'CloseIssuePayload',
103 | plural: false,
104 | selections: [
105 | {
106 | kind: 'LinkedField',
107 | alias: null,
108 | name: 'issue',
109 | storageKey: null,
110 | args: null,
111 | concreteType: 'Issue',
112 | plural: false,
113 | selections: [
114 | v2 /*: any*/,
115 | {
116 | kind: 'ScalarField',
117 | alias: null,
118 | name: 'id',
119 | args: null,
120 | storageKey: null,
121 | },
122 | ],
123 | },
124 | ],
125 | },
126 | ],
127 | },
128 | params: {
129 | operationKind: 'mutation',
130 | name: 'IssueActionsCloseIssueMutation',
131 | id: null,
132 | text:
133 | 'mutation IssueActionsCloseIssueMutation(\n $input: CloseIssueInput!\n) {\n closeIssue(input: $input) {\n issue {\n closed\n id\n }\n }\n}\n',
134 | metadata: {},
135 | },
136 | }
137 | })()
138 | ;(node as any).hash = '51e445c25b3f4b3c48c3d6ca6c5f2f5a'
139 | export default node
140 |
--------------------------------------------------------------------------------
/issue-tracker/src/__generated__/IssueActionsReopenIssueMutation.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ConcreteRequest } from 'relay-runtime'
4 | export type ReopenIssueInput = {
5 | readonly issueId: string
6 | readonly clientMutationId?: string | null
7 | }
8 | export type IssueActionsReopenIssueMutationVariables = {
9 | input: ReopenIssueInput
10 | }
11 | export type IssueActionsReopenIssueMutationResponse = {
12 | readonly reopenIssue: {
13 | readonly issue: {
14 | readonly closed: boolean
15 | } | null
16 | } | null
17 | }
18 | export type IssueActionsReopenIssueMutation = {
19 | readonly response: IssueActionsReopenIssueMutationResponse
20 | readonly variables: IssueActionsReopenIssueMutationVariables
21 | }
22 |
23 | /*
24 | mutation IssueActionsReopenIssueMutation(
25 | $input: ReopenIssueInput!
26 | ) {
27 | reopenIssue(input: $input) {
28 | issue {
29 | closed
30 | id
31 | }
32 | }
33 | }
34 | */
35 |
36 | const node: ConcreteRequest = (function() {
37 | var v0 = [
38 | {
39 | kind: 'LocalArgument',
40 | name: 'input',
41 | type: 'ReopenIssueInput!',
42 | defaultValue: null,
43 | },
44 | ],
45 | v1 = [
46 | {
47 | kind: 'Variable',
48 | name: 'input',
49 | variableName: 'input',
50 | },
51 | ],
52 | v2 = {
53 | kind: 'ScalarField',
54 | alias: null,
55 | name: 'closed',
56 | args: null,
57 | storageKey: null,
58 | }
59 | return {
60 | kind: 'Request',
61 | fragment: {
62 | kind: 'Fragment',
63 | name: 'IssueActionsReopenIssueMutation',
64 | type: 'Mutation',
65 | metadata: null,
66 | argumentDefinitions: v0 /*: any*/,
67 | selections: [
68 | {
69 | kind: 'LinkedField',
70 | alias: null,
71 | name: 'reopenIssue',
72 | storageKey: null,
73 | args: v1 /*: any*/,
74 | concreteType: 'ReopenIssuePayload',
75 | plural: false,
76 | selections: [
77 | {
78 | kind: 'LinkedField',
79 | alias: null,
80 | name: 'issue',
81 | storageKey: null,
82 | args: null,
83 | concreteType: 'Issue',
84 | plural: false,
85 | selections: [v2 /*: any*/],
86 | },
87 | ],
88 | },
89 | ],
90 | },
91 | operation: {
92 | kind: 'Operation',
93 | name: 'IssueActionsReopenIssueMutation',
94 | argumentDefinitions: v0 /*: any*/,
95 | selections: [
96 | {
97 | kind: 'LinkedField',
98 | alias: null,
99 | name: 'reopenIssue',
100 | storageKey: null,
101 | args: v1 /*: any*/,
102 | concreteType: 'ReopenIssuePayload',
103 | plural: false,
104 | selections: [
105 | {
106 | kind: 'LinkedField',
107 | alias: null,
108 | name: 'issue',
109 | storageKey: null,
110 | args: null,
111 | concreteType: 'Issue',
112 | plural: false,
113 | selections: [
114 | v2 /*: any*/,
115 | {
116 | kind: 'ScalarField',
117 | alias: null,
118 | name: 'id',
119 | args: null,
120 | storageKey: null,
121 | },
122 | ],
123 | },
124 | ],
125 | },
126 | ],
127 | },
128 | params: {
129 | operationKind: 'mutation',
130 | name: 'IssueActionsReopenIssueMutation',
131 | id: null,
132 | text:
133 | 'mutation IssueActionsReopenIssueMutation(\n $input: ReopenIssueInput!\n) {\n reopenIssue(input: $input) {\n issue {\n closed\n id\n }\n }\n}\n',
134 | metadata: {},
135 | },
136 | }
137 | })()
138 | ;(node as any).hash = '6dbb9d9a672b1703eb0983667e9638db'
139 | export default node
140 |
--------------------------------------------------------------------------------
/issue-tracker/src/__generated__/IssueActions_issue.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ReaderFragment } from 'relay-runtime'
4 | import { FragmentRefs } from 'relay-runtime'
5 | export type IssueActions_issue = {
6 | readonly id: string
7 | readonly closed: boolean
8 | readonly ' $refType': 'IssueActions_issue'
9 | }
10 | export type IssueActions_issue$data = IssueActions_issue
11 | export type IssueActions_issue$key = {
12 | readonly ' $data'?: IssueActions_issue$data
13 | readonly ' $fragmentRefs': FragmentRefs<'IssueActions_issue'>
14 | }
15 |
16 | const node: ReaderFragment = {
17 | kind: 'Fragment',
18 | name: 'IssueActions_issue',
19 | type: 'Issue',
20 | metadata: null,
21 | argumentDefinitions: [],
22 | selections: [
23 | {
24 | kind: 'ScalarField',
25 | alias: null,
26 | name: 'id',
27 | args: null,
28 | storageKey: null,
29 | },
30 | {
31 | kind: 'ScalarField',
32 | alias: null,
33 | name: 'closed',
34 | args: null,
35 | storageKey: null,
36 | },
37 | ],
38 | }
39 | ;(node as any).hash = '12c79d27df99eb9656621cead33c9d08'
40 | export default node
41 |
--------------------------------------------------------------------------------
/issue-tracker/src/__generated__/IssueDetailComments_issue.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ReaderFragment } from 'relay-runtime'
4 | import { FragmentRefs } from 'relay-runtime'
5 | export type IssueDetailComments_issue = {
6 | readonly comments: {
7 | readonly edges: ReadonlyArray<{
8 | readonly __id: string
9 | readonly node: {
10 | readonly id: string
11 | readonly author: {
12 | readonly login: string
13 | readonly avatarUrl: unknown
14 | } | null
15 | readonly body: string
16 | } | null
17 | } | null> | null
18 | }
19 | readonly id: string | null
20 | readonly ' $refType': 'IssueDetailComments_issue'
21 | }
22 | export type IssueDetailComments_issue$data = IssueDetailComments_issue
23 | export type IssueDetailComments_issue$key = {
24 | readonly ' $data'?: IssueDetailComments_issue$data
25 | readonly ' $fragmentRefs': FragmentRefs<'IssueDetailComments_issue'>
26 | }
27 |
28 | const node: ReaderFragment = (function() {
29 | var v0 = ['comments'],
30 | v1 = {
31 | kind: 'ScalarField',
32 | alias: null,
33 | name: 'id',
34 | args: null,
35 | storageKey: null,
36 | }
37 | return {
38 | kind: 'Fragment',
39 | name: 'IssueDetailComments_issue',
40 | type: 'Issue',
41 | metadata: {
42 | connection: [
43 | {
44 | count: 'count',
45 | cursor: 'cursor',
46 | direction: 'forward',
47 | path: v0 /*: any*/,
48 | },
49 | ],
50 | refetch: {
51 | connection: {
52 | forward: {
53 | count: 'count',
54 | cursor: 'cursor',
55 | },
56 | backward: null,
57 | path: v0 /*: any*/,
58 | },
59 | operation: require('./IssueDetailCommentsQuery.graphql.ts'),
60 | fragmentPathInResult: ['node'],
61 | },
62 | },
63 | argumentDefinitions: [
64 | {
65 | kind: 'LocalArgument',
66 | name: 'cursor',
67 | type: 'String',
68 | defaultValue: null,
69 | },
70 | {
71 | kind: 'LocalArgument',
72 | name: 'count',
73 | type: 'Int',
74 | defaultValue: 10,
75 | },
76 | ],
77 | selections: [
78 | {
79 | kind: 'LinkedField',
80 | alias: 'comments',
81 | name: '__IssueDetailComments_comments_connection',
82 | storageKey: null,
83 | args: null,
84 | concreteType: 'IssueCommentConnection',
85 | plural: false,
86 | selections: [
87 | {
88 | kind: 'LinkedField',
89 | alias: null,
90 | name: 'edges',
91 | storageKey: null,
92 | args: null,
93 | concreteType: 'IssueCommentEdge',
94 | plural: true,
95 | selections: [
96 | {
97 | kind: 'LinkedField',
98 | alias: null,
99 | name: 'node',
100 | storageKey: null,
101 | args: null,
102 | concreteType: 'IssueComment',
103 | plural: false,
104 | selections: [
105 | v1 /*: any*/,
106 | {
107 | kind: 'LinkedField',
108 | alias: null,
109 | name: 'author',
110 | storageKey: null,
111 | args: null,
112 | concreteType: null,
113 | plural: false,
114 | selections: [
115 | {
116 | kind: 'ScalarField',
117 | alias: null,
118 | name: 'login',
119 | args: null,
120 | storageKey: null,
121 | },
122 | {
123 | kind: 'ScalarField',
124 | alias: null,
125 | name: 'avatarUrl',
126 | args: null,
127 | storageKey: null,
128 | },
129 | ],
130 | },
131 | {
132 | kind: 'ScalarField',
133 | alias: null,
134 | name: 'body',
135 | args: null,
136 | storageKey: null,
137 | },
138 | {
139 | kind: 'ScalarField',
140 | alias: null,
141 | name: '__typename',
142 | args: null,
143 | storageKey: null,
144 | },
145 | ],
146 | },
147 | {
148 | kind: 'ScalarField',
149 | alias: null,
150 | name: 'cursor',
151 | args: null,
152 | storageKey: null,
153 | },
154 | {
155 | kind: 'ClientExtension',
156 | selections: [
157 | {
158 | kind: 'ScalarField',
159 | alias: null,
160 | name: '__id',
161 | args: null,
162 | storageKey: null,
163 | },
164 | ],
165 | },
166 | ],
167 | },
168 | {
169 | kind: 'LinkedField',
170 | alias: null,
171 | name: 'pageInfo',
172 | storageKey: null,
173 | args: null,
174 | concreteType: 'PageInfo',
175 | plural: false,
176 | selections: [
177 | {
178 | kind: 'ScalarField',
179 | alias: null,
180 | name: 'endCursor',
181 | args: null,
182 | storageKey: null,
183 | },
184 | {
185 | kind: 'ScalarField',
186 | alias: null,
187 | name: 'hasNextPage',
188 | args: null,
189 | storageKey: null,
190 | },
191 | ],
192 | },
193 | ],
194 | },
195 | v1 /*: any*/,
196 | ],
197 | }
198 | })()
199 | ;(node as any).hash = '674952f209c2653f27a5fad5539df511'
200 | export default node
201 |
--------------------------------------------------------------------------------
/issue-tracker/src/__generated__/IssueListItem_issue.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ReaderFragment } from 'relay-runtime'
4 | import { FragmentRefs } from 'relay-runtime'
5 | export type IssueListItem_issue = {
6 | readonly id: string
7 | readonly title: string
8 | readonly ' $refType': 'IssueListItem_issue'
9 | }
10 | export type IssueListItem_issue$data = IssueListItem_issue
11 | export type IssueListItem_issue$key = {
12 | readonly ' $data'?: IssueListItem_issue$data
13 | readonly ' $fragmentRefs': FragmentRefs<'IssueListItem_issue'>
14 | }
15 |
16 | const node: ReaderFragment = {
17 | kind: 'Fragment',
18 | name: 'IssueListItem_issue',
19 | type: 'Issue',
20 | metadata: null,
21 | argumentDefinitions: [],
22 | selections: [
23 | {
24 | kind: 'ScalarField',
25 | alias: null,
26 | name: 'id',
27 | args: null,
28 | storageKey: null,
29 | },
30 | {
31 | kind: 'ScalarField',
32 | alias: null,
33 | name: 'title',
34 | args: null,
35 | storageKey: null,
36 | },
37 | ],
38 | }
39 | ;(node as any).hash = '3d4a1f32013aca926236a60256c1f43e'
40 | export default node
41 |
--------------------------------------------------------------------------------
/issue-tracker/src/__generated__/Issues_repository.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ReaderFragment } from 'relay-runtime'
4 | import { FragmentRefs } from 'relay-runtime'
5 | export type Issues_repository = {
6 | readonly issues: {
7 | readonly edges: ReadonlyArray<{
8 | readonly __id: string
9 | readonly node: {
10 | readonly ' $fragmentRefs': FragmentRefs<'IssueListItem_issue'>
11 | } | null
12 | } | null> | null
13 | }
14 | readonly id: string | null
15 | readonly ' $refType': 'Issues_repository'
16 | }
17 | export type Issues_repository$data = Issues_repository
18 | export type Issues_repository$key = {
19 | readonly ' $data'?: Issues_repository$data
20 | readonly ' $fragmentRefs': FragmentRefs<'Issues_repository'>
21 | }
22 |
23 | const node: ReaderFragment = (function() {
24 | var v0 = ['issues']
25 | return {
26 | kind: 'Fragment',
27 | name: 'Issues_repository',
28 | type: 'Repository',
29 | metadata: {
30 | connection: [
31 | {
32 | count: 'count',
33 | cursor: 'cursor',
34 | direction: 'forward',
35 | path: v0 /*: any*/,
36 | },
37 | ],
38 | refetch: {
39 | connection: {
40 | forward: {
41 | count: 'count',
42 | cursor: 'cursor',
43 | },
44 | backward: null,
45 | path: v0 /*: any*/,
46 | },
47 | operation: require('./IssuesPaginationQuery.graphql.ts'),
48 | fragmentPathInResult: ['node'],
49 | },
50 | },
51 | argumentDefinitions: [
52 | {
53 | kind: 'LocalArgument',
54 | name: 'cursor',
55 | type: 'String',
56 | defaultValue: null,
57 | },
58 | {
59 | kind: 'LocalArgument',
60 | name: 'count',
61 | type: 'Int',
62 | defaultValue: 10,
63 | },
64 | {
65 | kind: 'LocalArgument',
66 | name: 'states',
67 | type: '[IssueState!]',
68 | defaultValue: ['OPEN'],
69 | },
70 | ],
71 | selections: [
72 | {
73 | kind: 'LinkedField',
74 | alias: 'issues',
75 | name: '__Issues_issues_connection',
76 | storageKey: null,
77 | args: [
78 | {
79 | kind: 'Variable',
80 | name: 'states',
81 | variableName: 'states',
82 | },
83 | ],
84 | concreteType: 'IssueConnection',
85 | plural: false,
86 | selections: [
87 | {
88 | kind: 'LinkedField',
89 | alias: null,
90 | name: 'edges',
91 | storageKey: null,
92 | args: null,
93 | concreteType: 'IssueEdge',
94 | plural: true,
95 | selections: [
96 | {
97 | kind: 'LinkedField',
98 | alias: null,
99 | name: 'node',
100 | storageKey: null,
101 | args: null,
102 | concreteType: 'Issue',
103 | plural: false,
104 | selections: [
105 | {
106 | kind: 'ScalarField',
107 | alias: null,
108 | name: '__typename',
109 | args: null,
110 | storageKey: null,
111 | },
112 | {
113 | kind: 'FragmentSpread',
114 | name: 'IssueListItem_issue',
115 | args: null,
116 | },
117 | ],
118 | },
119 | {
120 | kind: 'ScalarField',
121 | alias: null,
122 | name: 'cursor',
123 | args: null,
124 | storageKey: null,
125 | },
126 | {
127 | kind: 'ClientExtension',
128 | selections: [
129 | {
130 | kind: 'ScalarField',
131 | alias: null,
132 | name: '__id',
133 | args: null,
134 | storageKey: null,
135 | },
136 | ],
137 | },
138 | ],
139 | },
140 | {
141 | kind: 'LinkedField',
142 | alias: null,
143 | name: 'pageInfo',
144 | storageKey: null,
145 | args: null,
146 | concreteType: 'PageInfo',
147 | plural: false,
148 | selections: [
149 | {
150 | kind: 'ScalarField',
151 | alias: null,
152 | name: 'endCursor',
153 | args: null,
154 | storageKey: null,
155 | },
156 | {
157 | kind: 'ScalarField',
158 | alias: null,
159 | name: 'hasNextPage',
160 | args: null,
161 | storageKey: null,
162 | },
163 | ],
164 | },
165 | ],
166 | },
167 | {
168 | kind: 'ScalarField',
169 | alias: null,
170 | name: 'id',
171 | args: null,
172 | storageKey: null,
173 | },
174 | ],
175 | }
176 | })()
177 | ;(node as any).hash = '97f2b1fb82f838eb80e38bcc20d765b5'
178 | export default node
179 |
--------------------------------------------------------------------------------
/issue-tracker/src/__generated__/RootQuery.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ConcreteRequest } from 'relay-runtime'
4 | export type RootQueryVariables = {
5 | owner: string
6 | name: string
7 | }
8 | export type RootQueryResponse = {
9 | readonly repository: {
10 | readonly owner: {
11 | readonly login: string
12 | }
13 | readonly name: string
14 | } | null
15 | }
16 | export type RootQuery = {
17 | readonly response: RootQueryResponse
18 | readonly variables: RootQueryVariables
19 | }
20 |
21 | /*
22 | query RootQuery(
23 | $owner: String!
24 | $name: String!
25 | ) {
26 | repository(owner: $owner, name: $name) {
27 | owner {
28 | __typename
29 | login
30 | id
31 | }
32 | name
33 | id
34 | }
35 | }
36 | */
37 |
38 | const node: ConcreteRequest = (function() {
39 | var v0 = [
40 | {
41 | kind: 'LocalArgument',
42 | name: 'owner',
43 | type: 'String!',
44 | defaultValue: null,
45 | },
46 | {
47 | kind: 'LocalArgument',
48 | name: 'name',
49 | type: 'String!',
50 | defaultValue: null,
51 | },
52 | ],
53 | v1 = [
54 | {
55 | kind: 'Variable',
56 | name: 'name',
57 | variableName: 'name',
58 | },
59 | {
60 | kind: 'Variable',
61 | name: 'owner',
62 | variableName: 'owner',
63 | },
64 | ],
65 | v2 = {
66 | kind: 'ScalarField',
67 | alias: null,
68 | name: 'login',
69 | args: null,
70 | storageKey: null,
71 | },
72 | v3 = {
73 | kind: 'ScalarField',
74 | alias: null,
75 | name: 'name',
76 | args: null,
77 | storageKey: null,
78 | },
79 | v4 = {
80 | kind: 'ScalarField',
81 | alias: null,
82 | name: 'id',
83 | args: null,
84 | storageKey: null,
85 | }
86 | return {
87 | kind: 'Request',
88 | fragment: {
89 | kind: 'Fragment',
90 | name: 'RootQuery',
91 | type: 'Query',
92 | metadata: null,
93 | argumentDefinitions: v0 /*: any*/,
94 | selections: [
95 | {
96 | kind: 'LinkedField',
97 | alias: null,
98 | name: 'repository',
99 | storageKey: null,
100 | args: v1 /*: any*/,
101 | concreteType: 'Repository',
102 | plural: false,
103 | selections: [
104 | {
105 | kind: 'LinkedField',
106 | alias: null,
107 | name: 'owner',
108 | storageKey: null,
109 | args: null,
110 | concreteType: null,
111 | plural: false,
112 | selections: [v2 /*: any*/],
113 | },
114 | v3 /*: any*/,
115 | ],
116 | },
117 | ],
118 | },
119 | operation: {
120 | kind: 'Operation',
121 | name: 'RootQuery',
122 | argumentDefinitions: v0 /*: any*/,
123 | selections: [
124 | {
125 | kind: 'LinkedField',
126 | alias: null,
127 | name: 'repository',
128 | storageKey: null,
129 | args: v1 /*: any*/,
130 | concreteType: 'Repository',
131 | plural: false,
132 | selections: [
133 | {
134 | kind: 'LinkedField',
135 | alias: null,
136 | name: 'owner',
137 | storageKey: null,
138 | args: null,
139 | concreteType: null,
140 | plural: false,
141 | selections: [
142 | {
143 | kind: 'ScalarField',
144 | alias: null,
145 | name: '__typename',
146 | args: null,
147 | storageKey: null,
148 | },
149 | v2 /*: any*/,
150 | v4 /*: any*/,
151 | ],
152 | },
153 | v3 /*: any*/,
154 | v4 /*: any*/,
155 | ],
156 | },
157 | ],
158 | },
159 | params: {
160 | operationKind: 'query',
161 | name: 'RootQuery',
162 | id: null,
163 | text:
164 | 'query RootQuery(\n $owner: String!\n $name: String!\n) {\n repository(owner: $owner, name: $name) {\n owner {\n __typename\n login\n id\n }\n name\n id\n }\n}\n',
165 | metadata: {},
166 | },
167 | }
168 | })()
169 | ;(node as any).hash = '86d44615100f8e82c5a3ff63daa08bd5'
170 | export default node
171 |
--------------------------------------------------------------------------------
/issue-tracker/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | pre {
11 | background-color: #f5f5f5;
12 | padding: 10px;
13 | }
14 |
15 | code {
16 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
17 | monospace;
18 | }
19 |
20 | .root {
21 | background-color: #f5f5f5;
22 | min-height: 100vh;
23 | }
24 |
25 | .header {
26 | background-color: #282c34;
27 | display: flex;
28 | flex-direction: column;
29 | align-items: center;
30 | justify-content: center;
31 | font-size: calc(10px + 2vmin);
32 | color: white;
33 | }
34 |
35 | .content {
36 | margin: 20px;
37 | margin-bottom: 40px;
38 | padding: 10px;
39 | background-color: #fff;
40 | border-radius: 2px;
41 | }
42 |
43 | .issues {
44 | margin: 10px;
45 | }
46 |
47 | .issues-issue {
48 | padding: 2px 0;
49 | }
50 |
51 | .issues-load-more,
52 | .issue-comments-load-more {
53 | margin-top: 20px;
54 | padding: 4px 10px;
55 | background-color: #fff;
56 | border-color: #282c34;
57 | }
58 |
59 | .issue-comments-load-more {
60 | margin-left: 48px;
61 | }
62 |
63 | .issue {
64 | /* max-width: 50vw; */
65 | }
66 |
67 | .issue-title {
68 | font-size: 120%;
69 | padding-bottom: 10px;
70 | border-bottom: 1px solid #282c34;
71 | }
72 |
73 | .issue-title-github-link {
74 | float: right;
75 | }
76 |
77 | .issue-author {
78 | margin-bottom: 10px;
79 | font-weight: bold;
80 | }
81 |
82 | .issue-comment {
83 | padding: 10px 0px;
84 | margin-left: 48px;
85 | border-bottom: 1px solid #282c34;
86 | }
87 |
88 | .issue-comment-body p {
89 | margin-top: 0;
90 | margin-bottom: 0;
91 | }
92 |
93 | .issue-comment-author-name {
94 | font-weight: bold;
95 | padding: 8px 0px;
96 | }
97 |
98 | .issue-comment-author-image {
99 | float: left;
100 | margin-left: -48px;
101 | padding-right: 16px;
102 | width: 32px;
103 | height: 32px;
104 | }
105 |
106 | .issue-no-comments {
107 | margin-top: 20px;
108 | margin-left: 48px;
109 | font-weight: bold;
110 | }
111 |
112 | .issue-actions {
113 | margin-top: 20px;
114 | margin-left: 48px;
115 | padding-top: 10px;
116 | border-top: 1px solid #282c34;
117 | }
118 |
119 | .issue-actions-text {
120 | display: block;
121 | padding: 4px 10px;
122 | background-color: #fff;
123 | border-color: #282c34;
124 | box-sizing: border-box;
125 | width: 100%;
126 | margin-bottom: 20px;
127 | }
128 |
129 | .issue-actions-button {
130 | padding: 4px 10px;
131 | background-color: #fff;
132 | border-color: #282c34;
133 | margin-right: 10px;
134 | }
135 |
--------------------------------------------------------------------------------
/issue-tracker/src/index.tsx:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | import React from 'react'
4 | import ReactDOM from 'react-dom'
5 | import { RelayEnvironmentProvider } from 'react-relay/hooks'
6 | import './index.css'
7 | import RelayEnvironment from './RelayEnvironment'
8 | import RoutingContext from './routing/RoutingContext'
9 | import createRouter from './routing/createRouter'
10 | import RouterRenderer from './routing/RouteRenderer'
11 | import routes from './routes'
12 |
13 | // Uses the custom router setup to define a router instance that we can pass through context
14 | const router = createRouter(routes)
15 |
16 | ReactDOM.createRoot(document.getElementById('root')!).render(
17 |
18 |
19 | {/* Render the active route */}
20 |
21 |
22 | ,
23 | )
24 |
--------------------------------------------------------------------------------
/issue-tracker/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare module 'babel-plugin-relay/macro' {
3 | import { graphql } from 'react-relay/hooks'
4 | export default graphql
5 | }
6 |
--------------------------------------------------------------------------------
/issue-tracker/src/routes.ts:
--------------------------------------------------------------------------------
1 | import { preloadQuery } from 'react-relay/hooks'
2 |
3 | import JSResource from './JSResource'
4 | import RelayEnvironment from './RelayEnvironment'
5 | import { RouteConfig } from './routing/createRouter'
6 |
7 | import PreloadRootQuery, { RootQuery } from './__generated__/RootQuery.graphql'
8 | import PreloadIssuesQuery, {
9 | HomeRootIssuesQuery,
10 | } from './__generated__/HomeRootIssuesQuery.graphql'
11 | import PreloadIssueDetailRootQuery, {
12 | IssueDetailRootQuery,
13 | } from './__generated__/IssueDetailRootQuery.graphql'
14 |
15 | const routes: RouteConfig[] = [
16 | {
17 | component: JSResource('Root', () => import('./Root')),
18 | prepare: () => ({
19 | rootQuery: preloadQuery(
20 | RelayEnvironment,
21 | PreloadRootQuery,
22 | {
23 | owner: 'facebook',
24 | name: 'relay',
25 | },
26 | // The fetchPolicy allows us to specify whether to render from cached
27 | // data if possible (store-or-network) or only fetch from network
28 | // (network-only).
29 | { fetchPolicy: 'store-or-network' },
30 | ),
31 | }),
32 | routes: [
33 | {
34 | path: '/',
35 | exact: true,
36 | /**
37 | * A lazy reference to the component for the home route. Note that we intentionally don't
38 | * use React.lazy here: that would start loading the component only when it's rendered.
39 | * By using a custom alternative we can start loading the code instantly. This is
40 | * especially useful with nested routes, where React.lazy would not fetch the
41 | * component until its parents code/data had loaded. Nested route support isn't
42 | * implemented in our mini-router yet, but one can imagine iterating over all
43 | * the matched route entries and calling .load() on each of their components.
44 | */
45 | component: JSResource('HomeRoot', () => import('./HomeRoot')),
46 | /**
47 | * A function to prepare the data for the `component` in parallel with loading
48 | * that component code. The actual data to fetch is defined by the component
49 | * itself - here we just reference a description of the data - the generated
50 | * query.
51 | */
52 | prepare: () => ({
53 | issuesQuery: preloadQuery(
54 | RelayEnvironment,
55 | PreloadIssuesQuery,
56 | {
57 | owner: 'facebook',
58 | name: 'relay',
59 | },
60 | // The fetchPolicy allows us to specify whether to render from cached
61 | // data if possible (store-or-network) or only fetch from network
62 | // (network-only).
63 | { fetchPolicy: 'store-or-network' },
64 | ),
65 | }),
66 | },
67 | {
68 | path: '/issue/:id',
69 | component: JSResource('IssueDetailRoot', () =>
70 | import('./IssueDetailRoot'),
71 | ),
72 | prepare: params => ({
73 | issueDetailQuery: preloadQuery(
74 | RelayEnvironment,
75 | PreloadIssueDetailRootQuery,
76 | {
77 | id: params.id,
78 | },
79 | { fetchPolicy: 'store-or-network' },
80 | ),
81 | }),
82 | },
83 | ],
84 | },
85 | ]
86 |
87 | export default routes
88 |
--------------------------------------------------------------------------------
/issue-tracker/src/routing/Link.tsx:
--------------------------------------------------------------------------------
1 | /* eslint no-restricted-globals:0 */
2 | import React, { useCallback, useContext } from 'react'
3 | import RoutingContext from './RoutingContext'
4 |
5 | interface Props {
6 | to: string
7 | children: React.ReactNode
8 | }
9 |
10 | /**
11 | * An alternative to react-router's Link component that works with
12 | * our custom RoutingContext.
13 | */
14 | export default function Link(props: Props) {
15 | const router = useContext(RoutingContext)
16 |
17 | // When the user clicks, change route
18 | const changeRoute = useCallback(
19 | (event: React.MouseEvent) => {
20 | event.preventDefault()
21 | return router?.history.push(props.to)
22 | },
23 | [props.to, router],
24 | )
25 |
26 | // Callback to preload just the code for the route:
27 | // we pass this to onMouseEnter, which is a weaker signal
28 | // that the user *may* navigate to the route.
29 | const preloadRouteCode = useCallback(() => {
30 | return router?.preloadCode(props.to)
31 | }, [props.to, router])
32 |
33 | // Callback to preload the code and data for the route:
34 | // we pass this to onMouseDown, since this is a stronger
35 | // signal that the user will likely complete the navigation
36 | const preloadRoute = useCallback(() => {
37 | return router?.preload(props.to)
38 | }, [props.to, router])
39 |
40 | return (
41 |
47 | {props.children}
48 |
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/issue-tracker/src/routing/RouteRenderer.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Delay the pending indicator in case the transition is very fast:
3 | * https://reactjs.org/docs/concurrent-mode-patterns.html#delaying-a-pending-indicator
4 | */
5 |
6 | .RouteRenderer-pending {
7 | position: absolute;
8 | z-index: 1;
9 | background-color: #fff;
10 | animation: 0s linear 0.5s forwards makeVisible;
11 | visibility: hidden;
12 | }
13 |
14 | @keyframes makeVisible {
15 | to {
16 | visibility: visible;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/issue-tracker/src/routing/RouteRenderer.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useContext,
3 | useState,
4 | useEffect,
5 | Suspense,
6 | useTransition,
7 | } from 'react'
8 | import RoutingContext, { RouteComponentProps } from './RoutingContext'
9 | import ErrorBoundary from '../ErrorBoundary'
10 | import './RouteRenderer.css'
11 |
12 | const SUSPENSE_CONFIG = { timeoutMs: 2000 }
13 |
14 | export default function RouterRenderer() {
15 | // Access the router
16 | const router = useContext(RoutingContext)
17 | // Improve the route transition UX by delaying transitions: show the previous route entry
18 | // for a brief period while the next route is being prepared. See
19 | // https://reactjs.org/docs/concurrent-mode-patterns.html#transitions
20 | const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG)
21 |
22 | // Store the active entry in state - this allows the renderer to use features like
23 | // useTransition to delay when state changes become visible to the user.
24 | const [routeEntry, setRouteEntry] = useState(router!.get())
25 |
26 | // On mount subscribe to route changes
27 | useEffect(() => {
28 | // Check if the route has changed between the last render and commit:
29 | const currentEntry = router!.get()
30 | if (currentEntry !== routeEntry) {
31 | // if there was a concurrent modification, rerender and exit
32 | setRouteEntry(currentEntry)
33 | return
34 | }
35 |
36 | // If there *wasn't* a concurrent change to the route, then the UI
37 | // is current: subscribe for subsequent route updates
38 | const dispose = router!.subscribe(nextEntry => {
39 | // startTransition() delays the effect of the setRouteEntry (setState) call
40 | // for a brief period, continuing to show the old state while the new
41 | // state (route) is prepared.
42 | startTransition(() => {
43 | setRouteEntry(nextEntry)
44 | })
45 | })
46 | return () => dispose()
47 | // Note: this hook updates routeEntry manually; we exclude that variable
48 | // from the hook deps to avoid recomputing the effect after each change
49 | // triggered by the effect itself.
50 | // eslint-disable-next-line
51 | }, [router, startTransition])
52 |
53 | // The current route value is an array of matching entries - one entry per
54 | // level of routes (to allow nested routes). We have to map each one to a
55 | // RouteComponent to allow suspending, and also pass its children correctly.
56 | // Conceptually, we want this structure:
57 | // ```
58 | //
61 | //
64 | // // continue for nested items...
65 | //
66 | //
67 | // ```
68 | // To achieve this, we reverse the list so we can start at the bottom-most
69 | // component, and iteratively construct parent components w the previous
70 | // value as the child of the next one:
71 | const { entries } = routeEntry
72 | const reversedItems = entries.reverse() // reverse is in place
73 | const firstItem = reversedItems[0]
74 | // the bottom-most component is special since it will have no children
75 | // (though we could probably just pass null children to it)
76 | let routeComponent = (
77 |
82 | )
83 | for (let ii = 1; ii < reversedItems.length; ii++) {
84 | const nextItem = reversedItems[ii]
85 | routeComponent = (
86 |
91 | {routeComponent}
92 |
93 | )
94 | }
95 |
96 | // Routes can error so wrap in an
97 | // Routes can suspend, so wrap in
98 |
99 | return (
100 |
101 |
102 | {/* Indicate to the user that a transition is pending, even while showing the previous UI */}
103 | {isPending ? (
104 | Loading pending...
105 | ) : null}
106 | {routeComponent}
107 |
108 |
109 | )
110 | }
111 |
112 | /**
113 | * The `component` property from the route entry is a Resource, which may or may not be ready.
114 | * We use a helper child component to unwrap the resource with component.read(), and then
115 | * render it if its ready.
116 | *
117 | * NOTE: calling routeEntry.route.component.read() directly in RouteRenderer woldn't work the
118 | * way we'd expect. Because that method could throw - either suspending or on error - the error
119 | * would bubble up to the *caller* of RouteRenderer. We want the suspend/error to bubble up to
120 | * our ErrorBoundary/Suspense components, so we have to ensure that the suspend/error happens
121 | * in a child component.
122 | */
123 | function RouteComponent(props: RouteComponentProps) {
124 | const Component = props.component!.read()
125 | const { routeData, prepared } = props
126 | return (
127 |
132 | )
133 | }
134 |
--------------------------------------------------------------------------------
/issue-tracker/src/routing/RoutingContext.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { History, Location } from 'history'
3 | import { match } from 'react-router'
4 | import { Resource } from '../JSResource'
5 | import { PreloadedQuery } from 'react-relay/lib/relay-experimental/EntryPointTypes'
6 |
7 | export type PreparedQuery = {
8 | [queryName: string]: PreloadedQuery
9 | }
10 |
11 | export interface RouteComponentProps extends Entry {
12 | children?: JSX.Element
13 | }
14 | export interface Route {
15 | location: Location
16 | entries: Entry[]
17 | }
18 |
19 | export interface Entry {
20 | component?: Resource
21 | prepared: PreparedQuery
22 | routeData: match<{}>
23 | }
24 |
25 | export interface Router {
26 | history: History
27 | get: () => Route
28 | preloadCode: (pathname: string) => void
29 | preload: (pathname: string) => void
30 | subscribe: (callback: (arg: Route) => void) => () => void
31 | }
32 |
33 | const RoutingContext = React.createContext(null)
34 |
35 | /**
36 | * A custom context instance for our router type
37 | */
38 | export default RoutingContext
39 |
--------------------------------------------------------------------------------
/issue-tracker/src/routing/createRouter.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createBrowserHistory,
3 | BrowserHistoryBuildOptions,
4 | Location,
5 | } from 'history'
6 | import {
7 | matchRoutes,
8 | RouteConfig as DefaultRouteConfig,
9 | } from 'react-router-config'
10 | import { Router, Route, Entry } from './RoutingContext'
11 | import { Resource } from '../JSResource'
12 | import { PreloadedQuery } from 'react-relay/lib/relay-experimental/EntryPointTypes'
13 | import { match } from 'react-router'
14 |
15 | export interface RouteConfig {
16 | key?: React.Key
17 | location?: Location
18 | path?: string | string[]
19 | exact?: boolean
20 | strict?: boolean
21 | component: Resource
22 | prepare?: (params: {
23 | [key: string]: string
24 | }) => { [queryName: string]: PreloadedQuery }
25 | routes?: RouteConfig[]
26 | }
27 |
28 | export interface MatchedRoute {
29 | route: RouteConfig
30 | match: match
31 | }
32 |
33 | /**
34 | * A custom router built from the same primitives as react-router. Each object in `routes`
35 | * contains both a Component and a prepare() function that can preload data for the component.
36 | * The router watches for changes to the current location via the `history` package, maps the
37 | * location to the corresponding route entry, and then preloads the code and data for the route.
38 | */
39 | export default function createRouter(
40 | routes: RouteConfig[],
41 | options?: BrowserHistoryBuildOptions,
42 | ) {
43 | // Initialize history
44 | const history = createBrowserHistory(options)
45 |
46 | // Find the initial match and prepare it
47 | const initialMatches = matchRoute(routes, history.location)
48 | const initialEntries = prepareMatches(initialMatches)
49 | let currentEntry = {
50 | location: history.location,
51 | entries: initialEntries,
52 | }
53 |
54 | // maintain a set of subscribers to the active entry
55 | let nextId = 0
56 | const subscribers = new Map()
57 |
58 | // Listen for location changes, match to the route entry, prepare the entry,
59 | // and notify subscribers. Note that this pattern ensures that data-loading
60 | // occurs *outside* of - and *before* - rendering.
61 | const cleanup = history.listen((location, action) => {
62 | if (location.pathname === currentEntry.location.pathname) {
63 | return
64 | }
65 | const matches = matchRoute(routes, location)
66 | const entries = prepareMatches(matches)
67 | const nextEntry = {
68 | location,
69 | entries,
70 | }
71 | currentEntry = nextEntry
72 | subscribers.forEach(cb => cb(nextEntry))
73 | })
74 |
75 | // The actual object that will be passed on the RoutingContext.
76 | const context: Router = {
77 | history,
78 | get() {
79 | return currentEntry
80 | },
81 | preloadCode(pathname: string) {
82 | // preload just the code for a route, without storing the result
83 | const matches = (matchRoutes(
84 | (routes as unknown) as DefaultRouteConfig[],
85 | pathname,
86 | ) as unknown) as MatchedRoute<{}>[]
87 | matches.forEach(({ route }: { route: RouteConfig }) =>
88 | route.component.load(),
89 | )
90 | },
91 | preload(pathname: string) {
92 | // preload the code and data for a route, without storing the result
93 | const matches = (matchRoutes(
94 | (routes as unknown) as DefaultRouteConfig[],
95 | pathname,
96 | ) as unknown) as MatchedRoute<{}>[]
97 | prepareMatches(matches)
98 | },
99 | subscribe(cb: (arg: Route) => void) {
100 | const id = nextId++
101 | const dispose = () => {
102 | subscribers.delete(id)
103 | }
104 | subscribers.set(id, cb)
105 | return dispose
106 | },
107 | }
108 |
109 | // Return both the context object and a cleanup function
110 | return { cleanup, context }
111 | }
112 |
113 | /**
114 | * Match the current location to the corresponding route entry.
115 | */
116 | function matchRoute(routes: RouteConfig[], location: Location) {
117 | const matchedRoutes = (matchRoutes(
118 | (routes as unknown) as DefaultRouteConfig[],
119 | location.pathname,
120 | ) as unknown) as MatchedRoute<{}>[]
121 |
122 | if (!Array.isArray(matchedRoutes) || matchedRoutes.length === 0) {
123 | throw new Error('No route for ' + location.pathname)
124 | }
125 | return matchedRoutes
126 | }
127 |
128 | /**
129 | * Load the data for the matched route, given the params extracted from the route
130 | */
131 | function prepareMatches(matches: MatchedRoute<{}>[]): Entry[] {
132 | return matches.map(match => {
133 | const { route, match: matchData } = match
134 | const prepared = route.prepare!(matchData.params)
135 | const Component = route.component.get()
136 | if (Component == null) {
137 | route.component.load() // eagerly load
138 | }
139 | return {
140 | component: route.component,
141 | prepared: prepared,
142 | routeData: matchData,
143 | }
144 | })
145 | }
146 |
--------------------------------------------------------------------------------
/issue-tracker/src/useMutation.ts:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useCallback, useEffect } from 'react'
2 | import { GraphQLTaggedNode } from 'react-relay'
3 | import { useRelayEnvironment } from 'react-relay/hooks'
4 | import {
5 | MutationConfig,
6 | MutationParameters,
7 | commitMutation,
8 | Disposable,
9 | } from 'relay-runtime'
10 |
11 | type CustomMutationConfig = Omit<
12 | MutationConfig,
13 | 'mutation'
14 | >
15 |
16 | export default function useMutation(
17 | mutation: GraphQLTaggedNode,
18 | ): [boolean, (config?: CustomMutationConfig) => void] {
19 | const environment = useRelayEnvironment()
20 | const [isPending, setPending] = useState(false)
21 | const requestRef = useRef(null)
22 | const mountedRef = useRef(false)
23 |
24 | const execute = useCallback(
25 | (config: CustomMutationConfig = { variables: {} }) => {
26 | if (requestRef.current != null) return
27 |
28 | const request = commitMutation(environment, {
29 | ...config,
30 | mutation,
31 | onCompleted: (response, errors) => {
32 | if (!mountedRef.current) return
33 |
34 | requestRef.current = null
35 | setPending(false)
36 | config.onCompleted && config.onCompleted(response, errors)
37 | },
38 | onError: error => {
39 | console.error(error)
40 | if (!mountedRef.current) return
41 |
42 | requestRef.current = null
43 | setPending(false)
44 | config.onError && config.onError(error)
45 | },
46 | })
47 |
48 | requestRef.current = request
49 | setPending(true)
50 | },
51 | [environment, mutation],
52 | )
53 |
54 | useEffect(() => {
55 | mountedRef.current = true
56 | return () => {
57 | mountedRef.current = false
58 | }
59 | }, [])
60 |
61 | return [isPending, execute]
62 | }
63 |
--------------------------------------------------------------------------------
/issue-tracker/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react"
17 | },
18 | "include": ["src"]
19 | }
20 |
--------------------------------------------------------------------------------
/todo/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "passPerPreset": true,
3 | "plugins": [
4 | ["relay", { "artifactDirectory": "./ts/__relay_artifacts__" }],
5 | "@babel/plugin-transform-runtime",
6 | "@babel/plugin-proposal-class-properties"
7 | ],
8 | "presets": [
9 | "@babel/preset-react",
10 | "@babel/preset-env",
11 | "@babel/preset-typescript"
12 | ],
13 | "ignore": ["node_modules"]
14 | }
15 |
--------------------------------------------------------------------------------
/todo/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
--------------------------------------------------------------------------------
/todo/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .eslintcache
3 | node_modules
4 |
--------------------------------------------------------------------------------
/todo/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": false
6 | }
7 |
--------------------------------------------------------------------------------
/todo/README.md:
--------------------------------------------------------------------------------
1 | # Relay TodoMVC
2 |
3 | ## Installation
4 |
5 | ```
6 | yarn
7 | ```
8 |
9 | ## Running
10 |
11 | Set up generated files:
12 |
13 | ```
14 | yarn run update-schema
15 | yarn run build
16 | ```
17 |
18 | Start a local server:
19 |
20 | ```
21 | yarn run start
22 | ```
23 |
24 | ## Developing
25 |
26 | Any changes you make to files in the `js/` directory will cause the server to
27 | automatically rebuild the app and refresh your browser.
28 |
29 | If at any time you make changes to `data/schema.js`, stop the server,
30 | regenerate `data/schema.graphql`, and restart the server:
31 |
32 | ```
33 | yarn run update-schema
34 | yarn run build
35 | yarn run start
36 | ```
37 |
38 | ## License
39 |
40 | This file provided by Facebook is for non-commercial testing and evaluation
41 | purposes only. Facebook reserves all rights not expressly granted.
42 |
43 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
44 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
45 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
46 | FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
47 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
48 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
49 |
--------------------------------------------------------------------------------
/todo/data/database.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | export class Todo {}
14 | export class User {}
15 |
16 | // Mock authenticated ID
17 | const VIEWER_ID = 'me';
18 |
19 | // Mock user data
20 | const viewer = new User();
21 | viewer.id = VIEWER_ID;
22 | const usersById = {
23 | [VIEWER_ID]: viewer,
24 | };
25 |
26 | // Mock todo data
27 | const todosById = {};
28 | const todoIdsByUser = {
29 | [VIEWER_ID]: [],
30 | };
31 | let nextTodoId = 0;
32 | addTodo('Taste JavaScript', true);
33 | addTodo('Buy a unicorn', false);
34 |
35 | export function addTodo(text, complete) {
36 | const todo = new Todo();
37 | todo.complete = !!complete;
38 | todo.id = `${nextTodoId++}`;
39 | todo.text = text;
40 | todosById[todo.id] = todo;
41 | todoIdsByUser[VIEWER_ID].push(todo.id);
42 | return todo.id;
43 | }
44 |
45 | export function changeTodoStatus(id, complete) {
46 | const todo = getTodo(id);
47 | todo.complete = complete;
48 | }
49 |
50 | export function getTodo(id) {
51 | return todosById[id];
52 | }
53 |
54 | export function getTodos(status = 'any') {
55 | const todos = todoIdsByUser[VIEWER_ID].map(id => todosById[id]);
56 | if (status === 'any') {
57 | return todos;
58 | }
59 | return todos.filter(todo => todo.complete === (status === 'completed'));
60 | }
61 |
62 | export function getUser(id) {
63 | return usersById[id];
64 | }
65 |
66 | export function getViewer() {
67 | return getUser(VIEWER_ID);
68 | }
69 |
70 | export function markAllTodos(complete) {
71 | const changedTodos = [];
72 | getTodos().forEach(todo => {
73 | if (todo.complete !== complete) {
74 | todo.complete = complete;
75 | changedTodos.push(todo);
76 | }
77 | });
78 | return changedTodos.map(todo => todo.id);
79 | }
80 |
81 | export function removeTodo(id) {
82 | const todoIndex = todoIdsByUser[VIEWER_ID].indexOf(id);
83 | if (todoIndex !== -1) {
84 | todoIdsByUser[VIEWER_ID].splice(todoIndex, 1);
85 | }
86 | delete todosById[id];
87 | }
88 |
89 | export function removeCompletedTodos() {
90 | const todosToRemove = getTodos().filter(todo => todo.complete);
91 | todosToRemove.forEach(todo => removeTodo(todo.id));
92 | return todosToRemove.map(todo => todo.id);
93 | }
94 |
95 | export function renameTodo(id, text) {
96 | const todo = getTodo(id);
97 | todo.text = text;
98 | }
99 |
--------------------------------------------------------------------------------
/todo/data/schema.graphql:
--------------------------------------------------------------------------------
1 | input AddTodoInput {
2 | text: String!
3 | clientMutationId: String
4 | }
5 |
6 | type AddTodoPayload {
7 | todoEdge: TodoEdge
8 | viewer: User
9 | clientMutationId: String
10 | }
11 |
12 | input ChangeTodoStatusInput {
13 | complete: Boolean!
14 | id: ID!
15 | clientMutationId: String
16 | }
17 |
18 | type ChangeTodoStatusPayload {
19 | todo: Todo
20 | viewer: User
21 | clientMutationId: String
22 | }
23 |
24 | input MarkAllTodosInput {
25 | complete: Boolean!
26 | clientMutationId: String
27 | }
28 |
29 | type MarkAllTodosPayload {
30 | changedTodos: [Todo]
31 | viewer: User
32 | clientMutationId: String
33 | }
34 |
35 | type Mutation {
36 | addTodo(input: AddTodoInput!): AddTodoPayload
37 | changeTodoStatus(input: ChangeTodoStatusInput!): ChangeTodoStatusPayload
38 | markAllTodos(input: MarkAllTodosInput!): MarkAllTodosPayload
39 | removeCompletedTodos(input: RemoveCompletedTodosInput!): RemoveCompletedTodosPayload
40 | removeTodo(input: RemoveTodoInput!): RemoveTodoPayload
41 | renameTodo(input: RenameTodoInput!): RenameTodoPayload
42 | }
43 |
44 | """An object with an ID"""
45 | interface Node {
46 | """The id of the object."""
47 | id: ID!
48 | }
49 |
50 | """Information about pagination in a connection."""
51 | type PageInfo {
52 | """When paginating forwards, are there more items?"""
53 | hasNextPage: Boolean!
54 |
55 | """When paginating backwards, are there more items?"""
56 | hasPreviousPage: Boolean!
57 |
58 | """When paginating backwards, the cursor to continue."""
59 | startCursor: String
60 |
61 | """When paginating forwards, the cursor to continue."""
62 | endCursor: String
63 | }
64 |
65 | type Query {
66 | viewer: User
67 |
68 | """Fetches an object given its ID"""
69 | node(
70 | """The ID of an object"""
71 | id: ID!
72 | ): Node
73 | }
74 |
75 | input RemoveCompletedTodosInput {
76 | clientMutationId: String
77 | }
78 |
79 | type RemoveCompletedTodosPayload {
80 | deletedTodoIds: [String]
81 | viewer: User
82 | clientMutationId: String
83 | }
84 |
85 | input RemoveTodoInput {
86 | id: ID!
87 | clientMutationId: String
88 | }
89 |
90 | type RemoveTodoPayload {
91 | deletedTodoId: ID
92 | viewer: User
93 | clientMutationId: String
94 | }
95 |
96 | input RenameTodoInput {
97 | id: ID!
98 | text: String!
99 | clientMutationId: String
100 | }
101 |
102 | type RenameTodoPayload {
103 | todo: Todo
104 | clientMutationId: String
105 | }
106 |
107 | type Todo implements Node {
108 | """The ID of an object"""
109 | id: ID!
110 | text: String
111 | complete: Boolean
112 | }
113 |
114 | """A connection to a list of items."""
115 | type TodoConnection {
116 | """Information to aid in pagination."""
117 | pageInfo: PageInfo!
118 |
119 | """A list of edges."""
120 | edges: [TodoEdge]
121 | }
122 |
123 | """An edge in a connection."""
124 | type TodoEdge {
125 | """The item at the end of the edge"""
126 | node: Todo
127 |
128 | """A cursor for use in pagination"""
129 | cursor: String!
130 | }
131 |
132 | type User implements Node {
133 | """The ID of an object"""
134 | id: ID!
135 | todos(status: String = "any", after: String, first: Int, before: String, last: Int): TodoConnection
136 | totalCount: Int
137 | completedCount: Int
138 | }
139 |
--------------------------------------------------------------------------------
/todo/data/schema.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import {
14 | GraphQLBoolean,
15 | GraphQLID,
16 | GraphQLInt,
17 | GraphQLList,
18 | GraphQLNonNull,
19 | GraphQLObjectType,
20 | GraphQLSchema,
21 | GraphQLString,
22 | } from "graphql"
23 |
24 | import {
25 | connectionArgs,
26 | connectionDefinitions,
27 | connectionFromArray,
28 | cursorForObjectInConnection,
29 | fromGlobalId,
30 | globalIdField,
31 | mutationWithClientMutationId,
32 | nodeDefinitions,
33 | toGlobalId,
34 | } from "graphql-relay"
35 |
36 | import {
37 | Todo,
38 | User,
39 | addTodo,
40 | changeTodoStatus,
41 | getTodo,
42 | getTodos,
43 | getUser,
44 | getViewer,
45 | markAllTodos,
46 | removeCompletedTodos,
47 | removeTodo,
48 | renameTodo,
49 | } from "./database"
50 |
51 | const { nodeInterface, nodeField } = nodeDefinitions(
52 | globalId => {
53 | const { type, id } = fromGlobalId(globalId)
54 | if (type === "Todo") {
55 | return getTodo(id)
56 | } else if (type === "User") {
57 | return getUser(id)
58 | }
59 | return null
60 | },
61 | obj => {
62 | if (obj instanceof Todo) {
63 | return GraphQLTodo
64 | } else if (obj instanceof User) {
65 | return GraphQLUser
66 | }
67 | return null
68 | },
69 | )
70 |
71 | const GraphQLTodo = new GraphQLObjectType({
72 | name: "Todo",
73 | fields: {
74 | id: globalIdField("Todo"),
75 | text: {
76 | type: GraphQLString,
77 | resolve: obj => obj.text,
78 | },
79 | complete: {
80 | type: GraphQLBoolean,
81 | resolve: obj => obj.complete,
82 | },
83 | },
84 | interfaces: [nodeInterface],
85 | })
86 |
87 | const {
88 | connectionType: TodosConnection,
89 | edgeType: GraphQLTodoEdge,
90 | } = connectionDefinitions({
91 | name: "Todo",
92 | nodeType: GraphQLTodo,
93 | })
94 |
95 | const GraphQLUser = new GraphQLObjectType({
96 | name: "User",
97 | fields: {
98 | id: globalIdField("User"),
99 | todos: {
100 | type: TodosConnection,
101 | args: {
102 | status: {
103 | type: GraphQLString,
104 | defaultValue: "any",
105 | },
106 | ...connectionArgs,
107 | },
108 | resolve: (obj, { status, ...args }) =>
109 | connectionFromArray(getTodos(status), args),
110 | },
111 | totalCount: {
112 | type: GraphQLInt,
113 | resolve: () => getTodos().length,
114 | },
115 | completedCount: {
116 | type: GraphQLInt,
117 | resolve: () => getTodos("completed").length,
118 | },
119 | },
120 | interfaces: [nodeInterface],
121 | })
122 |
123 | const Query = new GraphQLObjectType({
124 | name: "Query",
125 | fields: {
126 | viewer: {
127 | type: GraphQLUser,
128 | resolve: () => getViewer(),
129 | },
130 | node: nodeField,
131 | },
132 | })
133 |
134 | const GraphQLAddTodoMutation = mutationWithClientMutationId({
135 | name: "AddTodo",
136 | inputFields: {
137 | text: { type: new GraphQLNonNull(GraphQLString) },
138 | },
139 | outputFields: {
140 | todoEdge: {
141 | type: GraphQLTodoEdge,
142 | resolve: ({ localTodoId }) => {
143 | const todo = getTodo(localTodoId)
144 | return {
145 | cursor: cursorForObjectInConnection(getTodos(), todo),
146 | node: todo,
147 | }
148 | },
149 | },
150 | viewer: {
151 | type: GraphQLUser,
152 | resolve: () => getViewer(),
153 | },
154 | },
155 | mutateAndGetPayload: ({ text }) => {
156 | const localTodoId = addTodo(text)
157 | return { localTodoId }
158 | },
159 | })
160 |
161 | const GraphQLChangeTodoStatusMutation = mutationWithClientMutationId({
162 | name: "ChangeTodoStatus",
163 | inputFields: {
164 | complete: { type: new GraphQLNonNull(GraphQLBoolean) },
165 | id: { type: new GraphQLNonNull(GraphQLID) },
166 | },
167 | outputFields: {
168 | todo: {
169 | type: GraphQLTodo,
170 | resolve: ({ localTodoId }) => getTodo(localTodoId),
171 | },
172 | viewer: {
173 | type: GraphQLUser,
174 | resolve: () => getViewer(),
175 | },
176 | },
177 | mutateAndGetPayload: ({ id, complete }) => {
178 | const localTodoId = fromGlobalId(id).id
179 | changeTodoStatus(localTodoId, complete)
180 | return { localTodoId }
181 | },
182 | })
183 |
184 | const GraphQLMarkAllTodosMutation = mutationWithClientMutationId({
185 | name: "MarkAllTodos",
186 | inputFields: {
187 | complete: { type: new GraphQLNonNull(GraphQLBoolean) },
188 | },
189 | outputFields: {
190 | changedTodos: {
191 | type: new GraphQLList(GraphQLTodo),
192 | resolve: ({ changedTodoLocalIds }) => changedTodoLocalIds.map(getTodo),
193 | },
194 | viewer: {
195 | type: GraphQLUser,
196 | resolve: () => getViewer(),
197 | },
198 | },
199 | mutateAndGetPayload: ({ complete }) => {
200 | const changedTodoLocalIds = markAllTodos(complete)
201 | return { changedTodoLocalIds }
202 | },
203 | })
204 |
205 | // TODO: Support plural deletes
206 | const GraphQLRemoveCompletedTodosMutation = mutationWithClientMutationId({
207 | name: "RemoveCompletedTodos",
208 | outputFields: {
209 | deletedTodoIds: {
210 | type: new GraphQLList(GraphQLString),
211 | resolve: ({ deletedTodoIds }) => deletedTodoIds,
212 | },
213 | viewer: {
214 | type: GraphQLUser,
215 | resolve: () => getViewer(),
216 | },
217 | },
218 | mutateAndGetPayload: () => {
219 | const deletedTodoLocalIds = removeCompletedTodos()
220 | const deletedTodoIds = deletedTodoLocalIds.map(
221 | toGlobalId.bind(null, "Todo"),
222 | )
223 | return { deletedTodoIds }
224 | },
225 | })
226 |
227 | const GraphQLRemoveTodoMutation = mutationWithClientMutationId({
228 | name: "RemoveTodo",
229 | inputFields: {
230 | id: { type: new GraphQLNonNull(GraphQLID) },
231 | },
232 | outputFields: {
233 | deletedTodoId: {
234 | type: GraphQLID,
235 | resolve: ({ id }) => id,
236 | },
237 | viewer: {
238 | type: GraphQLUser,
239 | resolve: () => getViewer(),
240 | },
241 | },
242 | mutateAndGetPayload: ({ id }) => {
243 | const localTodoId = fromGlobalId(id).id
244 | removeTodo(localTodoId)
245 | return { id }
246 | },
247 | })
248 |
249 | const GraphQLRenameTodoMutation = mutationWithClientMutationId({
250 | name: "RenameTodo",
251 | inputFields: {
252 | id: { type: new GraphQLNonNull(GraphQLID) },
253 | text: { type: new GraphQLNonNull(GraphQLString) },
254 | },
255 | outputFields: {
256 | todo: {
257 | type: GraphQLTodo,
258 | resolve: ({ localTodoId }) => getTodo(localTodoId),
259 | },
260 | },
261 | mutateAndGetPayload: ({ id, text }) => {
262 | const localTodoId = fromGlobalId(id).id
263 | renameTodo(localTodoId, text)
264 | return { localTodoId }
265 | },
266 | })
267 |
268 | const Mutation = new GraphQLObjectType({
269 | name: "Mutation",
270 | fields: {
271 | addTodo: GraphQLAddTodoMutation,
272 | changeTodoStatus: GraphQLChangeTodoStatusMutation,
273 | markAllTodos: GraphQLMarkAllTodosMutation,
274 | removeCompletedTodos: GraphQLRemoveCompletedTodosMutation,
275 | removeTodo: GraphQLRemoveTodoMutation,
276 | renameTodo: GraphQLRenameTodoMutation,
277 | },
278 | })
279 |
280 | export const schema = new GraphQLSchema({
281 | query: Query,
282 | mutation: Mutation,
283 | })
284 |
--------------------------------------------------------------------------------
/todo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "start": "babel-node ./server.js --extensions \".js,.ts,.tsx\"",
5 | "build": "relay-compiler --src ./ts/ --schema ./data/schema.graphql --language typescript --artifactDirectory ./ts/__relay_artifacts__",
6 | "update-schema": "babel-node ./scripts/updateSchema.js",
7 | "lint": "tslint --project tsconfig.json",
8 | "type-check": "tsc --noEmit --pretty"
9 | },
10 | "dependencies": {
11 | "classnames": "2.2.5",
12 | "cors": "^2.8.5",
13 | "express": "^4.15.2",
14 | "express-graphql": "^0.7.1",
15 | "graphql": "^14.5.8",
16 | "graphql-compiler": "^1.7.0",
17 | "graphql-relay": "^0.6.0",
18 | "prop-types": "^15.6.2",
19 | "react": "^16.11.0",
20 | "react-dom": "^16.11.0",
21 | "react-relay": "^7.0.0",
22 | "relay-runtime": "^7.0.0",
23 | "todomvc-app-css": "^2.1.0",
24 | "todomvc-common": "^1.0.3",
25 | "webpack": "^3.5.5",
26 | "webpack-dev-server": "^2.7.1",
27 | "whatwg-fetch": "2.0.3"
28 | },
29 | "devDependencies": {
30 | "@babel/cli": "^7.6.4",
31 | "@babel/core": "^7.6.4",
32 | "@babel/node": "^7.6.3",
33 | "@babel/plugin-transform-runtime": "^7.6.2",
34 | "@babel/preset-env": "^7.6.3",
35 | "@babel/preset-react": "^7.6.3",
36 | "@babel/preset-typescript": "^7.6.0",
37 | "@types/node": "^12.11.6",
38 | "@types/prop-types": "^15.5.5",
39 | "@types/react": "^16.4.12",
40 | "@types/react-dom": "^16.0.7",
41 | "@types/react-relay": "^7.0.0",
42 | "@types/relay-runtime": "^6.0.9",
43 | "babel-eslint": "6.1.2",
44 | "babel-loader": "^8.0.6",
45 | "babel-plugin-relay": "^7.0.0",
46 | "babel-plugin-transform-runtime": "^6.12.0",
47 | "babel-preset-env": "^1.7.0",
48 | "babel-preset-react": "^6.11.1",
49 | "babel-preset-stage-0": "^6.5.0",
50 | "babel-runtime": "^6.26.0",
51 | "csstype": "^2.5.6",
52 | "fork-ts-checker-webpack-plugin": "^0.3.0",
53 | "patch-package": "^6.2.0",
54 | "postinstall-postinstall": "^2.0.0",
55 | "relay-compiler": "^7.0.0",
56 | "relay-compiler-language-typescript": "^10.1.0",
57 | "ts-loader": "^6.2.0",
58 | "tslint": "^5.20.0",
59 | "tslint-plugin-relay": "^0.0.3",
60 | "typescript": "^3.6.3"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/todo/public/base.css:
--------------------------------------------------------------------------------
1 | hr {
2 | margin: 20px 0;
3 | border: 0;
4 | border-top: 1px dashed #c5c5c5;
5 | border-bottom: 1px dashed #f7f7f7;
6 | }
7 |
8 | .learn a {
9 | font-weight: normal;
10 | text-decoration: none;
11 | color: #b83f45;
12 | }
13 |
14 | .learn a:hover {
15 | text-decoration: underline;
16 | color: #787e7e;
17 | }
18 |
19 | .learn h3,
20 | .learn h4,
21 | .learn h5 {
22 | margin: 10px 0;
23 | font-weight: 500;
24 | line-height: 1.2;
25 | color: #000;
26 | }
27 |
28 | .learn h3 {
29 | font-size: 24px;
30 | }
31 |
32 | .learn h4 {
33 | font-size: 18px;
34 | }
35 |
36 | .learn h5 {
37 | margin-bottom: 0;
38 | font-size: 14px;
39 | }
40 |
41 | .learn ul {
42 | padding: 0;
43 | margin: 0 0 30px 25px;
44 | }
45 |
46 | .learn li {
47 | line-height: 20px;
48 | }
49 |
50 | .learn p {
51 | font-size: 15px;
52 | font-weight: 300;
53 | line-height: 1.3;
54 | margin-top: 0;
55 | margin-bottom: 0;
56 | }
57 |
58 | #issue-count {
59 | display: none;
60 | }
61 |
62 | .quote {
63 | border: none;
64 | margin: 20px 0 60px 0;
65 | }
66 |
67 | .quote p {
68 | font-style: italic;
69 | }
70 |
71 | .quote p:before {
72 | content: '“';
73 | font-size: 50px;
74 | opacity: .15;
75 | position: absolute;
76 | top: -20px;
77 | left: 3px;
78 | }
79 |
80 | .quote p:after {
81 | content: '”';
82 | font-size: 50px;
83 | opacity: .15;
84 | position: absolute;
85 | bottom: -42px;
86 | right: 3px;
87 | }
88 |
89 | .quote footer {
90 | position: absolute;
91 | bottom: -40px;
92 | right: 0;
93 | }
94 |
95 | .quote footer img {
96 | border-radius: 3px;
97 | }
98 |
99 | .quote footer a {
100 | margin-left: 5px;
101 | vertical-align: middle;
102 | }
103 |
104 | .speech-bubble {
105 | position: relative;
106 | padding: 10px;
107 | background: rgba(0, 0, 0, .04);
108 | border-radius: 5px;
109 | }
110 |
111 | .speech-bubble:after {
112 | content: '';
113 | position: absolute;
114 | top: 100%;
115 | right: 30px;
116 | border: 13px solid transparent;
117 | border-top-color: rgba(0, 0, 0, .04);
118 | }
119 |
120 | .learn-bar > .learn {
121 | position: absolute;
122 | width: 272px;
123 | top: 8px;
124 | left: -300px;
125 | padding: 10px;
126 | border-radius: 5px;
127 | background-color: rgba(255, 255, 255, .6);
128 | transition-property: left;
129 | transition-duration: 500ms;
130 | }
131 |
132 | @media (min-width: 899px) {
133 | .learn-bar {
134 | width: auto;
135 | padding-left: 300px;
136 | }
137 |
138 | .learn-bar > .learn {
139 | left: 8px;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/todo/public/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | button {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | background: none;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | font-family: inherit;
15 | font-weight: inherit;
16 | color: inherit;
17 | -webkit-appearance: none;
18 | appearance: none;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | }
22 |
23 | body {
24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
25 | line-height: 1.4em;
26 | background: #f5f5f5;
27 | color: #4d4d4d;
28 | min-width: 230px;
29 | max-width: 550px;
30 | margin: 0 auto;
31 | -webkit-font-smoothing: antialiased;
32 | -moz-osx-font-smoothing: grayscale;
33 | font-weight: 300;
34 | }
35 |
36 | :focus {
37 | outline: 0;
38 | }
39 |
40 | .hidden {
41 | display: none;
42 | }
43 |
44 | .todoapp {
45 | background: #fff;
46 | margin: 130px 0 40px 0;
47 | position: relative;
48 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
49 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
50 | }
51 |
52 | .todoapp input::-webkit-input-placeholder {
53 | font-style: italic;
54 | font-weight: 300;
55 | color: #e6e6e6;
56 | }
57 |
58 | .todoapp input::-moz-placeholder {
59 | font-style: italic;
60 | font-weight: 300;
61 | color: #e6e6e6;
62 | }
63 |
64 | .todoapp input::input-placeholder {
65 | font-style: italic;
66 | font-weight: 300;
67 | color: #e6e6e6;
68 | }
69 |
70 | .todoapp h1 {
71 | position: absolute;
72 | top: -155px;
73 | width: 100%;
74 | font-size: 100px;
75 | font-weight: 100;
76 | text-align: center;
77 | color: rgba(175, 47, 47, 0.15);
78 | -webkit-text-rendering: optimizeLegibility;
79 | -moz-text-rendering: optimizeLegibility;
80 | text-rendering: optimizeLegibility;
81 | }
82 |
83 | .new-todo,
84 | .edit {
85 | position: relative;
86 | margin: 0;
87 | width: 100%;
88 | font-size: 24px;
89 | font-family: inherit;
90 | font-weight: inherit;
91 | line-height: 1.4em;
92 | border: 0;
93 | color: inherit;
94 | padding: 6px;
95 | border: 1px solid #999;
96 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
97 | box-sizing: border-box;
98 | -webkit-font-smoothing: antialiased;
99 | -moz-osx-font-smoothing: grayscale;
100 | }
101 |
102 | .new-todo {
103 | padding: 16px 16px 16px 60px;
104 | border: none;
105 | background: rgba(0, 0, 0, 0.003);
106 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
107 | }
108 |
109 | .main {
110 | position: relative;
111 | z-index: 2;
112 | border-top: 1px solid #e6e6e6;
113 | }
114 |
115 | label[for='toggle-all'] {
116 | display: none;
117 | }
118 |
119 | .toggle-all {
120 | position: absolute;
121 | top: -55px;
122 | left: -12px;
123 | width: 60px;
124 | height: 34px;
125 | text-align: center;
126 | border: none; /* Mobile Safari */
127 | }
128 |
129 | .toggle-all:before {
130 | content: '❯';
131 | font-size: 22px;
132 | color: #e6e6e6;
133 | padding: 10px 27px 10px 27px;
134 | }
135 |
136 | .toggle-all:checked:before {
137 | color: #737373;
138 | }
139 |
140 | .todo-list {
141 | margin: 0;
142 | padding: 0;
143 | list-style: none;
144 | }
145 |
146 | .todo-list li {
147 | position: relative;
148 | font-size: 24px;
149 | border-bottom: 1px solid #ededed;
150 | }
151 |
152 | .todo-list li:last-child {
153 | border-bottom: none;
154 | }
155 |
156 | .todo-list li.editing {
157 | border-bottom: none;
158 | padding: 0;
159 | }
160 |
161 | .todo-list li.editing .edit {
162 | display: block;
163 | width: 506px;
164 | padding: 12px 16px;
165 | margin: 0 0 0 43px;
166 | }
167 |
168 | .todo-list li.editing .view {
169 | display: none;
170 | }
171 |
172 | .todo-list li .toggle {
173 | text-align: center;
174 | width: 40px;
175 | /* auto, since non-WebKit browsers doesn't support input styling */
176 | height: auto;
177 | position: absolute;
178 | top: 0;
179 | bottom: 0;
180 | margin: auto 0;
181 | border: none; /* Mobile Safari */
182 | -webkit-appearance: none;
183 | appearance: none;
184 | }
185 |
186 | .todo-list li .toggle:after {
187 | content: url('data:image/svg+xml;utf8, ');
188 | }
189 |
190 | .todo-list li .toggle:checked:after {
191 | content: url('data:image/svg+xml;utf8, ');
192 | }
193 |
194 | .todo-list li label {
195 | word-break: break-all;
196 | padding: 15px 60px 15px 15px;
197 | margin-left: 45px;
198 | display: block;
199 | line-height: 1.2;
200 | transition: color 0.4s;
201 | }
202 |
203 | .todo-list li.completed label {
204 | color: #d9d9d9;
205 | text-decoration: line-through;
206 | }
207 |
208 | .todo-list li .destroy {
209 | display: none;
210 | position: absolute;
211 | top: 0;
212 | right: 10px;
213 | bottom: 0;
214 | width: 40px;
215 | height: 40px;
216 | margin: auto 0;
217 | font-size: 30px;
218 | color: #cc9a9a;
219 | margin-bottom: 11px;
220 | transition: color 0.2s ease-out;
221 | }
222 |
223 | .todo-list li .destroy:hover {
224 | color: #af5b5e;
225 | }
226 |
227 | .todo-list li .destroy:after {
228 | content: '×';
229 | }
230 |
231 | .todo-list li:hover .destroy {
232 | display: block;
233 | }
234 |
235 | .todo-list li .edit {
236 | display: none;
237 | }
238 |
239 | .todo-list li.editing:last-child {
240 | margin-bottom: -1px;
241 | }
242 |
243 | .footer {
244 | color: #777;
245 | padding: 10px 15px;
246 | height: 20px;
247 | text-align: center;
248 | border-top: 1px solid #e6e6e6;
249 | }
250 |
251 | .footer:before {
252 | content: '';
253 | position: absolute;
254 | right: 0;
255 | bottom: 0;
256 | left: 0;
257 | height: 50px;
258 | overflow: hidden;
259 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
260 | 0 8px 0 -3px #f6f6f6,
261 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
262 | 0 16px 0 -6px #f6f6f6,
263 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
264 | }
265 |
266 | .todo-count {
267 | float: left;
268 | text-align: left;
269 | }
270 |
271 | .todo-count strong {
272 | font-weight: 300;
273 | }
274 |
275 | .filters {
276 | margin: 0;
277 | padding: 0;
278 | list-style: none;
279 | position: absolute;
280 | right: 0;
281 | left: 0;
282 | }
283 |
284 | .filters li {
285 | display: inline;
286 | }
287 |
288 | .filters li a {
289 | color: inherit;
290 | margin: 3px;
291 | padding: 3px 7px;
292 | text-decoration: none;
293 | border: 1px solid transparent;
294 | border-radius: 3px;
295 | }
296 |
297 | .filters li a:hover {
298 | border-color: rgba(175, 47, 47, 0.1);
299 | }
300 |
301 | .filters li a.selected {
302 | border-color: rgba(175, 47, 47, 0.2);
303 | }
304 |
305 | .clear-completed,
306 | html .clear-completed:active {
307 | float: right;
308 | position: relative;
309 | line-height: 20px;
310 | text-decoration: none;
311 | cursor: pointer;
312 | }
313 |
314 | .clear-completed:hover {
315 | text-decoration: underline;
316 | }
317 |
318 | .info {
319 | margin: 65px auto 0;
320 | color: #bfbfbf;
321 | font-size: 10px;
322 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
323 | text-align: center;
324 | }
325 |
326 | .info p {
327 | line-height: 1;
328 | }
329 |
330 | .info a {
331 | color: inherit;
332 | text-decoration: none;
333 | font-weight: 400;
334 | }
335 |
336 | .info a:hover {
337 | text-decoration: underline;
338 | }
339 |
340 | /*
341 | Hack to remove background from Mobile Safari.
342 | Can't use it globally since it destroys checkboxes in Firefox
343 | */
344 | @media screen and (-webkit-min-device-pixel-ratio:0) {
345 | .toggle-all,
346 | .todo-list li .toggle {
347 | background: none;
348 | }
349 |
350 | .todo-list li .toggle {
351 | height: 40px;
352 | }
353 |
354 | .toggle-all {
355 | -webkit-transform: rotate(90deg);
356 | transform: rotate(90deg);
357 | -webkit-appearance: none;
358 | appearance: none;
359 | }
360 | }
361 |
362 | @media (max-width: 430px) {
363 | .footer {
364 | height: 50px;
365 | }
366 |
367 | .filters {
368 | bottom: 10px;
369 | }
370 | }
371 |
--------------------------------------------------------------------------------
/todo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Relay • TodoMVC
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/todo/public/learn.json:
--------------------------------------------------------------------------------
1 | {
2 | "relay": {
3 | "name": "Relay",
4 | "description": "A JavaScript framework for building data-driven React applications",
5 | "homepage": "facebook.github.io/relay/",
6 | "examples": [
7 | {
8 | "name": "Relay + express-graphql Example",
9 | "url": "",
10 | "source_url": "https://github.com/relayjs/relay-examples/tree/master/todo",
11 | "type": "backend"
12 | }
13 | ],
14 | "link_groups": [
15 | {
16 | "heading": "Official Resources",
17 | "links": [
18 | {
19 | "name": "Documentation",
20 | "url": "https://facebook.github.io/relay/docs/getting-started.html"
21 | },
22 | {
23 | "name": "API Reference",
24 | "url": "https://facebook.github.io/relay/docs/api-reference-relay.html"
25 | },
26 | {
27 | "name": "Relay on GitHub",
28 | "url": "https://github.com/facebook/relay"
29 | }
30 | ]
31 | },
32 | {
33 | "heading": "Community",
34 | "links": [
35 | {
36 | "name": "Relay on StackOverflow",
37 | "url": "https://stackoverflow.com/questions/tagged/relayjs"
38 | }
39 | ]
40 | }
41 | ]
42 | },
43 | "templates": {
44 | "todomvc": " <%= description %>
<% if (typeof link_groups !== 'undefined') { %> <% link_groups.forEach(function (link_group) { %> <%= link_group.heading %> <% }); %> <% } %> If you have other helpful links to share, or find any of the links above no longer work, please let us know . "
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/todo/scripts/updateSchema.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env babel-node
2 | /**
3 | * This file provided by Facebook is for non-commercial testing and evaluation
4 | * purposes only. Facebook reserves all rights not expressly granted.
5 | *
6 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
7 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
8 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
9 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
10 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
11 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12 | */
13 |
14 | import fs from 'fs';
15 | import path from 'path';
16 | import { schema } from '../data/schema';
17 | import { printSchema } from 'graphql';
18 |
19 | const schemaPath = path.resolve(__dirname, '../data/schema.graphql');
20 |
21 | fs.writeFileSync(schemaPath, printSchema(schema));
22 |
23 | console.log('Wrote ' + schemaPath);
24 |
--------------------------------------------------------------------------------
/todo/server.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import express from "express"
14 | import graphQLHTTP from "express-graphql"
15 | import path from "path"
16 | import webpack from "webpack"
17 | import WebpackDevServer from "webpack-dev-server"
18 | import { schema } from "./data/schema"
19 | import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"
20 | import cors from "cors"
21 |
22 | const APP_PORT = 3000
23 | const GRAPHQL_PORT = 8080
24 |
25 | // Expose a GraphQL endpoint
26 | const graphQLServer = express()
27 | graphQLServer.use(cors())
28 | graphQLServer.use("/", graphQLHTTP({ schema, pretty: true }))
29 | graphQLServer.listen(GRAPHQL_PORT, () =>
30 | console.log(
31 | `GraphQL Server is now running on http://localhost:${GRAPHQL_PORT}`,
32 | ),
33 | )
34 |
35 | // Serve the Relay app
36 | const compiler = webpack({
37 | entry: ["whatwg-fetch", path.resolve(__dirname, "ts", "app.tsx")],
38 | resolve: {
39 | extensions: [".ts", ".tsx", ".js"],
40 | },
41 | module: {
42 | rules: [
43 | {
44 | test: /\.tsx?$/,
45 | exclude: /node_modules/,
46 | use: [
47 | {
48 | loader: "babel-loader",
49 | },
50 | { loader: "ts-loader", options: { transpileOnly: true } },
51 | ],
52 | },
53 | ],
54 | },
55 | plugins: [
56 | new ForkTsCheckerWebpackPlugin(),
57 | new webpack.ProvidePlugin({
58 | React: "react",
59 | }),
60 | ],
61 | output: { filename: "app.js", path: "/" },
62 | })
63 | const app = new WebpackDevServer(compiler, {
64 | contentBase: "/public/",
65 | proxy: { "/graphql": `http://localhost:${GRAPHQL_PORT}` },
66 | publicPath: "/js/",
67 | stats: { colors: true },
68 | })
69 | // Serve static resources
70 | app.use("/", express.static(path.resolve(__dirname, "public")))
71 | app.listen(APP_PORT, () => {
72 | console.log(`App is now running on http://localhost:${APP_PORT}`)
73 | })
74 |
--------------------------------------------------------------------------------
/todo/ts/ErrorBoundaryWithRetry.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | interface Props {
4 | fallback: (error: Error, retryFn: () => void) => JSX.Element
5 | }
6 | interface State {
7 | error: Error | null
8 | }
9 |
10 | class ErrorBoundaryWithRetry extends React.Component {
11 | state = { error: null }
12 |
13 | // @ts-ignore
14 | static getDerivedStateFromError(error) {
15 | return { error: error }
16 | }
17 |
18 | _retry = () => {
19 | this.setState({ error: null })
20 | }
21 |
22 | render() {
23 | const { children, fallback } = this.props
24 | const { error } = this.state
25 |
26 | if (error) {
27 | if (typeof fallback === "function") {
28 | return fallback(error!, this._retry)
29 | }
30 | return fallback
31 | }
32 | return children
33 | }
34 | }
35 |
36 | export default ErrorBoundaryWithRetry
37 |
--------------------------------------------------------------------------------
/todo/ts/__relay_artifacts__/AddTodoMutation.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ConcreteRequest } from "relay-runtime";
4 | export type AddTodoInput = {
5 | readonly text: string;
6 | readonly clientMutationId?: string | null;
7 | };
8 | export type AddTodoMutationVariables = {
9 | input: AddTodoInput;
10 | };
11 | export type AddTodoMutationResponse = {
12 | readonly addTodo: {
13 | readonly todoEdge: {
14 | readonly __typename: string;
15 | readonly cursor: string;
16 | readonly node: {
17 | readonly complete: boolean | null;
18 | readonly id: string;
19 | readonly text: string | null;
20 | } | null;
21 | } | null;
22 | readonly viewer: {
23 | readonly id: string;
24 | readonly totalCount: number | null;
25 | } | null;
26 | } | null;
27 | };
28 | export type AddTodoMutation = {
29 | readonly response: AddTodoMutationResponse;
30 | readonly variables: AddTodoMutationVariables;
31 | };
32 |
33 |
34 |
35 | /*
36 | mutation AddTodoMutation(
37 | $input: AddTodoInput!
38 | ) {
39 | addTodo(input: $input) {
40 | todoEdge {
41 | __typename
42 | cursor
43 | node {
44 | complete
45 | id
46 | text
47 | }
48 | }
49 | viewer {
50 | id
51 | totalCount
52 | }
53 | }
54 | }
55 | */
56 |
57 | const node: ConcreteRequest = (function(){
58 | var v0 = [
59 | {
60 | "kind": "LocalArgument",
61 | "name": "input",
62 | "type": "AddTodoInput!",
63 | "defaultValue": null
64 | }
65 | ],
66 | v1 = {
67 | "kind": "ScalarField",
68 | "alias": null,
69 | "name": "id",
70 | "args": null,
71 | "storageKey": null
72 | },
73 | v2 = [
74 | {
75 | "kind": "LinkedField",
76 | "alias": null,
77 | "name": "addTodo",
78 | "storageKey": null,
79 | "args": [
80 | {
81 | "kind": "Variable",
82 | "name": "input",
83 | "variableName": "input"
84 | }
85 | ],
86 | "concreteType": "AddTodoPayload",
87 | "plural": false,
88 | "selections": [
89 | {
90 | "kind": "LinkedField",
91 | "alias": null,
92 | "name": "todoEdge",
93 | "storageKey": null,
94 | "args": null,
95 | "concreteType": "TodoEdge",
96 | "plural": false,
97 | "selections": [
98 | {
99 | "kind": "ScalarField",
100 | "alias": null,
101 | "name": "__typename",
102 | "args": null,
103 | "storageKey": null
104 | },
105 | {
106 | "kind": "ScalarField",
107 | "alias": null,
108 | "name": "cursor",
109 | "args": null,
110 | "storageKey": null
111 | },
112 | {
113 | "kind": "LinkedField",
114 | "alias": null,
115 | "name": "node",
116 | "storageKey": null,
117 | "args": null,
118 | "concreteType": "Todo",
119 | "plural": false,
120 | "selections": [
121 | {
122 | "kind": "ScalarField",
123 | "alias": null,
124 | "name": "complete",
125 | "args": null,
126 | "storageKey": null
127 | },
128 | (v1/*: any*/),
129 | {
130 | "kind": "ScalarField",
131 | "alias": null,
132 | "name": "text",
133 | "args": null,
134 | "storageKey": null
135 | }
136 | ]
137 | }
138 | ]
139 | },
140 | {
141 | "kind": "LinkedField",
142 | "alias": null,
143 | "name": "viewer",
144 | "storageKey": null,
145 | "args": null,
146 | "concreteType": "User",
147 | "plural": false,
148 | "selections": [
149 | (v1/*: any*/),
150 | {
151 | "kind": "ScalarField",
152 | "alias": null,
153 | "name": "totalCount",
154 | "args": null,
155 | "storageKey": null
156 | }
157 | ]
158 | }
159 | ]
160 | }
161 | ];
162 | return {
163 | "kind": "Request",
164 | "fragment": {
165 | "kind": "Fragment",
166 | "name": "AddTodoMutation",
167 | "type": "Mutation",
168 | "metadata": null,
169 | "argumentDefinitions": (v0/*: any*/),
170 | "selections": (v2/*: any*/)
171 | },
172 | "operation": {
173 | "kind": "Operation",
174 | "name": "AddTodoMutation",
175 | "argumentDefinitions": (v0/*: any*/),
176 | "selections": (v2/*: any*/)
177 | },
178 | "params": {
179 | "operationKind": "mutation",
180 | "name": "AddTodoMutation",
181 | "id": null,
182 | "text": "mutation AddTodoMutation(\n $input: AddTodoInput!\n) {\n addTodo(input: $input) {\n todoEdge {\n __typename\n cursor\n node {\n complete\n id\n text\n }\n }\n viewer {\n id\n totalCount\n }\n }\n}\n",
183 | "metadata": {}
184 | }
185 | };
186 | })();
187 | (node as any).hash = 'd83bc4fc4eabb0be2a540834905e9427';
188 | export default node;
189 |
--------------------------------------------------------------------------------
/todo/ts/__relay_artifacts__/ChangeTodoStatusMutation.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ConcreteRequest } from "relay-runtime";
4 | export type ChangeTodoStatusInput = {
5 | readonly complete: boolean;
6 | readonly id: string;
7 | readonly clientMutationId?: string | null;
8 | };
9 | export type ChangeTodoStatusMutationVariables = {
10 | input: ChangeTodoStatusInput;
11 | };
12 | export type ChangeTodoStatusMutationResponse = {
13 | readonly changeTodoStatus: {
14 | readonly todo: {
15 | readonly id: string;
16 | readonly complete: boolean | null;
17 | } | null;
18 | readonly viewer: {
19 | readonly id: string;
20 | readonly completedCount: number | null;
21 | } | null;
22 | } | null;
23 | };
24 | export type ChangeTodoStatusMutation = {
25 | readonly response: ChangeTodoStatusMutationResponse;
26 | readonly variables: ChangeTodoStatusMutationVariables;
27 | };
28 |
29 |
30 |
31 | /*
32 | mutation ChangeTodoStatusMutation(
33 | $input: ChangeTodoStatusInput!
34 | ) {
35 | changeTodoStatus(input: $input) {
36 | todo {
37 | id
38 | complete
39 | }
40 | viewer {
41 | id
42 | completedCount
43 | }
44 | }
45 | }
46 | */
47 |
48 | const node: ConcreteRequest = (function(){
49 | var v0 = [
50 | {
51 | "kind": "LocalArgument",
52 | "name": "input",
53 | "type": "ChangeTodoStatusInput!",
54 | "defaultValue": null
55 | }
56 | ],
57 | v1 = {
58 | "kind": "ScalarField",
59 | "alias": null,
60 | "name": "id",
61 | "args": null,
62 | "storageKey": null
63 | },
64 | v2 = [
65 | {
66 | "kind": "LinkedField",
67 | "alias": null,
68 | "name": "changeTodoStatus",
69 | "storageKey": null,
70 | "args": [
71 | {
72 | "kind": "Variable",
73 | "name": "input",
74 | "variableName": "input"
75 | }
76 | ],
77 | "concreteType": "ChangeTodoStatusPayload",
78 | "plural": false,
79 | "selections": [
80 | {
81 | "kind": "LinkedField",
82 | "alias": null,
83 | "name": "todo",
84 | "storageKey": null,
85 | "args": null,
86 | "concreteType": "Todo",
87 | "plural": false,
88 | "selections": [
89 | (v1/*: any*/),
90 | {
91 | "kind": "ScalarField",
92 | "alias": null,
93 | "name": "complete",
94 | "args": null,
95 | "storageKey": null
96 | }
97 | ]
98 | },
99 | {
100 | "kind": "LinkedField",
101 | "alias": null,
102 | "name": "viewer",
103 | "storageKey": null,
104 | "args": null,
105 | "concreteType": "User",
106 | "plural": false,
107 | "selections": [
108 | (v1/*: any*/),
109 | {
110 | "kind": "ScalarField",
111 | "alias": null,
112 | "name": "completedCount",
113 | "args": null,
114 | "storageKey": null
115 | }
116 | ]
117 | }
118 | ]
119 | }
120 | ];
121 | return {
122 | "kind": "Request",
123 | "fragment": {
124 | "kind": "Fragment",
125 | "name": "ChangeTodoStatusMutation",
126 | "type": "Mutation",
127 | "metadata": null,
128 | "argumentDefinitions": (v0/*: any*/),
129 | "selections": (v2/*: any*/)
130 | },
131 | "operation": {
132 | "kind": "Operation",
133 | "name": "ChangeTodoStatusMutation",
134 | "argumentDefinitions": (v0/*: any*/),
135 | "selections": (v2/*: any*/)
136 | },
137 | "params": {
138 | "operationKind": "mutation",
139 | "name": "ChangeTodoStatusMutation",
140 | "id": null,
141 | "text": "mutation ChangeTodoStatusMutation(\n $input: ChangeTodoStatusInput!\n) {\n changeTodoStatus(input: $input) {\n todo {\n id\n complete\n }\n viewer {\n id\n completedCount\n }\n }\n}\n",
142 | "metadata": {}
143 | }
144 | };
145 | })();
146 | (node as any).hash = '82df4993530f2c7019c4cb7382a187fa';
147 | export default node;
148 |
--------------------------------------------------------------------------------
/todo/ts/__relay_artifacts__/MarkAllTodosMutation.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ConcreteRequest } from "relay-runtime";
4 | export type MarkAllTodosInput = {
5 | readonly complete: boolean;
6 | readonly clientMutationId?: string | null;
7 | };
8 | export type MarkAllTodosMutationVariables = {
9 | input: MarkAllTodosInput;
10 | };
11 | export type MarkAllTodosMutationResponse = {
12 | readonly markAllTodos: {
13 | readonly changedTodos: ReadonlyArray<{
14 | readonly id: string;
15 | readonly complete: boolean | null;
16 | } | null> | null;
17 | readonly viewer: {
18 | readonly id: string;
19 | readonly completedCount: number | null;
20 | } | null;
21 | } | null;
22 | };
23 | export type MarkAllTodosMutation = {
24 | readonly response: MarkAllTodosMutationResponse;
25 | readonly variables: MarkAllTodosMutationVariables;
26 | };
27 |
28 |
29 |
30 | /*
31 | mutation MarkAllTodosMutation(
32 | $input: MarkAllTodosInput!
33 | ) {
34 | markAllTodos(input: $input) {
35 | changedTodos {
36 | id
37 | complete
38 | }
39 | viewer {
40 | id
41 | completedCount
42 | }
43 | }
44 | }
45 | */
46 |
47 | const node: ConcreteRequest = (function(){
48 | var v0 = [
49 | {
50 | "kind": "LocalArgument",
51 | "name": "input",
52 | "type": "MarkAllTodosInput!",
53 | "defaultValue": null
54 | }
55 | ],
56 | v1 = {
57 | "kind": "ScalarField",
58 | "alias": null,
59 | "name": "id",
60 | "args": null,
61 | "storageKey": null
62 | },
63 | v2 = [
64 | {
65 | "kind": "LinkedField",
66 | "alias": null,
67 | "name": "markAllTodos",
68 | "storageKey": null,
69 | "args": [
70 | {
71 | "kind": "Variable",
72 | "name": "input",
73 | "variableName": "input"
74 | }
75 | ],
76 | "concreteType": "MarkAllTodosPayload",
77 | "plural": false,
78 | "selections": [
79 | {
80 | "kind": "LinkedField",
81 | "alias": null,
82 | "name": "changedTodos",
83 | "storageKey": null,
84 | "args": null,
85 | "concreteType": "Todo",
86 | "plural": true,
87 | "selections": [
88 | (v1/*: any*/),
89 | {
90 | "kind": "ScalarField",
91 | "alias": null,
92 | "name": "complete",
93 | "args": null,
94 | "storageKey": null
95 | }
96 | ]
97 | },
98 | {
99 | "kind": "LinkedField",
100 | "alias": null,
101 | "name": "viewer",
102 | "storageKey": null,
103 | "args": null,
104 | "concreteType": "User",
105 | "plural": false,
106 | "selections": [
107 | (v1/*: any*/),
108 | {
109 | "kind": "ScalarField",
110 | "alias": null,
111 | "name": "completedCount",
112 | "args": null,
113 | "storageKey": null
114 | }
115 | ]
116 | }
117 | ]
118 | }
119 | ];
120 | return {
121 | "kind": "Request",
122 | "fragment": {
123 | "kind": "Fragment",
124 | "name": "MarkAllTodosMutation",
125 | "type": "Mutation",
126 | "metadata": null,
127 | "argumentDefinitions": (v0/*: any*/),
128 | "selections": (v2/*: any*/)
129 | },
130 | "operation": {
131 | "kind": "Operation",
132 | "name": "MarkAllTodosMutation",
133 | "argumentDefinitions": (v0/*: any*/),
134 | "selections": (v2/*: any*/)
135 | },
136 | "params": {
137 | "operationKind": "mutation",
138 | "name": "MarkAllTodosMutation",
139 | "id": null,
140 | "text": "mutation MarkAllTodosMutation(\n $input: MarkAllTodosInput!\n) {\n markAllTodos(input: $input) {\n changedTodos {\n id\n complete\n }\n viewer {\n id\n completedCount\n }\n }\n}\n",
141 | "metadata": {}
142 | }
143 | };
144 | })();
145 | (node as any).hash = '00fd81d60a24546c792660837e3fc6bd';
146 | export default node;
147 |
--------------------------------------------------------------------------------
/todo/ts/__relay_artifacts__/RemoveCompletedTodosMutation.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ConcreteRequest } from "relay-runtime";
4 | export type RemoveCompletedTodosInput = {
5 | readonly clientMutationId?: string | null;
6 | };
7 | export type RemoveCompletedTodosMutationVariables = {
8 | input: RemoveCompletedTodosInput;
9 | };
10 | export type RemoveCompletedTodosMutationResponse = {
11 | readonly removeCompletedTodos: {
12 | readonly deletedTodoIds: ReadonlyArray | null;
13 | readonly viewer: {
14 | readonly completedCount: number | null;
15 | readonly totalCount: number | null;
16 | } | null;
17 | } | null;
18 | };
19 | export type RemoveCompletedTodosMutation = {
20 | readonly response: RemoveCompletedTodosMutationResponse;
21 | readonly variables: RemoveCompletedTodosMutationVariables;
22 | };
23 |
24 |
25 |
26 | /*
27 | mutation RemoveCompletedTodosMutation(
28 | $input: RemoveCompletedTodosInput!
29 | ) {
30 | removeCompletedTodos(input: $input) {
31 | deletedTodoIds
32 | viewer {
33 | completedCount
34 | totalCount
35 | id
36 | }
37 | }
38 | }
39 | */
40 |
41 | const node: ConcreteRequest = (function(){
42 | var v0 = [
43 | {
44 | "kind": "LocalArgument",
45 | "name": "input",
46 | "type": "RemoveCompletedTodosInput!",
47 | "defaultValue": null
48 | }
49 | ],
50 | v1 = [
51 | {
52 | "kind": "Variable",
53 | "name": "input",
54 | "variableName": "input"
55 | }
56 | ],
57 | v2 = {
58 | "kind": "ScalarField",
59 | "alias": null,
60 | "name": "deletedTodoIds",
61 | "args": null,
62 | "storageKey": null
63 | },
64 | v3 = {
65 | "kind": "ScalarField",
66 | "alias": null,
67 | "name": "completedCount",
68 | "args": null,
69 | "storageKey": null
70 | },
71 | v4 = {
72 | "kind": "ScalarField",
73 | "alias": null,
74 | "name": "totalCount",
75 | "args": null,
76 | "storageKey": null
77 | };
78 | return {
79 | "kind": "Request",
80 | "fragment": {
81 | "kind": "Fragment",
82 | "name": "RemoveCompletedTodosMutation",
83 | "type": "Mutation",
84 | "metadata": null,
85 | "argumentDefinitions": (v0/*: any*/),
86 | "selections": [
87 | {
88 | "kind": "LinkedField",
89 | "alias": null,
90 | "name": "removeCompletedTodos",
91 | "storageKey": null,
92 | "args": (v1/*: any*/),
93 | "concreteType": "RemoveCompletedTodosPayload",
94 | "plural": false,
95 | "selections": [
96 | (v2/*: any*/),
97 | {
98 | "kind": "LinkedField",
99 | "alias": null,
100 | "name": "viewer",
101 | "storageKey": null,
102 | "args": null,
103 | "concreteType": "User",
104 | "plural": false,
105 | "selections": [
106 | (v3/*: any*/),
107 | (v4/*: any*/)
108 | ]
109 | }
110 | ]
111 | }
112 | ]
113 | },
114 | "operation": {
115 | "kind": "Operation",
116 | "name": "RemoveCompletedTodosMutation",
117 | "argumentDefinitions": (v0/*: any*/),
118 | "selections": [
119 | {
120 | "kind": "LinkedField",
121 | "alias": null,
122 | "name": "removeCompletedTodos",
123 | "storageKey": null,
124 | "args": (v1/*: any*/),
125 | "concreteType": "RemoveCompletedTodosPayload",
126 | "plural": false,
127 | "selections": [
128 | (v2/*: any*/),
129 | {
130 | "kind": "LinkedField",
131 | "alias": null,
132 | "name": "viewer",
133 | "storageKey": null,
134 | "args": null,
135 | "concreteType": "User",
136 | "plural": false,
137 | "selections": [
138 | (v3/*: any*/),
139 | (v4/*: any*/),
140 | {
141 | "kind": "ScalarField",
142 | "alias": null,
143 | "name": "id",
144 | "args": null,
145 | "storageKey": null
146 | }
147 | ]
148 | }
149 | ]
150 | }
151 | ]
152 | },
153 | "params": {
154 | "operationKind": "mutation",
155 | "name": "RemoveCompletedTodosMutation",
156 | "id": null,
157 | "text": "mutation RemoveCompletedTodosMutation(\n $input: RemoveCompletedTodosInput!\n) {\n removeCompletedTodos(input: $input) {\n deletedTodoIds\n viewer {\n completedCount\n totalCount\n id\n }\n }\n}\n",
158 | "metadata": {}
159 | }
160 | };
161 | })();
162 | (node as any).hash = '303799d791e6e233861ee011ff3bdbb8';
163 | export default node;
164 |
--------------------------------------------------------------------------------
/todo/ts/__relay_artifacts__/RemoveTodoMutation.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ConcreteRequest } from "relay-runtime";
4 | export type RemoveTodoInput = {
5 | readonly id: string;
6 | readonly clientMutationId?: string | null;
7 | };
8 | export type RemoveTodoMutationVariables = {
9 | input: RemoveTodoInput;
10 | };
11 | export type RemoveTodoMutationResponse = {
12 | readonly removeTodo: {
13 | readonly deletedTodoId: string | null;
14 | readonly viewer: {
15 | readonly completedCount: number | null;
16 | readonly totalCount: number | null;
17 | } | null;
18 | } | null;
19 | };
20 | export type RemoveTodoMutation = {
21 | readonly response: RemoveTodoMutationResponse;
22 | readonly variables: RemoveTodoMutationVariables;
23 | };
24 |
25 |
26 |
27 | /*
28 | mutation RemoveTodoMutation(
29 | $input: RemoveTodoInput!
30 | ) {
31 | removeTodo(input: $input) {
32 | deletedTodoId
33 | viewer {
34 | completedCount
35 | totalCount
36 | id
37 | }
38 | }
39 | }
40 | */
41 |
42 | const node: ConcreteRequest = (function(){
43 | var v0 = [
44 | {
45 | "kind": "LocalArgument",
46 | "name": "input",
47 | "type": "RemoveTodoInput!",
48 | "defaultValue": null
49 | }
50 | ],
51 | v1 = [
52 | {
53 | "kind": "Variable",
54 | "name": "input",
55 | "variableName": "input"
56 | }
57 | ],
58 | v2 = {
59 | "kind": "ScalarField",
60 | "alias": null,
61 | "name": "deletedTodoId",
62 | "args": null,
63 | "storageKey": null
64 | },
65 | v3 = {
66 | "kind": "ScalarField",
67 | "alias": null,
68 | "name": "completedCount",
69 | "args": null,
70 | "storageKey": null
71 | },
72 | v4 = {
73 | "kind": "ScalarField",
74 | "alias": null,
75 | "name": "totalCount",
76 | "args": null,
77 | "storageKey": null
78 | };
79 | return {
80 | "kind": "Request",
81 | "fragment": {
82 | "kind": "Fragment",
83 | "name": "RemoveTodoMutation",
84 | "type": "Mutation",
85 | "metadata": null,
86 | "argumentDefinitions": (v0/*: any*/),
87 | "selections": [
88 | {
89 | "kind": "LinkedField",
90 | "alias": null,
91 | "name": "removeTodo",
92 | "storageKey": null,
93 | "args": (v1/*: any*/),
94 | "concreteType": "RemoveTodoPayload",
95 | "plural": false,
96 | "selections": [
97 | (v2/*: any*/),
98 | {
99 | "kind": "LinkedField",
100 | "alias": null,
101 | "name": "viewer",
102 | "storageKey": null,
103 | "args": null,
104 | "concreteType": "User",
105 | "plural": false,
106 | "selections": [
107 | (v3/*: any*/),
108 | (v4/*: any*/)
109 | ]
110 | }
111 | ]
112 | }
113 | ]
114 | },
115 | "operation": {
116 | "kind": "Operation",
117 | "name": "RemoveTodoMutation",
118 | "argumentDefinitions": (v0/*: any*/),
119 | "selections": [
120 | {
121 | "kind": "LinkedField",
122 | "alias": null,
123 | "name": "removeTodo",
124 | "storageKey": null,
125 | "args": (v1/*: any*/),
126 | "concreteType": "RemoveTodoPayload",
127 | "plural": false,
128 | "selections": [
129 | (v2/*: any*/),
130 | {
131 | "kind": "LinkedField",
132 | "alias": null,
133 | "name": "viewer",
134 | "storageKey": null,
135 | "args": null,
136 | "concreteType": "User",
137 | "plural": false,
138 | "selections": [
139 | (v3/*: any*/),
140 | (v4/*: any*/),
141 | {
142 | "kind": "ScalarField",
143 | "alias": null,
144 | "name": "id",
145 | "args": null,
146 | "storageKey": null
147 | }
148 | ]
149 | }
150 | ]
151 | }
152 | ]
153 | },
154 | "params": {
155 | "operationKind": "mutation",
156 | "name": "RemoveTodoMutation",
157 | "id": null,
158 | "text": "mutation RemoveTodoMutation(\n $input: RemoveTodoInput!\n) {\n removeTodo(input: $input) {\n deletedTodoId\n viewer {\n completedCount\n totalCount\n id\n }\n }\n}\n",
159 | "metadata": {}
160 | }
161 | };
162 | })();
163 | (node as any).hash = '560d32d6f18b4072042cf217a41beb97';
164 | export default node;
165 |
--------------------------------------------------------------------------------
/todo/ts/__relay_artifacts__/RenameTodoMutation.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ConcreteRequest } from "relay-runtime";
4 | export type RenameTodoInput = {
5 | readonly id: string;
6 | readonly text: string;
7 | readonly clientMutationId?: string | null;
8 | };
9 | export type RenameTodoMutationVariables = {
10 | input: RenameTodoInput;
11 | };
12 | export type RenameTodoMutationResponse = {
13 | readonly renameTodo: {
14 | readonly todo: {
15 | readonly id: string;
16 | readonly text: string | null;
17 | } | null;
18 | } | null;
19 | };
20 | export type RenameTodoMutation = {
21 | readonly response: RenameTodoMutationResponse;
22 | readonly variables: RenameTodoMutationVariables;
23 | };
24 |
25 |
26 |
27 | /*
28 | mutation RenameTodoMutation(
29 | $input: RenameTodoInput!
30 | ) {
31 | renameTodo(input: $input) {
32 | todo {
33 | id
34 | text
35 | }
36 | }
37 | }
38 | */
39 |
40 | const node: ConcreteRequest = (function(){
41 | var v0 = [
42 | {
43 | "kind": "LocalArgument",
44 | "name": "input",
45 | "type": "RenameTodoInput!",
46 | "defaultValue": null
47 | }
48 | ],
49 | v1 = [
50 | {
51 | "kind": "LinkedField",
52 | "alias": null,
53 | "name": "renameTodo",
54 | "storageKey": null,
55 | "args": [
56 | {
57 | "kind": "Variable",
58 | "name": "input",
59 | "variableName": "input"
60 | }
61 | ],
62 | "concreteType": "RenameTodoPayload",
63 | "plural": false,
64 | "selections": [
65 | {
66 | "kind": "LinkedField",
67 | "alias": null,
68 | "name": "todo",
69 | "storageKey": null,
70 | "args": null,
71 | "concreteType": "Todo",
72 | "plural": false,
73 | "selections": [
74 | {
75 | "kind": "ScalarField",
76 | "alias": null,
77 | "name": "id",
78 | "args": null,
79 | "storageKey": null
80 | },
81 | {
82 | "kind": "ScalarField",
83 | "alias": null,
84 | "name": "text",
85 | "args": null,
86 | "storageKey": null
87 | }
88 | ]
89 | }
90 | ]
91 | }
92 | ];
93 | return {
94 | "kind": "Request",
95 | "fragment": {
96 | "kind": "Fragment",
97 | "name": "RenameTodoMutation",
98 | "type": "Mutation",
99 | "metadata": null,
100 | "argumentDefinitions": (v0/*: any*/),
101 | "selections": (v1/*: any*/)
102 | },
103 | "operation": {
104 | "kind": "Operation",
105 | "name": "RenameTodoMutation",
106 | "argumentDefinitions": (v0/*: any*/),
107 | "selections": (v1/*: any*/)
108 | },
109 | "params": {
110 | "operationKind": "mutation",
111 | "name": "RenameTodoMutation",
112 | "id": null,
113 | "text": "mutation RenameTodoMutation(\n $input: RenameTodoInput!\n) {\n renameTodo(input: $input) {\n todo {\n id\n text\n }\n }\n}\n",
114 | "metadata": {}
115 | }
116 | };
117 | })();
118 | (node as any).hash = 'de4aa1639055c2e6a78ee22cce29870a';
119 | export default node;
120 |
--------------------------------------------------------------------------------
/todo/ts/__relay_artifacts__/TodoApp_viewer.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ReaderFragment } from "relay-runtime";
4 | import { FragmentRefs } from "relay-runtime";
5 | export type TodoApp_viewer = {
6 | readonly id: string;
7 | readonly totalCount: number | null;
8 | readonly " $fragmentRefs": FragmentRefs<"TodoListFooter_viewer" | "TodoList_viewer">;
9 | readonly " $refType": "TodoApp_viewer";
10 | };
11 | export type TodoApp_viewer$data = TodoApp_viewer;
12 | export type TodoApp_viewer$key = {
13 | readonly " $data"?: TodoApp_viewer$data;
14 | readonly " $fragmentRefs": FragmentRefs<"TodoApp_viewer">;
15 | };
16 |
17 |
18 |
19 | const node: ReaderFragment = {
20 | "kind": "Fragment",
21 | "name": "TodoApp_viewer",
22 | "type": "User",
23 | "metadata": null,
24 | "argumentDefinitions": [],
25 | "selections": [
26 | {
27 | "kind": "ScalarField",
28 | "alias": null,
29 | "name": "id",
30 | "args": null,
31 | "storageKey": null
32 | },
33 | {
34 | "kind": "ScalarField",
35 | "alias": null,
36 | "name": "totalCount",
37 | "args": null,
38 | "storageKey": null
39 | },
40 | {
41 | "kind": "FragmentSpread",
42 | "name": "TodoListFooter_viewer",
43 | "args": null
44 | },
45 | {
46 | "kind": "FragmentSpread",
47 | "name": "TodoList_viewer",
48 | "args": null
49 | }
50 | ]
51 | };
52 | (node as any).hash = 'b9743417c7b5ef2bbda96cf675aa9eb4';
53 | export default node;
54 |
--------------------------------------------------------------------------------
/todo/ts/__relay_artifacts__/TodoListFooter_viewer.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ReaderFragment } from "relay-runtime";
4 | import { FragmentRefs } from "relay-runtime";
5 | export type TodoListFooter_viewer = {
6 | readonly id: string;
7 | readonly completedCount: number | null;
8 | readonly completedTodos: {
9 | readonly edges: ReadonlyArray<{
10 | readonly node: {
11 | readonly id: string;
12 | readonly complete: boolean | null;
13 | } | null;
14 | } | null> | null;
15 | } | null;
16 | readonly totalCount: number | null;
17 | readonly " $refType": "TodoListFooter_viewer";
18 | };
19 | export type TodoListFooter_viewer$data = TodoListFooter_viewer;
20 | export type TodoListFooter_viewer$key = {
21 | readonly " $data"?: TodoListFooter_viewer$data;
22 | readonly " $fragmentRefs": FragmentRefs<"TodoListFooter_viewer">;
23 | };
24 |
25 |
26 |
27 | const node: ReaderFragment = (function(){
28 | var v0 = {
29 | "kind": "ScalarField",
30 | "alias": null,
31 | "name": "id",
32 | "args": null,
33 | "storageKey": null
34 | };
35 | return {
36 | "kind": "Fragment",
37 | "name": "TodoListFooter_viewer",
38 | "type": "User",
39 | "metadata": null,
40 | "argumentDefinitions": [],
41 | "selections": [
42 | (v0/*: any*/),
43 | {
44 | "kind": "ScalarField",
45 | "alias": null,
46 | "name": "completedCount",
47 | "args": null,
48 | "storageKey": null
49 | },
50 | {
51 | "kind": "LinkedField",
52 | "alias": "completedTodos",
53 | "name": "todos",
54 | "storageKey": "todos(first:2147483647,status:\"completed\")",
55 | "args": [
56 | {
57 | "kind": "Literal",
58 | "name": "first",
59 | "value": 2147483647
60 | },
61 | {
62 | "kind": "Literal",
63 | "name": "status",
64 | "value": "completed"
65 | }
66 | ],
67 | "concreteType": "TodoConnection",
68 | "plural": false,
69 | "selections": [
70 | {
71 | "kind": "LinkedField",
72 | "alias": null,
73 | "name": "edges",
74 | "storageKey": null,
75 | "args": null,
76 | "concreteType": "TodoEdge",
77 | "plural": true,
78 | "selections": [
79 | {
80 | "kind": "LinkedField",
81 | "alias": null,
82 | "name": "node",
83 | "storageKey": null,
84 | "args": null,
85 | "concreteType": "Todo",
86 | "plural": false,
87 | "selections": [
88 | (v0/*: any*/),
89 | {
90 | "kind": "ScalarField",
91 | "alias": null,
92 | "name": "complete",
93 | "args": null,
94 | "storageKey": null
95 | }
96 | ]
97 | }
98 | ]
99 | }
100 | ]
101 | },
102 | {
103 | "kind": "ScalarField",
104 | "alias": null,
105 | "name": "totalCount",
106 | "args": null,
107 | "storageKey": null
108 | }
109 | ]
110 | };
111 | })();
112 | (node as any).hash = '2490c58e1768d71f3824c1facd127033';
113 | export default node;
114 |
--------------------------------------------------------------------------------
/todo/ts/__relay_artifacts__/TodoList_viewer.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ReaderFragment } from "relay-runtime";
4 | import { FragmentRefs } from "relay-runtime";
5 | export type TodoList_viewer = {
6 | readonly todos: {
7 | readonly edges: ReadonlyArray<{
8 | readonly node: {
9 | readonly id: string;
10 | readonly complete: boolean | null;
11 | readonly " $fragmentRefs": FragmentRefs<"Todo_todo">;
12 | } | null;
13 | } | null> | null;
14 | } | null;
15 | readonly id: string;
16 | readonly totalCount: number | null;
17 | readonly completedCount: number | null;
18 | readonly " $fragmentRefs": FragmentRefs<"Todo_viewer">;
19 | readonly " $refType": "TodoList_viewer";
20 | };
21 | export type TodoList_viewer$data = TodoList_viewer;
22 | export type TodoList_viewer$key = {
23 | readonly " $data"?: TodoList_viewer$data;
24 | readonly " $fragmentRefs": FragmentRefs<"TodoList_viewer">;
25 | };
26 |
27 |
28 |
29 | const node: ReaderFragment = (function(){
30 | var v0 = {
31 | "kind": "ScalarField",
32 | "alias": null,
33 | "name": "id",
34 | "args": null,
35 | "storageKey": null
36 | };
37 | return {
38 | "kind": "Fragment",
39 | "name": "TodoList_viewer",
40 | "type": "User",
41 | "metadata": {
42 | "connection": [
43 | {
44 | "count": null,
45 | "cursor": null,
46 | "direction": "forward",
47 | "path": [
48 | "todos"
49 | ]
50 | }
51 | ]
52 | },
53 | "argumentDefinitions": [],
54 | "selections": [
55 | {
56 | "kind": "LinkedField",
57 | "alias": "todos",
58 | "name": "__TodoList_todos_connection",
59 | "storageKey": null,
60 | "args": null,
61 | "concreteType": "TodoConnection",
62 | "plural": false,
63 | "selections": [
64 | {
65 | "kind": "LinkedField",
66 | "alias": null,
67 | "name": "edges",
68 | "storageKey": null,
69 | "args": null,
70 | "concreteType": "TodoEdge",
71 | "plural": true,
72 | "selections": [
73 | {
74 | "kind": "LinkedField",
75 | "alias": null,
76 | "name": "node",
77 | "storageKey": null,
78 | "args": null,
79 | "concreteType": "Todo",
80 | "plural": false,
81 | "selections": [
82 | (v0/*: any*/),
83 | {
84 | "kind": "ScalarField",
85 | "alias": null,
86 | "name": "complete",
87 | "args": null,
88 | "storageKey": null
89 | },
90 | {
91 | "kind": "ScalarField",
92 | "alias": null,
93 | "name": "__typename",
94 | "args": null,
95 | "storageKey": null
96 | },
97 | {
98 | "kind": "FragmentSpread",
99 | "name": "Todo_todo",
100 | "args": null
101 | }
102 | ]
103 | },
104 | {
105 | "kind": "ScalarField",
106 | "alias": null,
107 | "name": "cursor",
108 | "args": null,
109 | "storageKey": null
110 | }
111 | ]
112 | },
113 | {
114 | "kind": "LinkedField",
115 | "alias": null,
116 | "name": "pageInfo",
117 | "storageKey": null,
118 | "args": null,
119 | "concreteType": "PageInfo",
120 | "plural": false,
121 | "selections": [
122 | {
123 | "kind": "ScalarField",
124 | "alias": null,
125 | "name": "endCursor",
126 | "args": null,
127 | "storageKey": null
128 | },
129 | {
130 | "kind": "ScalarField",
131 | "alias": null,
132 | "name": "hasNextPage",
133 | "args": null,
134 | "storageKey": null
135 | }
136 | ]
137 | }
138 | ]
139 | },
140 | (v0/*: any*/),
141 | {
142 | "kind": "ScalarField",
143 | "alias": null,
144 | "name": "totalCount",
145 | "args": null,
146 | "storageKey": null
147 | },
148 | {
149 | "kind": "ScalarField",
150 | "alias": null,
151 | "name": "completedCount",
152 | "args": null,
153 | "storageKey": null
154 | },
155 | {
156 | "kind": "FragmentSpread",
157 | "name": "Todo_viewer",
158 | "args": null
159 | }
160 | ]
161 | };
162 | })();
163 | (node as any).hash = 'a3e4165e516c834231092d435f5dd81c';
164 | export default node;
165 |
--------------------------------------------------------------------------------
/todo/ts/__relay_artifacts__/Todo_todo.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ReaderFragment } from "relay-runtime";
4 | import { FragmentRefs } from "relay-runtime";
5 | export type Todo_todo = {
6 | readonly complete: boolean | null;
7 | readonly id: string;
8 | readonly text: string | null;
9 | readonly " $refType": "Todo_todo";
10 | };
11 | export type Todo_todo$data = Todo_todo;
12 | export type Todo_todo$key = {
13 | readonly " $data"?: Todo_todo$data;
14 | readonly " $fragmentRefs": FragmentRefs<"Todo_todo">;
15 | };
16 |
17 |
18 |
19 | const node: ReaderFragment = {
20 | "kind": "Fragment",
21 | "name": "Todo_todo",
22 | "type": "Todo",
23 | "metadata": null,
24 | "argumentDefinitions": [],
25 | "selections": [
26 | {
27 | "kind": "ScalarField",
28 | "alias": null,
29 | "name": "complete",
30 | "args": null,
31 | "storageKey": null
32 | },
33 | {
34 | "kind": "ScalarField",
35 | "alias": null,
36 | "name": "id",
37 | "args": null,
38 | "storageKey": null
39 | },
40 | {
41 | "kind": "ScalarField",
42 | "alias": null,
43 | "name": "text",
44 | "args": null,
45 | "storageKey": null
46 | }
47 | ]
48 | };
49 | (node as any).hash = '1f979eb84ff026fe8a89323dd533d1fc';
50 | export default node;
51 |
--------------------------------------------------------------------------------
/todo/ts/__relay_artifacts__/Todo_viewer.graphql.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable */
2 |
3 | import { ReaderFragment } from "relay-runtime";
4 | import { FragmentRefs } from "relay-runtime";
5 | export type Todo_viewer = {
6 | readonly id: string;
7 | readonly totalCount: number | null;
8 | readonly completedCount: number | null;
9 | readonly " $refType": "Todo_viewer";
10 | };
11 | export type Todo_viewer$data = Todo_viewer;
12 | export type Todo_viewer$key = {
13 | readonly " $data"?: Todo_viewer$data;
14 | readonly " $fragmentRefs": FragmentRefs<"Todo_viewer">;
15 | };
16 |
17 |
18 |
19 | const node: ReaderFragment = {
20 | "kind": "Fragment",
21 | "name": "Todo_viewer",
22 | "type": "User",
23 | "metadata": null,
24 | "argumentDefinitions": [],
25 | "selections": [
26 | {
27 | "kind": "ScalarField",
28 | "alias": null,
29 | "name": "id",
30 | "args": null,
31 | "storageKey": null
32 | },
33 | {
34 | "kind": "ScalarField",
35 | "alias": null,
36 | "name": "totalCount",
37 | "args": null,
38 | "storageKey": null
39 | },
40 | {
41 | "kind": "ScalarField",
42 | "alias": null,
43 | "name": "completedCount",
44 | "args": null,
45 | "storageKey": null
46 | }
47 | ]
48 | };
49 | (node as any).hash = '1e2b17bb7b92d4521c4e72309d996339';
50 | export default node;
51 |
--------------------------------------------------------------------------------
/todo/ts/app.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import * as React from "react"
14 | import ReactDOM from "react-dom"
15 |
16 | import "todomvc-common"
17 |
18 | import { RelayEnvironmentProvider } from "react-relay/hooks"
19 | import { Environment, Network, RecordSource, Store } from "relay-runtime"
20 |
21 | import TodoRoot from "./components/TodoRoot"
22 |
23 | const mountNode = document.getElementById("root")
24 |
25 | function fetchQuery(operation: any, variables: any) {
26 | return fetch("http://localhost:8080/graphql", {
27 | method: "POST",
28 | headers: {
29 | "Content-Type": "application/json",
30 | },
31 | body: JSON.stringify({
32 | query: operation.text,
33 | variables,
34 | }),
35 | }).then(response => {
36 | return response.json()
37 | })
38 | }
39 |
40 | const modernEnvironment = new Environment({
41 | network: Network.create(fetchQuery),
42 | store: new Store(new RecordSource()),
43 | })
44 |
45 | ReactDOM.render(
46 |
47 |
48 | ,
49 | mountNode,
50 | )
51 |
--------------------------------------------------------------------------------
/todo/ts/components/Todo.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 | import React, { ChangeEvent } from "react"
13 | import classnames from "classnames"
14 |
15 | import ChangeTodoStatusMutation from "../mutations/ChangeTodoStatusMutation"
16 | import RemoveTodoMutation from "../mutations/RemoveTodoMutation"
17 | import RenameTodoMutation from "../mutations/RenameTodoMutation"
18 | import TodoTextInput from "./TodoTextInput"
19 |
20 | import { graphql, useRelayEnvironment, useFragment } from "react-relay/hooks"
21 |
22 | import { Todo_todo$key } from "../__relay_artifacts__/Todo_todo.graphql"
23 | import { Todo_viewer$key } from "../__relay_artifacts__/Todo_viewer.graphql"
24 |
25 | interface Props {
26 | todo: Todo_todo$key
27 | viewer: Todo_viewer$key
28 | }
29 |
30 | const Todo = (props: Props) => {
31 | const [isEditing, setIsEditing] = React.useState(false)
32 |
33 | const environment = useRelayEnvironment()
34 |
35 | const todo = useFragment(
36 | graphql`
37 | fragment Todo_todo on Todo {
38 | complete
39 | id
40 | text
41 | }
42 | `,
43 | props.todo,
44 | )
45 |
46 | const viewer = useFragment(
47 | graphql`
48 | fragment Todo_viewer on User {
49 | id
50 | totalCount
51 | completedCount
52 | }
53 | `,
54 | props.viewer,
55 | )
56 |
57 | const handleCompleteChange = (e: ChangeEvent) => {
58 | const complete = e.target.checked
59 | ChangeTodoStatusMutation.commit(environment, complete, todo, viewer)
60 | }
61 | const handleDestroyClick = () => {
62 | removeTodo()
63 | }
64 | const handleLabelDoubleClick = () => {
65 | setIsEditing(true)
66 | }
67 | const handleTextInputCancel = () => {
68 | setIsEditing(false)
69 | }
70 | const handleTextInputDelete = () => {
71 | setIsEditing(false)
72 | removeTodo()
73 | }
74 | const handleTextInputSave = (text: string) => {
75 | setIsEditing(false)
76 | RenameTodoMutation.commit(environment, text, todo)
77 | }
78 | function removeTodo() {
79 | RemoveTodoMutation.commit(environment, todo, viewer)
80 | }
81 |
82 | function renderTextInput() {
83 | return (
84 |
92 | )
93 | }
94 |
95 | return (
96 |
102 |
103 |
109 | {todo.text}
110 |
111 |
112 | {isEditing && renderTextInput()}
113 |
114 | )
115 | }
116 |
117 | export default Todo
118 |
--------------------------------------------------------------------------------
/todo/ts/components/TodoApp.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import React from "react"
14 | import { graphql, useFragment, useRelayEnvironment } from "react-relay/hooks"
15 |
16 | import AddTodoMutation from "../mutations/AddTodoMutation"
17 | import TodoList from "./TodoList"
18 | import TodoListFooter from "./TodoListFooter"
19 | import TodoTextInput from "./TodoTextInput"
20 |
21 | import { TodoApp_viewer$key } from "../__relay_artifacts__/TodoApp_viewer.graphql"
22 |
23 | interface Props {
24 | viewer: TodoApp_viewer$key
25 | }
26 |
27 | const TodoApp = (props: Props) => {
28 | const environment = useRelayEnvironment()
29 |
30 | const viewer = useFragment(
31 | graphql`
32 | fragment TodoApp_viewer on User {
33 | id
34 | totalCount
35 | ...TodoListFooter_viewer
36 | ...TodoList_viewer
37 | }
38 | `,
39 | props.viewer,
40 | )
41 |
42 | const handleTextInputSave = (text: string) => {
43 | AddTodoMutation.commit(environment, text, viewer)
44 | }
45 |
46 | const hasTodos = (viewer.totalCount || 0) > 0
47 |
48 | return (
49 |
50 |
51 |
60 |
61 | {hasTodos && }
62 |
63 |
64 | Double-click to edit a todo
65 |
66 | Created by the{" "}
67 | Relay team
68 |
69 |
70 | Part of TodoMVC
71 |
72 |
73 |
74 | )
75 | }
76 |
77 | export default TodoApp
78 |
--------------------------------------------------------------------------------
/todo/ts/components/TodoList.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import React, { ChangeEvent } from "react"
14 | import { graphql, useFragment, useRelayEnvironment } from "react-relay/hooks"
15 |
16 | import MarkAllTodosMutation from "../mutations/MarkAllTodosMutation"
17 | import Todo from "./Todo"
18 |
19 | import { TodoList_viewer$key } from "../__relay_artifacts__/TodoList_viewer.graphql"
20 |
21 | interface Props {
22 | viewer: TodoList_viewer$key
23 | }
24 |
25 | const TodoList = (props: Props) => {
26 | const environment = useRelayEnvironment()
27 |
28 | const viewer = useFragment(
29 | graphql`
30 | fragment TodoList_viewer on User {
31 | todos(
32 | first: 2147483647 # max GraphQLInt
33 | ) @connection(key: "TodoList_todos") {
34 | edges {
35 | node {
36 | id
37 | complete
38 | ...Todo_todo
39 | }
40 | }
41 | }
42 | id
43 | totalCount
44 | completedCount
45 | ...Todo_viewer
46 | }
47 | `,
48 | props.viewer,
49 | )
50 |
51 | const numTodos = viewer.totalCount
52 | const numCompletedTodos = viewer.completedCount
53 |
54 | const handleMarkAllChange = (e: ChangeEvent) => {
55 | const complete = e.target.checked
56 | MarkAllTodosMutation.commit(environment, complete, viewer.todos, viewer)
57 | }
58 |
59 | const renderTodos = () => {
60 | if (!viewer.todos || !viewer.todos.edges) {
61 | throw new Error("assertion failed")
62 | }
63 | return viewer.todos.edges.map(edge => {
64 | const node = edge && edge.node
65 | if (!node) throw new Error("assertion failed")
66 | return
67 | })
68 | }
69 |
70 | return (
71 |
81 | )
82 | }
83 |
84 | export default TodoList
85 |
--------------------------------------------------------------------------------
/todo/ts/components/TodoListFooter.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import React from "react"
14 | import { graphql, useRelayEnvironment, useFragment } from "react-relay/hooks"
15 |
16 | import RemoveCompletedTodosMutation from "../mutations/RemoveCompletedTodosMutation"
17 |
18 | import { TodoListFooter_viewer$key } from "../__relay_artifacts__/TodoListFooter_viewer.graphql"
19 |
20 | interface Props {
21 | viewer: TodoListFooter_viewer$key
22 | }
23 |
24 | const TodoListFooter = (props: Props) => {
25 | const environment = useRelayEnvironment()
26 |
27 | const viewer = useFragment(
28 | graphql`
29 | fragment TodoListFooter_viewer on User {
30 | id
31 | completedCount
32 | completedTodos: todos(
33 | status: "completed"
34 | first: 2147483647 # max GraphQLInt
35 | ) {
36 | edges {
37 | node {
38 | id
39 | complete
40 | }
41 | }
42 | }
43 | totalCount
44 | }
45 | `,
46 | props.viewer,
47 | )
48 |
49 | const numCompletedTodos = viewer.completedCount || 0
50 | const numRemainingTodos = (viewer.totalCount || 0) - numCompletedTodos
51 |
52 | const handleRemoveCompletedTodosClick = () => {
53 | RemoveCompletedTodosMutation.commit(
54 | environment,
55 | viewer.completedTodos,
56 | viewer,
57 | )
58 | }
59 |
60 | return (
61 |
62 |
63 | {numRemainingTodos} item
64 | {numRemainingTodos === 1 ? "" : "s"} left
65 |
66 | {numCompletedTodos > 0 && (
67 |
71 | Clear completed
72 |
73 | )}
74 |
75 | )
76 | }
77 |
78 | export default TodoListFooter
79 |
--------------------------------------------------------------------------------
/todo/ts/components/TodoRoot.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import React from "react"
14 | import { useLazyLoadQuery, graphql } from "react-relay/hooks"
15 |
16 | import { TodoRootQuery } from "../__relay_artifacts__/TodoRootQuery.graphql"
17 |
18 | import TodoApp from "./TodoApp"
19 |
20 | const TodoRoot = () => {
21 | const { viewer } = useLazyLoadQuery(
22 | graphql`
23 | query TodoRootQuery {
24 | viewer {
25 | ...TodoApp_viewer
26 | }
27 | }
28 | `,
29 | {},
30 | )
31 |
32 | return
33 | }
34 |
35 | const TodoRootWrapper = () => {
36 | return (
37 | Loading}>
38 |
39 |
40 | )
41 | }
42 |
43 | export default TodoRootWrapper
44 |
--------------------------------------------------------------------------------
/todo/ts/components/TodoTextInput.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import React, { ChangeEvent } from "react"
14 | import ReactDOM from "react-dom"
15 |
16 | const ENTER_KEY_CODE = 13
17 | const ESC_KEY_CODE = 27
18 |
19 | interface Props extends React.HTMLProps {
20 | className?: string
21 | commitOnBlur?: boolean
22 | initialValue?: string | null
23 | onDelete?: () => void
24 | onCancel?: () => void
25 | onSave: (text: string) => void
26 | placeholder?: string
27 | }
28 |
29 | export default class TodoTextInput extends React.Component {
30 | static defaultProps = {
31 | commitOnBlur: false,
32 | }
33 | state = {
34 | isEditing: false,
35 | text: this.props.initialValue || "",
36 | }
37 | componentDidMount() {
38 | const element = ReactDOM.findDOMNode(this) as HTMLElement
39 | element.focus()
40 | }
41 | _commitChanges = () => {
42 | const newText = this.state.text.trim()
43 | if (this.props.onDelete && newText === "") {
44 | this.props.onDelete()
45 | } else if (this.props.onCancel && newText === this.props.initialValue) {
46 | this.props.onCancel()
47 | } else if (newText !== "") {
48 | this.props.onSave(newText)
49 | this.setState({ text: "" })
50 | }
51 | }
52 | _handleBlur = () => {
53 | if (this.props.commitOnBlur) {
54 | this._commitChanges()
55 | }
56 | }
57 | _handleChange = (e: ChangeEvent) => {
58 | this.setState({ text: (e.target as HTMLInputElement).value })
59 | }
60 | // FIXME: KeyboardEvent in the global.d.ts file that ships with react.d.ts is not generic
61 | // _handleKeyDown = (e: KeyboardEvent) => {
62 | _handleKeyDown = (e: any) => {
63 | if (this.props.onCancel && e.keyCode === ESC_KEY_CODE) {
64 | this.props.onCancel()
65 | } else if (e.keyCode === ENTER_KEY_CODE) {
66 | this._commitChanges()
67 | }
68 | }
69 | render() {
70 | return (
71 |
79 | )
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/todo/ts/definitions.d.ts:
--------------------------------------------------------------------------------
1 | declare module "relay-devtools" {
2 | export function installRelayDevTools(): void
3 | }
4 |
5 | // FIXME: The @types/classnames typings say there is no default export, which is incorrect
6 | declare module "classnames" {
7 | export default function classnames(classes: any): string
8 | }
9 |
--------------------------------------------------------------------------------
/todo/ts/mutations/AddTodoMutation.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { commitMutation, graphql } from "react-relay"
14 | import {
15 | ConnectionHandler,
16 | RecordSourceSelectorProxy,
17 | Environment,
18 | } from "relay-runtime"
19 |
20 | import { TodoApp_viewer } from "../__relay_artifacts__/TodoApp_viewer.graphql"
21 | import { AddTodoMutation } from "../__relay_artifacts__/AddTodoMutation.graphql"
22 |
23 | const mutation = graphql`
24 | mutation AddTodoMutation($input: AddTodoInput!) {
25 | addTodo(input: $input) {
26 | todoEdge {
27 | __typename
28 | cursor
29 | node {
30 | complete
31 | id
32 | text
33 | }
34 | }
35 | viewer {
36 | id
37 | totalCount
38 | }
39 | }
40 | }
41 | `
42 |
43 | function sharedUpdater(
44 | store: RecordSourceSelectorProxy,
45 | user: TodoApp_viewer,
46 | newEdge: any,
47 | ) {
48 | const userProxy = store.get(user.id)
49 | const conn = ConnectionHandler.getConnection(userProxy!, "TodoList_todos")
50 | ConnectionHandler.insertEdgeAfter(conn!, newEdge)
51 | }
52 |
53 | let tempID = 0
54 |
55 | function commit(environment: Environment, text: string, user: TodoApp_viewer) {
56 | return commitMutation(environment, {
57 | mutation,
58 | variables: {
59 | input: {
60 | text,
61 | clientMutationId: (tempID++).toString(),
62 | },
63 | },
64 | updater: store => {
65 | const payload = store.getRootField("addTodo")
66 | if (!payload) throw new Error("assertion failed")
67 | const newEdge = payload.getLinkedRecord("todoEdge")
68 | sharedUpdater(store, user, newEdge)
69 | },
70 | optimisticUpdater: store => {
71 | const id = "client:newTodo:" + tempID++
72 | const node = store.create(id, "Todo")
73 | node.setValue(text, "text")
74 | node.setValue(id, "id")
75 | const newEdge = store.create("client:newEdge:" + tempID++, "TodoEdge")
76 | newEdge.setLinkedRecord(node, "node")
77 | sharedUpdater(store, user, newEdge)
78 | const userProxy = store.get(user.id)
79 | if (!userProxy) throw new Error("assertion failed")
80 | userProxy.setValue(
81 | (userProxy.getValue("totalCount") as number) + 1,
82 | "totalCount",
83 | )
84 | },
85 | })
86 | }
87 |
88 | export default { commit }
89 |
--------------------------------------------------------------------------------
/todo/ts/mutations/ChangeTodoStatusMutation.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { commitMutation, graphql } from "react-relay"
14 | import { Environment } from "relay-runtime"
15 |
16 | import { Todo_todo } from "../__relay_artifacts__/Todo_todo.graphql"
17 | import { Todo_viewer } from "../__relay_artifacts__/Todo_viewer.graphql"
18 | import { ChangeTodoStatusMutation } from "../__relay_artifacts__/ChangeTodoStatusMutation.graphql"
19 |
20 | const mutation = graphql`
21 | mutation ChangeTodoStatusMutation($input: ChangeTodoStatusInput!) {
22 | changeTodoStatus(input: $input) {
23 | todo {
24 | id
25 | complete
26 | }
27 | viewer {
28 | id
29 | completedCount
30 | }
31 | }
32 | }
33 | `
34 |
35 | function getOptimisticResponse(
36 | complete: boolean,
37 | todo: Todo_todo,
38 | user: Todo_viewer,
39 | ) {
40 | const userPayload: { id: string; completedCount: number } = {
41 | id: user.id,
42 | completedCount: 0,
43 | }
44 | if (user.completedCount != null) {
45 | userPayload.completedCount = complete
46 | ? user.completedCount + 1
47 | : user.completedCount - 1
48 | }
49 | return {
50 | changeTodoStatus: {
51 | todo: {
52 | complete: complete,
53 | id: todo.id,
54 | },
55 | viewer: userPayload,
56 | },
57 | }
58 | }
59 |
60 | function commit(
61 | environment: Environment,
62 | complete: boolean,
63 | todo: Todo_todo,
64 | user: Todo_viewer,
65 | ) {
66 | return commitMutation(environment, {
67 | mutation,
68 | variables: {
69 | input: { complete, id: todo.id },
70 | },
71 | optimisticResponse: getOptimisticResponse(complete, todo, user),
72 | })
73 | }
74 |
75 | export default { commit }
76 |
--------------------------------------------------------------------------------
/todo/ts/mutations/MarkAllTodosMutation.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { commitMutation, graphql } from "react-relay"
14 |
15 | import { TodoList_viewer } from "../__relay_artifacts__/TodoList_viewer.graphql"
16 | import { Environment } from "relay-runtime"
17 | import { MarkAllTodosMutation } from "../__relay_artifacts__/MarkAllTodosMutation.graphql"
18 |
19 | const mutation = graphql`
20 | mutation MarkAllTodosMutation($input: MarkAllTodosInput!) {
21 | markAllTodos(input: $input) {
22 | changedTodos {
23 | id
24 | complete
25 | }
26 | viewer {
27 | id
28 | completedCount
29 | }
30 | }
31 | }
32 | `
33 |
34 | function getOptimisticResponse(
35 | complete: boolean,
36 | todos: TodoList_viewer["todos"],
37 | user: TodoList_viewer,
38 | ) {
39 | const payload: any = { user: { id: user.id } }
40 | if (todos && todos.edges) {
41 | payload.changedTodos = todos.edges
42 | .filter(edge => edge && edge.node && edge.node.complete !== complete)
43 | .map(edge => ({
44 | complete: complete,
45 | id: edge && edge.node && edge.node.id,
46 | }))
47 | }
48 | if (user.totalCount != null) {
49 | payload.user.completedCount = complete ? user.totalCount : 0
50 | }
51 | return {
52 | markAllTodos: payload,
53 | }
54 | }
55 |
56 | function commit(
57 | environment: Environment,
58 | complete: boolean,
59 | todos: TodoList_viewer["todos"],
60 | user: TodoList_viewer,
61 | ) {
62 | return commitMutation(environment, {
63 | mutation,
64 | variables: {
65 | input: { complete },
66 | },
67 | optimisticResponse: getOptimisticResponse(complete, todos, user),
68 | })
69 | }
70 |
71 | export default { commit }
72 |
--------------------------------------------------------------------------------
/todo/ts/mutations/RemoveCompletedTodosMutation.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { commitMutation, graphql } from "react-relay"
14 | import {
15 | ConnectionHandler,
16 | Environment,
17 | DataID,
18 | RecordSourceSelectorProxy,
19 | } from "relay-runtime"
20 |
21 | import { TodoListFooter_viewer } from "../__relay_artifacts__/TodoListFooter_viewer.graphql"
22 | import { RemoveCompletedTodosMutation } from "../__relay_artifacts__/RemoveCompletedTodosMutation.graphql"
23 |
24 | const mutation = graphql`
25 | mutation RemoveCompletedTodosMutation($input: RemoveCompletedTodosInput!) {
26 | removeCompletedTodos(input: $input) {
27 | deletedTodoIds
28 | viewer {
29 | completedCount
30 | totalCount
31 | }
32 | }
33 | }
34 | `
35 |
36 | function sharedUpdater(
37 | store: RecordSourceSelectorProxy,
38 | user: TodoListFooter_viewer,
39 | deletedIDs: string[],
40 | ) {
41 | const userProxy = store.get(user.id)
42 | const conn = ConnectionHandler.getConnection(userProxy!, "TodoList_todos")
43 | deletedIDs.forEach(deletedID =>
44 | ConnectionHandler.deleteNode(conn!, deletedID),
45 | )
46 | }
47 |
48 | function commit(
49 | environment: Environment,
50 | todos: TodoListFooter_viewer["completedTodos"],
51 | user: TodoListFooter_viewer,
52 | ) {
53 | return commitMutation(environment, {
54 | mutation,
55 | variables: {
56 | input: {},
57 | },
58 | updater: store => {
59 | const payload = store.getRootField("removeCompletedTodos")
60 | if (!payload) throw new Error("assertion failed")
61 | sharedUpdater(store, user, payload.getValue("deletedTodoIds") as string[])
62 | },
63 | optimisticUpdater: store => {
64 | if (todos && todos.edges) {
65 | const deletedIDs = todos.edges
66 | .filter(edge => edge && edge.node && edge.node.complete)
67 | .map(edge => (edge && edge.node && edge.node.id) as string)
68 | sharedUpdater(store, user, deletedIDs)
69 | }
70 | },
71 | })
72 | }
73 |
74 | export default { commit }
75 |
--------------------------------------------------------------------------------
/todo/ts/mutations/RemoveTodoMutation.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { commitMutation, graphql } from "react-relay"
14 | import {
15 | ConnectionHandler,
16 | Environment,
17 | RecordSourceSelectorProxy,
18 | } from "relay-runtime"
19 |
20 | import { Todo_todo } from "../__relay_artifacts__/Todo_todo.graphql"
21 | import { Todo_viewer } from "../__relay_artifacts__/Todo_viewer.graphql"
22 | import { RemoveTodoMutation } from "../__relay_artifacts__/RemoveTodoMutation.graphql"
23 |
24 | const mutation = graphql`
25 | mutation RemoveTodoMutation($input: RemoveTodoInput!) {
26 | removeTodo(input: $input) {
27 | deletedTodoId
28 | viewer {
29 | completedCount
30 | totalCount
31 | }
32 | }
33 | }
34 | `
35 |
36 | function sharedUpdater(
37 | store: RecordSourceSelectorProxy,
38 | user: Todo_viewer,
39 | deletedID: string,
40 | ) {
41 | const userProxy = store.get(user.id)
42 | const conn = ConnectionHandler.getConnection(userProxy!, "TodoList_todos")
43 | ConnectionHandler.deleteNode(conn!, deletedID)
44 | }
45 |
46 | function commit(environment: Environment, todo: Todo_todo, user: Todo_viewer) {
47 | return commitMutation(environment, {
48 | mutation,
49 | variables: {
50 | input: { id: todo.id },
51 | },
52 | updater: store => {
53 | const payload = store.getRootField("removeTodo")
54 | if (!payload) throw new Error("assertion failed")
55 | sharedUpdater(store, user, payload.getValue("deletedTodoId") as string)
56 | },
57 | optimisticUpdater: store => {
58 | sharedUpdater(store, user, todo.id)
59 | },
60 | })
61 | }
62 |
63 | export default { commit }
64 |
--------------------------------------------------------------------------------
/todo/ts/mutations/RenameTodoMutation.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file provided by Facebook is for non-commercial testing and evaluation
3 | * purposes only. Facebook reserves all rights not expressly granted.
4 | *
5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | */
12 |
13 | import { commitMutation, graphql } from "react-relay"
14 | import { Environment } from "relay-runtime"
15 |
16 | import { Todo_todo } from "../__relay_artifacts__/Todo_todo.graphql"
17 | import { RenameTodoMutation } from "../__relay_artifacts__/RenameTodoMutation.graphql"
18 |
19 | const mutation = graphql`
20 | mutation RenameTodoMutation($input: RenameTodoInput!) {
21 | renameTodo(input: $input) {
22 | todo {
23 | id
24 | text
25 | }
26 | }
27 | }
28 | `
29 |
30 | function getOptimisticResponse(text: string, todo: Todo_todo) {
31 | return {
32 | renameTodo: {
33 | todo: {
34 | id: todo.id,
35 | text: text,
36 | },
37 | },
38 | }
39 | }
40 |
41 | function commit(environment: Environment, text: string, todo: Todo_todo) {
42 | return commitMutation(environment, {
43 | mutation,
44 | variables: {
45 | input: { text, id: todo.id },
46 | },
47 | optimisticResponse: getOptimisticResponse(text, todo),
48 | })
49 | }
50 |
51 | export default { commit }
52 |
--------------------------------------------------------------------------------
/todo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "preserve",
4 | "lib": ["dom", "esnext"],
5 | "module": "es2015",
6 | "noEmit": true,
7 | "moduleResolution": "node",
8 | "strict": true,
9 | "target": "es2015",
10 | "allowSyntheticDefaultImports": true
11 | },
12 | "exclude": ["node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------
/todo/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": ["tslint:recommended", "tslint-plugin-relay"],
4 | "jsRules": {},
5 | "rules": {
6 | "arrow-parens": false,
7 | "interface-name": false,
8 | "object-literal-sort-keys": false,
9 | "variable-name": false,
10 | "trailing-comma": false,
11 | "quotemark": false,
12 | "ordered-imports": false,
13 | "no-console": false,
14 | "semicolon": false,
15 | "member-access": false,
16 | "curly": false,
17 | "object-literal-shorthand": false
18 | },
19 | "rulesDirectory": []
20 | }
21 |
--------------------------------------------------------------------------------