@@ -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+
8692export 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
94102type 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- ] ;
103106const SEND_TO_DESTINATIONS = [ "page" , "graph" ] ;
104107
105108const 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