-
Notifications
You must be signed in to change notification settings - Fork 6
New Feature: GitHub Sync #253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 20 commits
e393965
00aa560
a323e7a
bf89829
f342f36
8fc4d29
b17b1ea
79617c4
e953356
bae6519
f9f14a6
402ada7
1b2bd93
dab592a
32452f3
af67257
91f3aba
d8b5e26
8565186
c79a736
d3d7c04
82c8df9
6b6b52f
f899fee
6a356e5
6db4cbf
a2ffa74
8218df8
57b2b2f
d85d3f6
72ed156
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # GitHub Sync | ||
|
|
||
| This extension implemnents GitHub Sync, allowing you to synchronize specific pages directly from Roam to a specified GitHub repository as Issues. | ||
|
|
||
| ## Config Page | ||
|
|
||
| This will be found at `[[roam/js/github-sync]]` in your graph. | ||
|
|
||
| ### Node Select | ||
|
|
||
| Select which type of pages you want to sync to GitHub as Issues. | ||
|
|
||
| The list is made from defined [Discourse Graph Nodes](discourse-graphs.md). | ||
|
|
||
| ### Comments Block | ||
|
|
||
| This is where the comments will live. A `Add Comment` button will appear on this block as well as a download button. | ||
|
||
|
|
||
| After you add a comment, that new comment block will have a `Add to GitHub` button to send it to the issue. | ||
|
|
||
| Clicking the download button will grab any new comments and open a dialog to confirm adding them to the block. | ||
|
|
||
| **Query Block Definition** | ||
|
|
||
| Create a `{{query block}}` somewhere in your graph to define which block will be the Comments Block. | ||
|
|
||
| You can add the variables `:in NODETEXT` or `:in NODETITLE` which will grab the current pages's text or title. | ||
|
|
||
| Then add that Query Block's alias or block reference to the field. | ||
|
|
||
| Example: | ||
|
|
||
|  | ||
|
|
||
| ## Issue Page | ||
|
|
||
| ### Send To GitHub | ||
|
|
||
| When you first navigate to a defined Issue Page, you will see two buttons under the title | ||
|
|
||
|  | ||
|
|
||
| Click the Send To GitHub button to start the upload process. The process is as follows: | ||
|
|
||
| - install the SamePage GitHub App | ||
| - authorize the app to access your GitHub repository | ||
|
Comment on lines
+43
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's clarify below that these steps only need to happen once |
||
| - select the repository you want to send the issue to | ||
| - click `Export` | ||
|
|
||
| Once this is complete, the title will just show the `GitHub Sync Details Button`. Clicking this button will show additional details about the issue, include a link to the issue, a link to settings, as well as the ability to re-authorize if required. | ||
|
|
||
| ### Comments | ||
|
|
||
| Click the `Add Comment` button to add a comment to the page. Once the comment is created you should see a `Add to GitHub` button. Clicking this will add the comment to the issue. After it is sent, the `Add to GitHub` button should change to a `link` icon which will open the comment on GitHub. | ||
|
|
||
| Click the `Download Comments` button to fetch any new comments from the issue. You will see a confirmation dialog of all the comments that will be added. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,7 +45,7 @@ import { getNodeEnv } from "roamjs-components/util/env"; | |
| import apiGet from "roamjs-components/util/apiGet"; | ||
| import apiPut from "roamjs-components/util/apiPut"; | ||
| import localStorageGet from "roamjs-components/util/localStorageGet"; | ||
| import { ExportGithub } from "./ExportGithub"; | ||
| import { ExportGithub, GitHubDestination } from "./ExportGithub"; | ||
| import localStorageSet from "roamjs-components/util/localStorageSet"; | ||
|
|
||
| const ExportProgress = ({ id }: { id: string }) => { | ||
|
|
@@ -79,24 +79,28 @@ const ExportProgress = ({ id }: { id: string }) => { | |
| ); | ||
| }; | ||
|
|
||
| const EXPORT_DESTINATIONS = [ | ||
| { id: "local", label: "Download Locally", active: true }, | ||
| { id: "app", label: "Store in Roam", active: false }, | ||
| { id: "samepage", label: "Store with SamePage", active: false }, | ||
| { id: "github", label: "Send to GitHub", active: true }, | ||
| ]; | ||
|
|
||
| export type ExportDialogProps = { | ||
| results?: Result[] | ((isSamePageEnabled: boolean) => Promise<Result[]>); | ||
| title?: string; | ||
| columns?: Column[]; | ||
| isExportDiscourseGraph?: boolean; | ||
| initialPanel?: "sendTo" | "export"; | ||
| initialExportDestination?: (typeof EXPORT_DESTINATIONS)[number]["id"]; | ||
| initialGitHubDestination?: GitHubDestination; | ||
| onClose?: () => void; | ||
| }; | ||
|
|
||
| type ExportDialogComponent = ( | ||
| props: RoamOverlayProps<ExportDialogProps> | ||
| ) => JSX.Element; | ||
|
|
||
| const EXPORT_DESTINATIONS = [ | ||
| { id: "local", label: "Download Locally", active: true }, | ||
| { id: "app", label: "Store in Roam", active: false }, | ||
| { id: "samepage", label: "Store with SamePage", active: false }, | ||
| { id: "github", label: "Send to GitHub", active: true }, | ||
| ]; | ||
| const exportDestinationById = Object.fromEntries( | ||
| EXPORT_DESTINATIONS.map((ed) => [ed.id, ed]) | ||
| ); | ||
|
|
@@ -109,6 +113,8 @@ const ExportDialog: ExportDialogComponent = ({ | |
| title = "Share Data", | ||
| isExportDiscourseGraph = false, | ||
| initialPanel, | ||
| initialExportDestination, | ||
| initialGitHubDestination, | ||
| }) => { | ||
| const [selectedRepo, setSelectedRepo] = useState( | ||
| localStorageGet("selected-repo") | ||
|
|
@@ -139,7 +145,11 @@ const ExportDialog: ExportDialogComponent = ({ | |
| exportTypes[0].name | ||
| ); | ||
| const [activeExportDestination, setActiveExportDestination] = | ||
| useState<string>(EXPORT_DESTINATIONS[0].id); | ||
| useState<string>( | ||
| initialExportDestination | ||
| ? exportDestinationById[initialExportDestination].id | ||
| : EXPORT_DESTINATIONS[0].id | ||
| ); | ||
| const [isSamePageEnabled, setIsSamePageEnabled] = useState(false); | ||
|
|
||
| const checkForCanvasPage = (title: string) => { | ||
|
|
@@ -170,6 +180,9 @@ const ExportDialog: ExportDialogComponent = ({ | |
| localStorageGet("oauth-github") | ||
| ); | ||
| const [canSendToGitHub, setCanSendToGitHub] = useState(false); | ||
| const [githubDestination, setGithubDestination] = useState<GitHubDestination>( | ||
| initialGitHubDestination || "File" | ||
| ); | ||
|
|
||
| const writeFileToRepo = async ({ | ||
| filename, | ||
|
|
@@ -180,9 +193,12 @@ const ExportDialog: ExportDialogComponent = ({ | |
| content: string; | ||
| setError: (error: string) => void; | ||
| }): Promise<{ status: number }> => { | ||
| const base64Content = btoa(content); | ||
| const encoder = new TextEncoder(); | ||
| const uint8Array = encoder.encode(content); | ||
| const base64Content = btoa(String.fromCharCode(...uint8Array)); | ||
|
|
||
| try { | ||
| // https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#create-or-update-file-contents | ||
| const response = await apiPut({ | ||
| domain: "https://api.github.com", | ||
| path: `repos/${selectedRepo}/contents/${filename}`, | ||
|
|
@@ -211,6 +227,70 @@ const ExportDialog: ExportDialogComponent = ({ | |
| return { status: 500 }; | ||
| } | ||
| }; | ||
| const writeFileToIssue = async ({ | ||
| title, | ||
| body, | ||
| setError, | ||
| }: { | ||
| title: string; | ||
| body: string; | ||
| setError: (error: string) => void; | ||
| }): Promise<{ status: number }> => { | ||
| try { | ||
| // https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#create-an-issue | ||
| const response = await apiPost({ | ||
| domain: "https://api.github.com", | ||
| path: `repos/${selectedRepo}/issues`, | ||
| headers: { | ||
| Authorization: `token ${gitHubAccessToken}`, | ||
| }, | ||
| data: { | ||
| title, | ||
| body, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [oos] There will be formatting differences, like Roam bolding -> Github bolding. This is where proxying through SamePage I think could be helpful, since we are defining the AtJson standard there to be interoperable across all text editors |
||
| // milestone, | ||
| // labels, | ||
| // assignees | ||
| }, | ||
| }); | ||
| if (response.status === 401) { | ||
| setGitHubAccessToken(null); | ||
| setError("Authentication failed. Please log in again."); | ||
| localStorageSet("oauth-github", ""); | ||
| return { status: 401 }; | ||
| } | ||
|
|
||
| if (response.status === 201) { | ||
| const props = getBlockProps(currentPageUid); | ||
| const newProps = { | ||
| ...props, | ||
| ["github-sync"]: { | ||
| issue: { | ||
| id: response.id, | ||
| number: response.number, | ||
| html_url: response.html_url, | ||
| state: response.state, | ||
| labels: response.labels, | ||
| createdAt: response.created_at, | ||
| updatedAt: response.updated_at, | ||
| repo: selectedRepo, | ||
| }, | ||
| }, | ||
| }; | ||
| window.roamAlphaAPI.updateBlock({ | ||
| block: { | ||
| uid: currentPageUid, | ||
| props: newProps, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| return { status: response.status }; | ||
| } catch (error) { | ||
| const e = error as Error; | ||
| setError("Failed to create issue"); | ||
| return { status: 500 }; | ||
| } | ||
| }; | ||
|
|
||
| const handleSetSelectedPage = (title: string) => { | ||
| setSelectedPageTitle(title); | ||
|
|
@@ -501,6 +581,8 @@ const ExportDialog: ExportDialogComponent = ({ | |
| gitHubAccessToken={gitHubAccessToken} | ||
| setGitHubAccessToken={setGitHubAccessToken} | ||
| setCanSendToGitHub={setCanSendToGitHub} | ||
| githubDestination={githubDestination} | ||
| setGithubDestination={setGithubDestination} | ||
|
||
| /> | ||
| </div> | ||
| </div> | ||
|
|
@@ -623,16 +705,31 @@ const ExportDialog: ExportDialogComponent = ({ | |
| if (activeExportDestination === "github") { | ||
| const { title, content } = files[0]; | ||
| try { | ||
| const { status } = await writeFileToRepo({ | ||
| filename: title, | ||
| content, | ||
| setError, | ||
| }); | ||
| let status; | ||
| if (githubDestination === "File") { | ||
| status = ( | ||
| await writeFileToRepo({ | ||
| filename: title, | ||
| content, | ||
| setError, | ||
| }) | ||
| ).status; | ||
| } | ||
| if (githubDestination === "Issue") { | ||
| status = ( | ||
| await writeFileToIssue({ | ||
| title: title.replace(/\.[^/.]+$/, ""), // remove extension | ||
| body: content, | ||
| setError, | ||
| }) | ||
| ).status; | ||
| } | ||
|
|
||
| if (status === 201) { | ||
| // TODO: remove toast by prolonging ExportProgress | ||
| renderToast({ | ||
| id: "export-success", | ||
| content: "Upload Success", | ||
| content: `Upload Success to ${githubDestination}`, | ||
| intent: "success", | ||
| }); | ||
| onClose(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First thing that stood out to me in the video - thoughts on this living within the existing node config pages instead of introducing a new page? Couple of motivations behind this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially this felt far enough away from Discourse Graph to be on it's own. Then I ended up hooking into Discourse Nodes for their definition 😅. But I could see users want to use only this feature and none of the other dgraph features ... then it would be kind of buried / confusing.