├── .env.example ├── .gitattributes ├── .gitignore ├── .yarn └── releases │ └── yarn-3.2.0.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── bsconfig.json ├── index.html ├── package.json ├── relay.config.cjs ├── rescriptRelayRouter.config.cjs ├── src ├── Config.res ├── HomeRoot.res ├── Index.res ├── IssueActions.res ├── IssueDetailComments.res ├── IssueDetailRoot.res ├── Issues.res ├── IssuesListItem.res ├── RelayEnv.res ├── Root.res ├── Router.res ├── SuspenseImage.res ├── SuspenseImage.resi ├── __generated__ │ ├── HomeRootQuery_graphql.res │ ├── IssueActionsAddCommentMutation_graphql.res │ ├── IssueActionsCloseIssueMutation_graphql.res │ ├── IssueActionsReopenIssueMutation_graphql.res │ ├── IssueActions_issue_graphql.res │ ├── IssueDetailCommentsQuery_graphql.res │ ├── IssueDetailComments_issue_graphql.res │ ├── IssueDetailRootQuery_graphql.res │ ├── IssuesListItem_issue_graphql.res │ ├── IssuesPaginationQuery_graphql.res │ ├── Issues_repository_graphql.res │ └── RootQuery_graphql.res ├── bindings │ ├── ErrorBoundary.res │ ├── ReactExperimental.res │ └── ReactMarkdown.res ├── favicon.ico ├── index.css └── routes │ ├── Root__HomeRoot_route_renderer.res │ ├── Root__IssueDetailRoot_route_renderer.res │ ├── Root_route_renderer.res │ ├── __generated__ │ ├── RouteDeclarations.res │ ├── RouteDeclarations.resi │ ├── Route__Root__HomeRoot_route.res │ ├── Route__Root__IssueDetailRoot_route.res │ ├── Route__Root_route.res │ └── Routes.res │ └── routes.json ├── vite.config.mjs └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | VITE_GITHUB_TOKEN="" -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bs.js linguist-generated 2 | src/__generated__/* linguist-generated 3 | 4 | .yarn/** linguist-vendored 5 | .yarn/releases/* binary linguist-vendored 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.local 2 | *.log 3 | .env 4 | .bsb.lock 5 | .merlin 6 | .DS_Store 7 | 8 | node_modules/ 9 | .yarn/* 10 | !.yarn/patches 11 | !.yarn/releases 12 | !.yarn/plugins 13 | !.yarn/sdks 14 | !.yarn/versions 15 | .pnp.* 16 | 17 | lib/ 18 | dist/ 19 | dist-ssr/ 20 | *.bs.js 21 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | nmMode: hardlinks-global 3 | 4 | yarnPath: .yarn/releases/yarn-3.2.0.cjs 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hyeseong Kim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReScript Relay Issue Tracker Example 2 | 3 | [ReScript Relay](https://github.com/zth/rescript-relay) port of Relay's [official issue tracker example](https://github.com/relayjs/relay-examples/blob/main/issue-tracker) 4 | 5 | This is an all-in-one demo for 6 | - Understanding [ReScript](https://rescript-lang.org) 7 | - Understanding [Relay](https://relay.dev/) 8 | - Understanding [Concurrent React](https://reactjs.org/docs/concurrent-mode-intro.html) 9 | 10 | And also using experimental [ReScript Relay Router](https://www.npmjs.com/package/rescript-relay-router)! 11 | 12 | ## Start example 13 | 14 | 1. Create a [Personal Access Token](https://github.com/settings/tokens/new) for the GitHub API 15 | 16 | 2. Make `.env` file in the project root 17 | ```env 18 | VITE_GITHUB_TOKEN="" 19 | ``` 20 | 21 | 3. Run `yarn install` & `yarn dev` 22 | 23 | ## LICENSE 24 | 25 | MIT 26 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rescript-relay-issut-tracker", 3 | "reason": { 4 | "react-jsx": 3 5 | }, 6 | "refmt": 3, 7 | "suffix": ".bs.js", 8 | "sources": { 9 | "dir" : "src", 10 | "subdirs" : true 11 | }, 12 | "bsc-flags": [ 13 | "-bs-super-errors", 14 | "-bs-no-version-header" 15 | ], 16 | "package-specs": [ 17 | { 18 | "module": "es6", 19 | "in-source": true 20 | } 21 | ], 22 | "ppx-flags": [ 23 | "rescript-relay/ppx" 24 | ], 25 | "bs-dependencies": [ 26 | "@rescript/react", 27 | "@ryyppy/rescript-promise", 28 | "rescript-relay", 29 | "rescript-relay-router", 30 | "rescript-webapi" 31 | ], 32 | "bs-dev-dependencies": [ 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ReScript Relay Issue Tracker 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rescript-relay-issue-tracker", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "serve": "vite preview", 10 | "relay": "rescript-relay-compiler", 11 | "relay:watch": "rescript-relay-compiler --watch" 12 | }, 13 | "resolutions": { 14 | "react": "0.0.0-experimental-229c86af0-20220616", 15 | "react-dom": "0.0.0-experimental-229c86af0-20220616" 16 | }, 17 | "dependencies": { 18 | "@rescript/react": "^0.10.3", 19 | "@ryyppy/rescript-promise": "^2.1.0", 20 | "assert": "^2.0.0", 21 | "graphql": "^16.5.0", 22 | "react": "experimental", 23 | "react-dom": "experimental", 24 | "react-error-boundary": "^3.1.4", 25 | "react-markdown": "^8.0.3", 26 | "react-relay": "13.2.0", 27 | "relay-runtime": "13.2.0", 28 | "rescript": "^9.1.4", 29 | "rescript-relay": "1.0.0-beta.21", 30 | "rescript-relay-router": "^0.0.15-editor-tooling.2", 31 | "rescript-webapi": "^0.6.1" 32 | }, 33 | "devDependencies": { 34 | "@jihchi/vite-plugin-rescript": "^2.1.0", 35 | "@octokit/graphql-schema": "^10.73.0", 36 | "@vitejs/plugin-react": "^1.3.2", 37 | "vite": "^2.9.12" 38 | }, 39 | "packageManager": "yarn@3.2.0" 40 | } 41 | -------------------------------------------------------------------------------- /relay.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | src: './src', 3 | schema: require.resolve('@octokit/graphql-schema/schema.graphql'), 4 | artifactDirectory: './src/__generated__', 5 | customScalars: { 6 | Date: 'string', 7 | DateTime: 'string', 8 | GitObjectID: 'string', 9 | GitSSHRemote: 'string', 10 | GitTimestamp: 'string', 11 | HTML: 'string', 12 | PreciseDateTime: 'string', 13 | URI: 'string', 14 | X509Certificate: 'string', 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /rescriptRelayRouter.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "routesFolderPath": "./src/routes" 3 | } -------------------------------------------------------------------------------- /src/Config.res: -------------------------------------------------------------------------------- 1 | let owner = "facebook" 2 | let name = "relay" 3 | -------------------------------------------------------------------------------- /src/HomeRoot.res: -------------------------------------------------------------------------------- 1 | module HomeRootQuery = %relay(` 2 | query HomeRootQuery($owner: String!, $name: String!) { 3 | repository(owner: $owner, name: $name) { 4 | # Compose the data dependencies of child components 5 | # by spreading their fragments: 6 | ...Issues_repository 7 | } 8 | } 9 | `) 10 | 11 | @react.component 12 | let make = (~queryRef) => { 13 | let data = HomeRootQuery.usePreloaded(~queryRef, ()) 14 | 15 | switch data.repository { 16 | | Some(repository) => 17 | | None => React.null 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Index.res: -------------------------------------------------------------------------------- 1 | @module("./index.css") external _css: string = "default" 2 | 3 | ReactDOMExperimental.renderConcurrentRootAtElementWithId( 4 | 5 | 6 | 7 | 8 | React.string("Error!")}> 9 | React.string("Loading fallback...")} 11 | renderPending={pending => 12 | switch pending { 13 | | true => 14 |
15 | {React.string("Loading pending...")} 16 |
17 | | false => React.null 18 | }} 19 | /> 20 |
21 |
22 |
23 |
24 |
, 25 | "root", 26 | ) 27 | -------------------------------------------------------------------------------- /src/IssueActions.res: -------------------------------------------------------------------------------- 1 | open RescriptRelay 2 | 3 | module IssueFragment = %relay(` 4 | fragment IssueActions_issue on Issue { 5 | id 6 | closed 7 | } 8 | `) 9 | 10 | module AddCommentMutation = %relay(` 11 | mutation IssueActionsAddCommentMutation( 12 | $connections: [ID!]! 13 | $input: AddCommentInput! 14 | ) { 15 | addComment(input: $input) { 16 | subject { 17 | __typename 18 | id 19 | } 20 | commentEdge @appendEdge(connections: $connections) { 21 | __id 22 | node { 23 | id 24 | author { 25 | __typename 26 | login 27 | avatarUrl 28 | } 29 | body 30 | } 31 | } 32 | } 33 | } 34 | `) 35 | 36 | module CloseIssueMutation = %relay(` 37 | mutation IssueActionsCloseIssueMutation($input: CloseIssueInput!) { 38 | closeIssue(input: $input) { 39 | issue { 40 | ...IssueActions_issue 41 | } 42 | } 43 | } 44 | `) 45 | 46 | module ReopenIssueMutation = %relay(` 47 | mutation IssueActionsReopenIssueMutation($input: ReopenIssueInput!) { 48 | reopenIssue(input: $input) { 49 | issue { 50 | ...IssueActions_issue 51 | } 52 | } 53 | } 54 | `) 55 | 56 | @react.component 57 | let make = (~issue) => { 58 | let (_, startTransition) = ReactExperimental.useTransition() 59 | let (commentText, setCommentText) = React.useState(() => "") 60 | let isCommentEmpty = commentText->Js.String2.trim->Js.String2.length == 0 61 | 62 | let issue = IssueFragment.use(issue) 63 | 64 | let (addComment, isCommentPending) = AddCommentMutation.use() 65 | let (reopenIssue, isReopenPending) = ReopenIssueMutation.use() 66 | let (closeIssue, isClosePending) = CloseIssueMutation.use() 67 | let isPending = isCommentPending || isClosePending || isReopenPending 68 | 69 | let onChange = React.useCallback0(event => { 70 | let value = (event->ReactEvent.Form.target)["value"] 71 | startTransition(() => { 72 | setCommentText(_ => value) 73 | }) 74 | }) 75 | 76 | let onSubmit = React.useCallback3(event => { 77 | event->ReactEvent.Form.preventDefault 78 | let dataId = issue.id->makeDataId 79 | let connectionId = dataId->ConnectionHandler.getConnectionID("IssueDetailComments_comments", ()) 80 | startTransition(() => { 81 | addComment( 82 | ~variables={ 83 | connections: [connectionId], 84 | input: { 85 | clientMutationId: None, 86 | body: commentText, 87 | subjectId: issue.id, 88 | }, 89 | }, 90 | (), 91 | )->ignore 92 | setCommentText(_ => "") 93 | }) 94 | }, (issue, addComment, commentText)) 95 | 96 | let onToggleOpen = React.useCallback3(event => { 97 | event->ReactEvent.Mouse.preventDefault 98 | startTransition(() => { 99 | switch issue.closed { 100 | | true => 101 | reopenIssue( 102 | ~variables={ 103 | input: { 104 | clientMutationId: None, 105 | issueId: issue.id, 106 | }, 107 | }, 108 | (), 109 | ) 110 | | false => 111 | closeIssue( 112 | ~variables={ 113 | input: { 114 | clientMutationId: None, 115 | issueId: issue.id, 116 | }, 117 | }, 118 | (), 119 | ) 120 | }->ignore 121 | }) 122 | }, (issue, reopenIssue, closeIssue)) 123 | 124 |
125 |