-
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 15 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 |
|---|---|---|
|
|
@@ -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,7 +193,9 @@ 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 { | ||
| const response = await apiPut({ | ||
|
|
@@ -211,6 +226,68 @@ const ExportDialog: ExportDialogComponent = ({ | |
| return { status: 500 }; | ||
| } | ||
| }; | ||
| const writeFileToIssue = async ({ | ||
| title, | ||
| body, | ||
| setError, | ||
| }: { | ||
| title: string; | ||
| body: string; | ||
| setError: (error: string) => void; | ||
| }): Promise<{ status: number }> => { | ||
| try { | ||
| const response = await apiPost({ | ||
| domain: "https://api.github.com", | ||
| path: `repos/${selectedRepo}/issues`, | ||
| headers: { | ||
| Authorization: `token ${gitHubAccessToken}`, | ||
| }, | ||
| data: { | ||
| title, | ||
| body, | ||
| // 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, | ||
| }, | ||
| }, | ||
| }; | ||
| 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 +578,8 @@ const ExportDialog: ExportDialogComponent = ({ | |
| gitHubAccessToken={gitHubAccessToken} | ||
| setGitHubAccessToken={setGitHubAccessToken} | ||
| setCanSendToGitHub={setCanSendToGitHub} | ||
| githubDestination={githubDestination} | ||
| setGithubDestination={setGithubDestination} | ||
|
||
| /> | ||
| </div> | ||
| </div> | ||
|
|
@@ -623,11 +702,26 @@ 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({ | ||
|
|
||
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.
[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