Skip to content

Commit 280d8f6

Browse files
nahSystemuhjball
andauthored
feat: initial github integration with importing projects (#421)
* feat: initial github integration with importing projects * fix: remove unused args * chore: remove duplicate col --------- Co-authored-by: Henry <henry_ball@hotmail.co.uk>
1 parent eeae23a commit 280d8f6

File tree

11 files changed

+3961
-36
lines changed

11 files changed

+3961
-36
lines changed

apps/web/src/views/boards/components/ImportBoardsForm.tsx

Lines changed: 227 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { t } from "@lingui/core/macro";
44
import { Plural, Trans } from "@lingui/react/macro";
55
import { Fragment, useEffect, useState } from "react";
66
import { Controller, useForm } from "react-hook-form";
7-
import { FaTrello } from "react-icons/fa";
7+
import { FaGithub, FaTrello } from "react-icons/fa";
88
import {
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 = () => {
313503
export 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

Comments
 (0)