Skip to content

Commit 6080afb

Browse files
committed
initial port
1 parent 6e34519 commit 6080afb

File tree

10 files changed

+1464
-129
lines changed

10 files changed

+1464
-129
lines changed

apps/roam/src/components/Export.tsx

Lines changed: 125 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -83,23 +83,26 @@ const ExportProgress = ({ id }: { id: string }) => {
8383
);
8484
};
8585

86+
const EXPORT_DESTINATIONS = [
87+
{ id: "local", label: "Download Locally", active: true },
88+
{ id: "app", label: "Store in Roam", active: false },
89+
{ id: "github", label: "Send to GitHub", active: true },
90+
];
91+
8692
export type ExportDialogProps = {
8793
results?: Result[];
8894
title?: string;
8995
columns?: Column[];
9096
isExportDiscourseGraph?: boolean;
9197
initialPanel?: "sendTo" | "export";
98+
initialExportDestination?: (typeof EXPORT_DESTINATIONS)[number]["id"];
99+
onClose?: () => void;
92100
};
93101

94102
type ExportDialogComponent = (
95103
props: RoamOverlayProps<ExportDialogProps>,
96104
) => JSX.Element;
97105

98-
const EXPORT_DESTINATIONS = [
99-
{ id: "local", label: "Download Locally", active: true },
100-
{ id: "app", label: "Store in Roam", active: false },
101-
{ id: "github", label: "Send to GitHub", active: true },
102-
];
103106
const SEND_TO_DESTINATIONS = ["page", "graph"];
104107

105108
const exportDestinationById = Object.fromEntries(
@@ -114,6 +117,7 @@ const ExportDialog: ExportDialogComponent = ({
114117
title = "Share Data",
115118
isExportDiscourseGraph = false,
116119
initialPanel,
120+
initialExportDestination,
117121
}) => {
118122
const [selectedRepo, setSelectedRepo] = useState(
119123
localStorageGet("selected-repo"),
@@ -144,7 +148,11 @@ const ExportDialog: ExportDialogComponent = ({
144148
exportTypes[0].name,
145149
);
146150
const [activeExportDestination, setActiveExportDestination] =
147-
useState<string>(EXPORT_DESTINATIONS[0].id);
151+
useState<string>(
152+
initialExportDestination
153+
? exportDestinationById[initialExportDestination].id
154+
: EXPORT_DESTINATIONS[0].id,
155+
);
148156

149157
const firstColumnKey = columns?.[0]?.key || "text";
150158
const currentPageUid = getCurrentPageUid();
@@ -168,9 +176,6 @@ const ExportDialog: ExportDialogComponent = ({
168176
const [includeDiscourseContext, setIncludeDiscourseContext] = useState(
169177
discourseGraphEnabled as boolean,
170178
);
171-
const [gitHubAccessToken, setGitHubAccessToken] = useState<string | null>(
172-
localStorageGet("oauth-github"),
173-
);
174179
const [canSendToGitHub, setCanSendToGitHub] = useState(false);
175180

176181
const writeFileToRepo = async ({
@@ -182,9 +187,15 @@ const ExportDialog: ExportDialogComponent = ({
182187
content: string;
183188
setError: (error: string) => void;
184189
}): Promise<{ status: number }> => {
185-
const base64Content = btoa(content);
190+
const gitHubAccessToken = localStorageGet("github-oauth");
191+
const selectedRepo = localStorageGet("github-repo");
192+
193+
const encoder = new TextEncoder();
194+
const uint8Array = encoder.encode(content);
195+
const base64Content = btoa(String.fromCharCode(...uint8Array));
186196

187197
try {
198+
// https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#create-or-update-file-contents
188199
const response = await apiPut({
189200
domain: "https://api.github.com",
190201
path: `repos/${selectedRepo}/contents/${filename}`,
@@ -197,9 +208,8 @@ const ExportDialog: ExportDialogComponent = ({
197208
},
198209
});
199210
if (response.status === 401) {
200-
setGitHubAccessToken(null);
201211
setError("Authentication failed. Please log in again.");
202-
localStorageSet("oauth-github", "");
212+
localStorageSet("github-oauth", "");
203213
return { status: 401 };
204214
}
205215
return { status: response.status };
@@ -214,6 +224,74 @@ const ExportDialog: ExportDialogComponent = ({
214224
}
215225
};
216226

227+
const writeFileToIssue = async ({
228+
title,
229+
body,
230+
setError,
231+
pageUid,
232+
}: {
233+
title: string;
234+
body: string;
235+
setError: (error: string) => void;
236+
pageUid: string;
237+
}): Promise<{ status: number }> => {
238+
const gitHubAccessToken = localStorageGet("github-oauth");
239+
const selectedRepo = localStorageGet("github-repo");
240+
try {
241+
// https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#create-an-issue
242+
const response = await apiPost({
243+
domain: "https://api.github.com",
244+
path: `repos/${selectedRepo}/issues`,
245+
headers: {
246+
Authorization: `token ${gitHubAccessToken}`,
247+
},
248+
data: {
249+
title,
250+
body,
251+
// milestone,
252+
// labels,
253+
// assignees
254+
},
255+
});
256+
if (response.status === 401) {
257+
setError("Authentication failed. Please log in again.");
258+
localStorageSet("github-oauth", "");
259+
return { status: 401 };
260+
}
261+
262+
if (response.status === 201) {
263+
const props = getBlockProps(pageUid);
264+
const newProps = {
265+
...props,
266+
["github-sync"]: {
267+
issue: {
268+
id: response.id,
269+
number: response.number,
270+
html_url: response.html_url,
271+
state: response.state,
272+
labels: response.labels,
273+
createdAt: response.created_at,
274+
updatedAt: response.updated_at,
275+
repo: selectedRepo,
276+
},
277+
},
278+
};
279+
window.roamAlphaAPI.updateBlock({
280+
block: {
281+
uid: pageUid,
282+
props: newProps,
283+
},
284+
});
285+
}
286+
287+
return { status: response.status };
288+
} catch (error) {
289+
const e = error as Error;
290+
setError("Failed to create issue");
291+
return { status: 500 };
292+
}
293+
};
294+
217295
const handleSetSelectedPage = (title: string) => {
218296
setSelectedPageTitle(title);
219297
setSelectedPageUid(getPageUidByPageTitle(title));
@@ -526,15 +604,12 @@ const ExportDialog: ExportDialogComponent = ({
526604
onItemSelect={(et) => setActiveExportDestination(et)}
527605
/>
528606
</Label>
529-
<ExportGithub
530-
isVisible={activeExportDestination === "github"}
531-
selectedRepo={selectedRepo}
532-
setSelectedRepo={setSelectedRepo}
533-
setError={setError}
534-
gitHubAccessToken={gitHubAccessToken}
535-
setGitHubAccessToken={setGitHubAccessToken}
536-
setCanSendToGitHub={setCanSendToGitHub}
537-
/>
607+
{activeExportDestination === "github" && (
608+
<ExportGithub
609+
setError={setError}
610+
setCanSendToGitHub={setCanSendToGitHub}
611+
/>
612+
)}
538613
</div>
539614
</div>
540615

@@ -628,17 +703,40 @@ const ExportDialog: ExportDialogComponent = ({
628703

629704
if (activeExportDestination === "github") {
630705
const { title, content } = files[0];
706+
const githubDestination =
707+
localStorageGet("github-destination");
631708
try {
632-
const { status } = await writeFileToRepo({
633-
filename: title,
634-
content,
635-
setError,
636-
});
709+
let status;
710+
if (githubDestination === "File") {
711+
status = (
712+
await writeFileToRepo({
713+
filename: title,
714+
content,
715+
setError,
716+
})
717+
).status;
718+
}
719+
if (githubDestination === "Issue") {
720+
const pageUid =
721+
typeof results === "function" ? "" : results[0].uid; // TODO handle multiple results
722+
if (!pageUid) {
723+
setError("No page UID found.");
724+
return;
725+
}
726+
status = (
727+
await writeFileToIssue({
728+
title: title.replace(/\.[^/.]+$/, ""), // remove extension
729+
body: content,
730+
setError,
731+
pageUid,
732+
})
733+
).status;
734+
}
637735
if (status === 201) {
638736
// TODO: remove toast by prolonging ExportProgress
639737
renderToast({
640738
id: "export-success",
641-
content: "Upload Success",
739+
content: `Upload Success to ${githubDestination}`,
642740
intent: "success",
643741
});
644742
onClose();

0 commit comments

Comments
 (0)