mirror of
https://github.com/SrIzan10/echospace.git
synced 2026-06-06 00:56:54 +00:00
feat: github issue creation
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
72
src/components/app/GithubIssueCreate/GithubIssueCreate.tsx
Normal file
72
src/components/app/GithubIssueCreate/GithubIssueCreate.tsx
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
30
src/components/ui/checkbox.tsx
Normal file
30
src/components/ui/checkbox.tsx
Normal 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 }
|
||||
22
src/components/ui/textarea.tsx
Normal file
22
src/components/ui/textarea.tsx
Normal 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 }
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
19
yarn.lock
19
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user