feat: github issue creation

This commit is contained in:
2024-12-21 00:45:36 +01:00
parent 49e1c5d48b
commit 33db34c3d0
10 changed files with 229 additions and 13 deletions

View File

@@ -19,6 +19,7 @@
"@octokit/core": "^6.1.2",
"@prisma/client": "^6.0.1",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-label": "^2.1.1",

View File

@@ -21,6 +21,7 @@ import {
} from '@/components/ui/breadcrumb';
import { Eye, Github } from 'lucide-react';
import FeedbackView from '@/components/app/FeedbackView/FeedbackView';
import GithubIssueCreate from '@/components/app/GithubIssueCreate/GithubIssueCreate';
// TODO: refactor to maybe append the no feedback message to the table div
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
@@ -100,9 +101,7 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
<TableCell className='flex gap-2'>
<FeedbackView feedback={feedback} />
{project.github && (
<Button size={'icon'}>
<Github className="w-5 h-5" />
</Button>
<GithubIssueCreate project={project} feedback={feedback} />
)}
</TableCell>
</TableRow>

View File

@@ -0,0 +1,72 @@
'use client';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import type { Feedback, Project } from '@prisma/client';
import { Github } from 'lucide-react';
import { UniversalForm } from '../UniversalForm/UniversalForm';
import { githubCreateIssue } from '@/lib/forms/actions';
import React from 'react';
export default function GithubIssueCreate(props: Props) {
const [open, setOpen] = React.useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button size={'icon'}>
<Github className="w-5 h-5" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create Github issue</DialogTitle>
</DialogHeader>
<UniversalForm
fields={[
{
type: 'text',
name: 'title',
label: 'Title',
},
{
type: 'text',
name: 'message',
textArea: true,
label: 'Message',
value: props.feedback.message,
},
{
type: 'hidden',
name: 'feedback',
label: 'Feedback',
value: props.feedback.id.toString(),
},
{
type: 'hidden',
name: 'project',
label: 'Project',
value: props.project.id.toString(),
},
]}
submitText={'Create issue'}
submitClassname="float-right !mt-5"
schemaName={'githubIssueCreate'}
action={githubCreateIssue}
onActionComplete={() => setOpen(false)}
/>
</DialogContent>
</Dialog>
);
}
interface Props {
project: Project;
feedback: Feedback;
}

View File

@@ -17,6 +17,7 @@ import { z } from 'zod';
import type { UniversalFormProps } from './types';
import {
customDataSchema,
githubIssueCreateSchema,
githubSettingsSchema,
githubTestIssueSchema,
projectSettingsSchema,
@@ -27,6 +28,7 @@ import { useActionState } from 'react';
import React from 'react';
import { toast } from 'sonner';
import { createSchema } from '@/lib/forms/zod';
import { Textarea } from '@/components/ui/textarea';
export const schemaDb = [
{ name: 'projectSettings', zod: projectSettingsSchema },
@@ -35,6 +37,7 @@ export const schemaDb = [
{ name: 'create', zod: createSchema },
{ name: 'githubSettings', zod: githubSettingsSchema },
{ name: 'githubTestIssue', zod: githubTestIssueSchema },
{ name: 'githubIssueCreate', zod: githubIssueCreateSchema },
] as const;
export function UniversalForm<T extends z.ZodType>({
@@ -58,7 +61,7 @@ export function UniversalForm<T extends z.ZodType>({
const initialValues = React.useMemo(() => {
const values: Record<string, any> = {};
fields.forEach((field) => {
values[field.name] = field.value ?? ''; // Use empty string as fallback
values[field.name] = field.value ?? ''; // Use empty string as fallback
});
return { ...values, ...defaultValues };
}, [fields, defaultValues]);
@@ -89,12 +92,20 @@ export function UniversalForm<T extends z.ZodType>({
<FormItem>
{field.type !== 'hidden' && <FormLabel>{field.label}</FormLabel>}
<FormControl>
<Input
type={field.type || 'text'}
placeholder={field.placeholder}
{...formField}
value={formField.value ?? ''}
/>
{field.textArea ? (
<Textarea
placeholder={field.placeholder}
{...formField}
value={formField.value ?? ''}
/>
) : (
<Input
type={field.type || 'text'}
placeholder={field.placeholder}
{...formField}
value={formField.value ?? ''}
/>
)}
</FormControl>
{field.description && <FormDescription>{field.description}</FormDescription>}
<FormMessage />
@@ -106,4 +117,4 @@ export function UniversalForm<T extends z.ZodType>({
</form>
</Form>
);
}
}

View File

@@ -9,11 +9,12 @@ export type FormFieldConfig = {
placeholder?: string;
description?: string;
value?: string;
textArea?: boolean;
};
export type UniversalFormProps<T extends z.ZodType> = {
fields: FormFieldConfig[];
schemaName: typeof schemaDb[number]['name'];
schemaName: (typeof schemaDb)[number]['name'];
action: (prev: any, formData: FormData) => void;
onActionComplete?: (result: unknown) => void;
defaultValues?: Partial<z.infer<T>>;

View File

@@ -36,4 +36,11 @@ export const githubSettingsSchema = z.object({
export const githubTestIssueSchema = z.object({
id: z.string().nonempty(),
});
export const githubIssueCreateSchema = z.object({
feedback: z.string().nonempty(),
project: z.string().nonempty(),
message: z.string().nonempty(),
title: z.string().nonempty(),
});

View File

@@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

View File

@@ -2,6 +2,7 @@
import {
customDataSchema,
githubIssueCreateSchema,
githubSettingsSchema,
githubTestIssueSchema,
projectSettingsSchema,
@@ -11,7 +12,6 @@ import { validateRequest } from '../auth';
import prisma from '../db';
import zodVerify from '../zodVerify';
import { createSchema } from './zod';
import { Octokit } from '@octokit/core';
import { octokitApp } from '../octokitApp';
export async function create(prev: any, formData: FormData) {
@@ -183,3 +183,57 @@ export async function githubTestIssue(prev: any, formData: FormData) {
return { success: true };
}
export async function githubCreateIssue(prev: any, formData: FormData) {
const zod = await zodVerify(githubIssueCreateSchema, formData);
const { user } = await validateRequest();
if (!user) {
return { success: false, error: 'You must be logged in' };
}
if (!zod.success) {
return zod;
}
const project = await prisma.project.findUnique({
where: {
id: zod.data.project,
},
include: {
user: true,
},
});
if (!project) {
return { success: false, error: 'Project not found' };
}
try {
const [owner, repo] = project.github!.split('/').slice(-2);
for (const installationId of project.user.installations) {
const installation = await octokitApp.getInstallationOctokit(Number(installationId));
const getRepo = await installation
.request('GET /repos/{owner}/{repo}', {
owner,
repo,
})
.catch(() => ({ status: 404 }));
if (getRepo.status === 200) {
const createIssue = await installation.request('POST /repos/{owner}/{repo}/issues', {
owner,
repo,
title: zod.data.title,
body: `### Feedback\n\nFeedback ID: ${zod.data.feedback}\n\n### Message\n\n${zod.data.message}`,
});
if (createIssue.status === 201) {
break;
}
}
}
} catch (e) {
console.error(e);
return { success: false, error: e };
}
return { success: true };
}

View File

@@ -1223,6 +1223,20 @@
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-checkbox@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz#0e2ab913fddf3c88603625f7a9457d73882c8a32"
integrity sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==
dependencies:
"@radix-ui/primitive" "1.1.1"
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-presence" "1.1.2"
"@radix-ui/react-primitive" "2.0.1"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-use-previous" "1.1.0"
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/react-collection@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed"
@@ -1700,6 +1714,11 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==
"@radix-ui/react-use-previous@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz#d4dd37b05520f1d996a384eb469320c2ada8377c"
integrity sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==
"@radix-ui/react-use-rect@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88"