@@ -4,7 +4,7 @@ import { t } from "@lingui/core/macro";
44import { Plural , Trans } from "@lingui/react/macro" ;
55import { Fragment , useEffect , useState } from "react" ;
66import { Controller , useForm } from "react-hook-form" ;
7- import { FaTrello } from "react-icons/fa" ;
7+ import { FaGithub , FaTrello } from "react-icons/fa" ;
88import {
99 HiChevronUpDown ,
1010 HiMiniArrowTopRightOnSquare ,
@@ -27,12 +27,23 @@ const integrationProviders: Record<
2727 name : "Trello" ,
2828 icon : < FaTrello /> ,
2929 } ,
30+ github : {
31+ name : "GitHub" ,
32+ icon : < FaGithub /> ,
33+ } ,
3034} ;
3135
32- const SelectSource = ( { handleNextStep } : { handleNextStep : ( ) => void } ) => {
36+ const SelectSource = ( {
37+ handleNextStep,
38+ } : {
39+ handleNextStep : ( provider : string ) => void ;
40+ } ) => {
3341 const { data : integrations , refetch : refetchIntegrations } =
3442 api . integration . providers . useQuery ( ) ;
35- const { control, handleSubmit } = useForm ( {
43+ const { data : githubStatus , refetch : refetchGithubStatus } =
44+ api . integration . getGitHubStatus . useQuery ( ) ;
45+
46+ const { control, handleSubmit, watch } = useForm ( {
3647 defaultValues : {
3748 source : integrations ?. [ 0 ] ?. provider ?? "trello" ,
3849 } ,
@@ -47,23 +58,36 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => {
4758 } ,
4859 ) ;
4960
50- const hasIntegrations = integrations && integrations . length > 0 ;
61+ const availableIntegrations = [
62+ ...( integrations ?? [ ] ) ,
63+ ...( githubStatus ?. connected ? [ { provider : "github" } ] : [ ] ) ,
64+ ] ;
65+
66+ const hasIntegrations = availableIntegrations . length > 0 ;
5167
5268 useEffect ( ( ) => {
5369 const handleFocus = ( ) => {
54- refetchIntegrations ( ) ;
70+ void refetchIntegrations ( ) ;
71+ void refetchGithubStatus ( ) ;
5572 } ;
5673 window . addEventListener ( "focus" , handleFocus ) ;
5774 return ( ) => {
5875 window . removeEventListener ( "focus" , handleFocus ) ;
5976 } ;
60- } , [ refetchIntegrations ] ) ;
77+ } , [ refetchIntegrations , refetchGithubStatus ] ) ;
6178
6279 const onSubmit = ( ) => {
63- if ( ! hasIntegrations && trelloUrl ) {
64- window . open ( trelloUrl . url , "trello_auth" , "height=800,width=600" ) ;
80+ const selected = watch ( "source" ) ;
81+ if (
82+ selected === "trello" &&
83+ ! integrations ?. some ( ( i ) => i . provider === "trello" )
84+ ) {
85+ if ( trelloUrl )
86+ window . open ( trelloUrl . url , "trello_auth" , "height=800,width=600" ) ;
87+ } else if ( selected === "github" && ! githubStatus ?. connected ) {
88+ window . open ( "/settings/integrations" , "_blank" ) ;
6589 } else {
66- handleNextStep ( ) ;
90+ handleNextStep ( selected ) ;
6791 }
6892 } ;
6993
@@ -102,7 +126,7 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => {
102126 >
103127 < Listbox . Options className = "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-light-50 py-1 text-base text-neutral-900 shadow-lg ring-1 ring-light-600 ring-opacity-5 focus:outline-none dark:bg-dark-300 dark:text-dark-1000 sm:text-sm" >
104128 { hasIntegrations ? (
105- integrations . map ( ( integration , index ) => (
129+ availableIntegrations . map ( ( integration , index ) => (
106130 < Listbox . Option
107131 key = { `source_${ index } ` }
108132 className = "relative cursor-default select-none px-1"
@@ -123,18 +147,32 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => {
123147 </ Listbox . Option >
124148 ) )
125149 ) : (
126- < Listbox . Option
127- key = "trello_placeholder"
128- className = "font-sm relative cursor-default select-none px-1"
129- value = "trello"
130- >
131- < div className = "flex items-center rounded-[5px] p-1 text-sm hover:bg-light-200 dark:hover:bg-dark-400" >
132- { integrationProviders . trello ?. icon }
133- < span className = "ml-2 block truncate text-sm" >
134- { integrationProviders . trello ?. name }
135- </ span >
136- </ div >
137- </ Listbox . Option >
150+ < >
151+ < Listbox . Option
152+ key = "trello_placeholder"
153+ className = "font-sm relative cursor-default select-none px-1"
154+ value = "trello"
155+ >
156+ < div className = "flex items-center rounded-[5px] p-1 text-sm hover:bg-light-200 dark:hover:bg-dark-400" >
157+ { integrationProviders . trello ?. icon }
158+ < span className = "ml-2 block truncate text-sm" >
159+ { integrationProviders . trello ?. name }
160+ </ span >
161+ </ div >
162+ </ Listbox . Option >
163+ < Listbox . Option
164+ key = "github_placeholder"
165+ className = "font-sm relative cursor-default select-none px-1"
166+ value = "github"
167+ >
168+ < div className = "flex items-center rounded-[5px] p-1 text-sm hover:bg-light-200 dark:hover:bg-dark-400" >
169+ { integrationProviders . github ?. icon }
170+ < span className = "ml-2 block truncate text-sm" >
171+ { integrationProviders . github ?. name }
172+ </ span >
173+ </ div >
174+ </ Listbox . Option >
175+ </ >
138176 ) }
139177 </ Listbox . Options >
140178 </ Transition >
@@ -154,7 +192,159 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => {
154192 ! hasIntegrations ? < HiMiniArrowTopRightOnSquare /> : undefined
155193 }
156194 >
157- { hasIntegrations ? t `Select source` : t `Connect Trello` }
195+ { hasIntegrations ? t `Select source` : t `Connect` }
196+ </ Button >
197+ </ div >
198+ </ div >
199+ </ form >
200+ ) ;
201+ } ;
202+
203+ const ImportGithub : React . FC = ( ) => {
204+ const utils = api . useUtils ( ) ;
205+ const { closeModal } = useModal ( ) ;
206+ const { workspace } = useWorkspace ( ) ;
207+ const { showPopup } = usePopup ( ) ;
208+ const [ isSelectAllEnabled , setIsSelectAllEnabled ] = useState ( false ) ;
209+
210+ const refetchBoards = ( ) => utils . board . all . refetch ( ) ;
211+
212+ const { data : projects , isLoading : projectsLoading } =
213+ api . import . github . getProjects . useQuery ( ) ;
214+
215+ const {
216+ register : registerProjects ,
217+ handleSubmit : handleSubmitProjects ,
218+ setValue,
219+ watch,
220+ } = useForm ( {
221+ defaultValues : Object . fromEntries (
222+ projects ?. map ( ( project ) => [ project . id , true ] ) ?? [ ] ,
223+ ) ,
224+ } ) ;
225+
226+ const importProjects = api . import . github . importProjects . useMutation ( {
227+ onSuccess : async ( ) => {
228+ showPopup ( {
229+ header : t `Import complete` ,
230+ message : t `Your projects have been imported.` ,
231+ icon : "success" ,
232+ } ) ;
233+ try {
234+ await refetchBoards ( ) ;
235+ closeModal ( ) ;
236+ } catch ( e ) {
237+ console . log ( e ) ;
238+ }
239+ } ,
240+ onError : ( ) => {
241+ showPopup ( {
242+ header : t `Import failed` ,
243+ message : t `Please try again later, or contact customer support.` ,
244+ icon : "error" ,
245+ } ) ;
246+ } ,
247+ } ) ;
248+
249+ const projectWatchers = projects ?. map ( ( project ) => ( {
250+ id : project . id ,
251+ value : watch ( project . id ) ,
252+ } ) ) ;
253+
254+ const projectCount =
255+ projectWatchers ?. filter ( ( w ) => w . value === true ) . length ?? 0 ;
256+
257+ const onSubmitProjects = ( values : Record < string , boolean > ) => {
258+ const projectIds = Object . keys ( values ) . filter (
259+ ( key ) => values [ key ] === true ,
260+ ) ;
261+
262+ importProjects . mutate ( {
263+ projectIds,
264+ workspacePublicId : workspace . publicId ,
265+ } ) ;
266+ } ;
267+
268+ const renderContent = ( ) => {
269+ if ( projectsLoading ) {
270+ return (
271+ < div className = "flex h-full w-full flex-col items-center justify-center gap-1" >
272+ < div className = "h-[30px] w-full animate-pulse rounded-[5px] bg-light-200 dark:bg-dark-300" />
273+ < div className = "h-[30px] w-full animate-pulse rounded-[5px] bg-light-200 dark:bg-dark-300" />
274+ < div className = "h-[30px] w-full animate-pulse rounded-[5px] bg-light-200 dark:bg-dark-300" />
275+ </ div >
276+ ) ;
277+ }
278+
279+ if ( ! projects ?. length ) {
280+ return (
281+ < div className = "flex h-full w-full items-center justify-center" >
282+ < p className = "text-sm text-neutral-500 dark:text-dark-900" >
283+ { t `No projects found` }
284+ </ p >
285+ </ div >
286+ ) ;
287+ }
288+
289+ return projects . map ( ( project ) => (
290+ < div key = { project . id } >
291+ < label
292+ className = "flex cursor-pointer items-center rounded-[5px] p-2 hover:bg-light-100 dark:hover:bg-dark-300"
293+ htmlFor = { project . id }
294+ >
295+ < input
296+ id = { project . id }
297+ type = "checkbox"
298+ className = "h-[14px] w-[14px] rounded bg-transparent ring-0 focus:outline-none focus:ring-0 focus:ring-offset-0"
299+ { ...registerProjects ( project . id ) }
300+ />
301+ < span className = "ml-3 text-sm text-neutral-900 dark:text-dark-1000" >
302+ { project . name }
303+ </ span >
304+ </ label >
305+ </ div >
306+ ) ) ;
307+ } ;
308+
309+ return (
310+ < form onSubmit = { handleSubmitProjects ( onSubmitProjects ) } >
311+ < div className = "h-[105px] overflow-auto px-5" > { renderContent ( ) } </ div >
312+
313+ < div className = "mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600" >
314+ < Toggle
315+ label = { t `Select all` }
316+ isChecked = { ! ! isSelectAllEnabled }
317+ onChange = { ( ) => {
318+ const newState = ! isSelectAllEnabled ;
319+ setIsSelectAllEnabled ( newState ) ;
320+
321+ for ( const project of projects ?? [ ] ) {
322+ setValue ( project . id , newState ) ;
323+ }
324+ } }
325+ />
326+ < div className = "space-x-2" >
327+ < Button
328+ type = "submit"
329+ isLoading = { importProjects . isPending }
330+ disabled = {
331+ importProjects . isPending ||
332+ projectsLoading ||
333+ ! projects ?. length ||
334+ ! projects . some (
335+ ( project ) =>
336+ projectWatchers ?. find ( ( w ) => w . id === project . id ) ?. value ===
337+ true ,
338+ )
339+ }
340+ >
341+ < Trans >
342+ < Plural
343+ value = { projectCount }
344+ one = { `Import project (1)` }
345+ other = { `Import projects (${ projectCount } )` }
346+ />
347+ </ Trans >
158348 </ Button >
159349 </ div >
160350 </ div >
@@ -213,7 +403,7 @@ const ImportTrello: React.FC = () => {
213403 value : watch ( board . id ) ,
214404 } ) ) ;
215405
216- const boardCount = boardWatchers ?. filter ( ( w ) => w . value === true ) . length || 0 ;
406+ const boardCount = boardWatchers ?. filter ( ( w ) => w . value === true ) . length ?? 0 ;
217407
218408 const onSubmitBoards = ( values : Record < string , boolean > ) => {
219409 const boardIds = Object . keys ( values ) . filter ( ( key ) => values [ key ] === true ) ;
@@ -267,7 +457,7 @@ const ImportTrello: React.FC = () => {
267457
268458 return (
269459 < form onSubmit = { handleSubmitBoards ( onSubmitBoards ) } >
270- < div className = "h-[105px] overflow-scroll px-5" > { renderContent ( ) } </ div >
460+ < div className = "h-[105px] overflow-auto px-5" > { renderContent ( ) } </ div >
271461
272462 < div className = "mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600" >
273463 < Toggle
@@ -277,7 +467,7 @@ const ImportTrello: React.FC = () => {
277467 const newState = ! isSelectAllEnabled ;
278468 setIsSelectAllEnabled ( newState ) ;
279469
280- for ( const board of boards || [ ] ) {
470+ for ( const board of boards ?? [ ] ) {
281471 setValue ( board . id , newState ) ;
282472 }
283473 } }
@@ -313,6 +503,7 @@ const ImportTrello: React.FC = () => {
313503export function ImportBoardsForm ( ) {
314504 const { closeModal } = useModal ( ) ;
315505 const [ step , setStep ] = useState ( 1 ) ;
506+ const [ provider , setProvider ] = useState < string | null > ( null ) ;
316507
317508 return (
318509 < div >
@@ -339,8 +530,16 @@ export function ImportBoardsForm() {
339530 </ button >
340531 </ div >
341532
342- { step === 1 && < SelectSource handleNextStep = { ( ) => setStep ( step + 1 ) } /> }
343- { step === 2 && < ImportTrello /> }
533+ { step === 1 && (
534+ < SelectSource
535+ handleNextStep = { ( p ) => {
536+ setProvider ( p ) ;
537+ setStep ( step + 1 ) ;
538+ } }
539+ />
540+ ) }
541+ { step === 2 && provider === "trello" && < ImportTrello /> }
542+ { step === 2 && provider === "github" && < ImportGithub /> }
344543 </ div >
345544 ) ;
346545}
0 commit comments