Skip to content
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

Allow syncing projects to milestones #144

Merged
merged 6 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 89 additions & 42 deletions components/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
import { Cross1Icon, InfoCircledIcon, WidthIcon } from "@radix-ui/react-icons";
import React, { useContext, useState } from "react";
import React, { useContext, useEffect, useState } from "react";
import { LINEAR } from "../utils/constants";
import { updateGitHubWebhook } from "../utils/github";
import { updateLinearWebhook } from "../utils/linear";
import { getLinearWebhook, updateLinearWebhook } from "../utils/linear";
import { Context } from "./ContextProvider";
import Tooltip from "./Tooltip";

const options = ["Cycle", "Project"] as const;
type Option = (typeof options)[number];

const Dashboard = () => {
const { syncs, setSyncs, gitHubContext, linearContext } =
useContext(Context);

const [loading, setLoading] = useState(false);
const [milestoneAction, setMilestoneAction] = useState<Option | null>(null);

// Get initial webhook settings
useEffect(() => {
if (!syncs?.length) return;

getLinearWebhook(
linearContext.apiKey,
syncs[0].LinearTeam.teamName
).then(res => {
if (res.resourceTypes.includes("Cycle")) {
setMilestoneAction("Cycle");
} else if (res.resourceTypes.includes("Project")) {
setMilestoneAction("Project");
}
});
}, [syncs]);

const removeSync = async (syncId: string) => {
if (!syncId || !gitHubContext.apiKey) return;
Expand All @@ -37,36 +57,37 @@ const Dashboard = () => {
});
};

const handleMilestoneSyncChange = async (
e: React.ChangeEvent<HTMLInputElement>
) => {
setLoading(true);
useEffect(() => {
const handleMilestoneSyncChange = async () => {
setLoading(true);

const checked = e.target.checked || false;
for (const sync of syncs) {
await updateGitHubWebhook(
gitHubContext.apiKey,
sync.GitHubRepo.repoName,
{
...(milestoneAction
? { add_events: ["milestone"] }
: { remove_events: ["milestone"] })
}
);
await updateLinearWebhook(
linearContext.apiKey,
sync.LinearTeam.teamName,
{
resourceTypes: [
...LINEAR.WEBHOOK_EVENTS,
...(milestoneAction ? [milestoneAction] : [])
]
}
);
}

for (const sync of syncs) {
await updateGitHubWebhook(
gitHubContext.apiKey,
sync.GitHubRepo.repoName,
{
...(checked && { add_events: ["milestone"] }),
...(!checked && { remove_events: ["milestone"] })
}
);
await updateLinearWebhook(
linearContext.apiKey,
sync.LinearTeam.teamName,
{
resourceTypes: [
...LINEAR.WEBHOOK_EVENTS,
...(checked ? ["Cycle"] : [])
]
}
);
}
setLoading(false);
};

setLoading(false);
};
handleMilestoneSyncChange();
}, [milestoneAction]);

if (!syncs?.length) return <></>;

Expand Down Expand Up @@ -104,19 +125,45 @@ const Dashboard = () => {
</Tooltip>
</div>
))}
<div className="flex items-center space-x-2 mb-4">
<input
disabled={!linearContext.apiKey}
type="checkbox"
id="syncsMilestones"
onChange={handleMilestoneSyncChange}
/>
<label htmlFor="syncsMilestones" className="whitespace-nowrap">
Sync milestones to cycles
</label>
<Tooltip content="Requires connecting to Linear first">
<InfoCircledIcon className="w-6 h-6 text-gray-400 hover:font-secondary transition-colors duration-200" />
</Tooltip>
<div className="flex flex-col items-start">
{options.map(option => (
<div
key={option}
className="flex items-center space-x-2 mb-4"
>
<input
id={option}
disabled={!linearContext.apiKey}
type="checkbox"
checked={milestoneAction === option}
onChange={e =>
setMilestoneAction(
e.target.checked
? (e.target.id as Option)
: null
)
}
/>
<label htmlFor={option} className="whitespace-nowrap">
Sync {option}s to Milestones
</label>
<Tooltip
content={
!linearContext.apiKey
? "Requires connecting to Linear first"
: milestoneAction
? `Will disable ${
option == "Cycle"
? "Project"
: "Cycle"
} sync`
: ""
}
>
<InfoCircledIcon className="w-6 h-6 text-gray-400 hover:font-secondary transition-colors duration-200" />
</Tooltip>
</div>
))}
</div>
</div>
);
Expand Down
55 changes: 35 additions & 20 deletions components/GitHubAuthButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
getRepoWebhook,
getGitHubAuthURL,
saveGitHubContext,
setGitHubWebook
setGitHubWebook,
getGitHubContext
} from "../utils/github";
import { Context } from "./ContextProvider";
import Select from "./Select";
Expand Down Expand Up @@ -90,21 +91,28 @@ const GitHubAuthButton = ({
const startingPage = 0;

const listReposRecursively = async (page: number): Promise<void> => {
const res = await listReposForUser(gitHubToken, page);
try {
const res = await listReposForUser(gitHubToken, page);

if (!res || res.length < 1) {
if (!res || res?.length < 1) {
setReposLoading(false);
return;
}

setRepos((current: GitHubRepo[]) => [
...current,
...(res?.map?.(repo => {
return { id: repo.id, name: repo.full_name };
}) ?? [])
]);

return await listReposRecursively(page + 1);
} catch (err) {
alert(`Error fetching repos: ${err}`);
setReposLoading(false);

return;
}

setRepos((current: GitHubRepo[]) => [
...current,
...(res?.map?.(repo => {
return { id: repo.id, name: repo.full_name };
}) ?? [])
]);

return await listReposRecursively(page + 1);
};

setReposLoading(true);
Expand All @@ -121,9 +129,14 @@ const GitHubAuthButton = ({

setLoading(true);

getRepoWebhook(chosenRepo.name, gitHubToken)
.then(res => {
if (res?.exists) {
const checkRepo = async () => {
try {
const [webhook, repo] = await Promise.all([
getRepoWebhook(chosenRepo.name, gitHubToken),
getGitHubContext(chosenRepo.id, gitHubToken)
]);

if (webhook?.exists && repo?.inDb) {
setDeployed(true);
onDeployWebhook({
userId: gitHubUser.id,
Expand All @@ -133,12 +146,14 @@ const GitHubAuthButton = ({
} else {
setDeployed(false);
}
setLoading(false);
})
.catch(err => {
} catch (err) {
alert(`Error checking for existing repo: ${err}`);
setLoading(false);
});
}

setLoading(false);
};

checkRepo();
}, [chosenRepo]);

const openAuthPage = () => {
Expand Down
34 changes: 34 additions & 0 deletions pages/api/github/repo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "../../../prisma";

// POST /api/github/repo
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.setHeader("Allow", "POST").status(405).send({
error: "Only POST requests are accepted"
});
}

const { repoId } = JSON.parse(req.body);

if (!repoId || isNaN(repoId)) {
return res.status(400).send({ error: "Request is missing repo ID" });
}

try {
const inDb = repoId
? await prisma.gitHubRepo.findFirst({
where: { repoId: Number(repoId) }
})
: false;

return res.status(200).json({ inDb });
} catch (err) {
console.error(err);
return res.status(404).send({ error: err });
}
}

Loading
Loading