Working on project.

This commit is contained in:
charles-gauthereau
2024-11-30 21:05:47 +01:00
parent 3e62359bd9
commit afa46c171b
19 changed files with 701 additions and 180 deletions
-1
View File
@@ -11,7 +11,6 @@ import {prisma} from "@/prisma";
export default async function Layout({children}: { children: React.ReactNode }) { export default async function Layout({children}: { children: React.ReactNode }) {
const user = await currentUser() const user = await currentUser()
console.log(user)
if (user) { if (user) {
const userInfo = await prisma.user.findUnique({ const userInfo = await prisma.user.findUnique({
where: { where: {
@@ -3,7 +3,7 @@ import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user"; import {requiredCurrentUser} from "@/auth/current-user";
import {prisma} from "@/prisma"; import {prisma} from "@/prisma";
import {notFound} from "next/navigation"; import {notFound} from "next/navigation";
import {ProjectForm} from "@/components/wrappers/project/ProjectForm"; import {ProjectForm} from "@/components/wrappers/Project/ProjectForm";
export default async function RoutePage(props: PageParams<{ export default async function RoutePage(props: PageParams<{
@@ -16,12 +16,31 @@ export default async function RoutePage(props: PageParams<{
const project = await prisma.project.findUnique({ const project = await prisma.project.findUnique({
where: { where: {
id: projectId, id: projectId,
},
include: {
databases: {}
} }
}); });
if (!project) { if (!project) {
notFound(); notFound();
} }
const organization = await prisma.organization.findFirst({
where: {
slug: 'default',
}
})
const availableDatabases = await prisma.database.findMany({
where: {
OR: [
{ projectId: null },
{ projectId: project.id },
],
},
orderBy: {
createdAt: 'desc',
},
})
return ( return (
@@ -32,7 +51,7 @@ export default async function RoutePage(props: PageParams<{
</PageTitle> </PageTitle>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
<ProjectForm defaultValues={project} projectId={project.id}/> <ProjectForm organization={organization} databases={availableDatabases} defaultValues={project} projectId={project.id}/>
</PageContent> </PageContent>
</Page> </Page>
) )
@@ -12,13 +12,15 @@ export default async function RoutePage(props: PageParams<{ projectId: string }>
const {projectId} = await props.params const {projectId} = await props.params
// const project = await prisma.project.findUnique({ const project = await prisma.project.findUnique({
// where: { where: {
// id: projectId, id: projectId,
// }, },
// }) include:{
databases: {}
}
})
const project = projects.find(p => p.id === projectId)
return ( return (
@@ -35,7 +37,7 @@ export default async function RoutePage(props: PageParams<{ projectId: string }>
</PageActions> </PageActions>
</div> </div>
{/*<PageDescription className="mt-5 sm:mt-0">{project.description}</PageDescription>*/} {/*<PageDescription className="mt-5 sm:mt-0">{Project.description}</PageDescription>*/}
<PageContent className="flex flex-col w-full h-full"> <PageContent className="flex flex-col w-full h-full">
</PageContent> </PageContent>
</Page> </Page>
@@ -1,7 +1,7 @@
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {prisma} from "@/prisma"; import {prisma} from "@/prisma";
import {ProjectForm} from "@/components/wrappers/project/ProjectForm"; import {ProjectForm} from "@/components/wrappers/Project/ProjectForm";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
@@ -15,6 +15,12 @@ export default async function RoutePage(props: PageParams<{}>) {
}, },
}) })
const organization = await prisma.organization.findFirst({
where: {
slug: 'default',
}
})
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
@@ -23,12 +29,7 @@ export default async function RoutePage(props: PageParams<{}>) {
</PageTitle> </PageTitle>
</PageHeader> </PageHeader>
<PageContent> <PageContent>
Add databases <ProjectForm databases={availableDatabases} organization={organization}/>
{availableDatabases.map(database =>
<div key={database.id}>
{database.name}
</div>)}
<ProjectForm/>
</PageContent> </PageContent>
</Page> </Page>
) )
+12 -4
View File
@@ -5,11 +5,21 @@ import {Button} from "@/components/ui/button";
import Link from 'next/link' import Link from 'next/link'
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {projects} from "@/utils/mock-data"; import {projects} from "@/utils/mock-data";
import {ProjectCard} from "@/components/wrappers/project/ProjectCard"; import {ProjectCard} from "@/components/wrappers/Project/ProjectCard";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
// const projects = await prisma.project.findMany({})
const projects = await prisma.project.findMany({
where: {
organization: {
slug: "default",
}
},
include:{
databases: {}
}
})
return ( return (
<Page> <Page>
@@ -44,8 +54,6 @@ export default async function RoutePage(props: PageParams<{}>) {
</Link> </Link>
} }
</PageContent> </PageContent>
</Page> </Page>
) )
@@ -1,9 +1,3 @@
/*
Warnings:
- A unique constraint covering the columns `[generatedId]` on the table `databases` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable -- AlterTable
ALTER TABLE "databases" ADD COLUMN "project_id" TEXT; ALTER TABLE "databases" ADD COLUMN "project_id" TEXT;
@@ -34,9 +28,6 @@ CREATE UNIQUE INDEX "organizations_slug_key" ON "organizations"("slug");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "projects_slug_key" ON "projects"("slug"); CREATE UNIQUE INDEX "projects_slug_key" ON "projects"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "databases_generatedId_key" ON "databases"("generatedId");
-- AddForeignKey -- AddForeignKey
ALTER TABLE "projects" ADD CONSTRAINT "projects_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "projects" ADD CONSTRAINT "projects_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+10 -12
View File
@@ -6,6 +6,7 @@ import {env} from "@/env.mjs";
import GoogleProvider from "next-auth/providers/google"; import GoogleProvider from "next-auth/providers/google";
export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({ export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({
adapter: PrismaAdapter(prisma), adapter: PrismaAdapter(prisma),
theme: { theme: {
@@ -78,22 +79,20 @@ export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({
// // session.user.authMethod = user.authMethod; // // session.user.authMethod = user.authMethod;
// return session // return session
// }, // },
async jwt({token, trigger, session, user}) { async jwt({ token, trigger, session, user }) {
if (trigger === "update" && session) { if (trigger === "update" && session) {
return {...session, user: {...token, ...session?.user}}; return { ...token, ...session?.user };
// return {...token, ...session?.user};
} }
return {...token, ...user}; return { ...token, ...user };
}, },
async session({session, token, user}) { async session({ session, token, user }) {
session.user = token.user; session.user = token;
// session.user = token;
return session; return session;
}, },
async signIn({account, user, profile}) { async signIn({ account, user, profile }) {
const existingUser = await prisma.user.findFirst({ const existingUser = await prisma.user.findFirst({
where: {email: user.email}, where: { email: user.email },
}); });
if (!existingUser) { if (!existingUser) {
@@ -126,7 +125,7 @@ export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({
// Update auth method if user exists // Update auth method if user exists
await prisma.user.update({ await prisma.user.update({
where: {id: existingUser.id}, where: { id: existingUser.id },
data: { data: {
authMethod: account.provider, authMethod: account.provider,
}, },
@@ -136,5 +135,4 @@ export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({
}, },
} }
}); });
-1
View File
@@ -3,7 +3,6 @@ import {User} from "@prisma/client";
export const currentUser = async () => { export const currentUser = async () => {
const session = await baseAuth(); const session = await baseAuth();
console.log("mysession", session)
if (!session?.user) { if (!session?.user) {
return null; return null;
} }
@@ -3,12 +3,12 @@ import {
SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent,
SidebarGroupLabel, SidebarGroupLabel,
SidebarHeader, SidebarHeader,
SidebarMenu, SidebarMenu, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import {LoggedInButton} from "@/components/wrappers/Dashboard/LoggedInButton/LoggedInButton"; import {LoggedInButton} from "@/components/wrappers/Dashboard/LoggedInButton/LoggedInButton";
import {SidebarMenuCustom} from "@/components/wrappers/Dashboard/SideBar/SideBarMenu/SideBarMenu"; import {SidebarMenuCustom} from "@/components/wrappers/Dashboard/SideBar/SideBarMenu/SideBarMenu";
import {OrganizationComboBox} from "@/components/wrappers/organization/OrganizationCombobox"; import {OrganizationComboBox} from "@/components/wrappers/Organization/OrganizationCombobox";
import {prisma} from "@/prisma"; import {prisma} from "@/prisma";
export async function AppSidebar() { export async function AppSidebar() {
@@ -0,0 +1,382 @@
//https://github.com/sersavan/shadcn-multi-select-component
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import {
CheckIcon,
XCircle,
ChevronDown,
XIcon,
WandSparkles,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
/**
* Variants for the multi-select component to handle different styles.
* Uses class-variance-authority (cva) to define different styles based on "variant" prop.
*/
const multiSelectVariants = cva(
"m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300",
{
variants: {
variant: {
default:
"border-foreground/10 text-foreground bg-card hover:bg-card/80",
secondary:
"border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
inverted: "inverted",
},
},
defaultVariants: {
variant: "default",
},
}
);
/**
* Props for MultiSelect component
*/
interface MultiSelectProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof multiSelectVariants> {
/**
* An array of option objects to be displayed in the multi-select component.
* Each option object has a label, value, and an optional icon.
*/
options: {
/** The text to display for the option. */
label: string;
/** The unique value associated with the option. */
value: string;
/** Optional icon component to display alongside the option. */
icon?: React.ComponentType<{ className?: string }>;
}[];
/**
* Callback function triggered when the selected values change.
* Receives an array of the new selected values.
*/
onValueChange: (value: string[]) => void;
/** The default selected values when the component mounts. */
defaultValue?: string[];
/**
* Placeholder text to be displayed when no values are selected.
* Optional, defaults to "Select options".
*/
placeholder?: string;
/**
* Animation duration in seconds for the visual effects (e.g., bouncing badges).
* Optional, defaults to 0 (no animation).
*/
animation?: number;
/**
* Maximum number of items to display. Extra selected items will be summarized.
* Optional, defaults to 3.
*/
maxCount?: number;
/**
* The modality of the popover. When set to true, interaction with outside elements
* will be disabled and only popover content will be visible to screen readers.
* Optional, defaults to false.
*/
modalPopover?: boolean;
/**
* If true, renders the multi-select component as a child of another component.
* Optional, defaults to false.
*/
asChild?: boolean;
/**
* Additional class names to apply custom styles to the multi-select component.
* Optional, can be used to add custom styles.
*/
className?: string;
}
export const MultiSelect = React.forwardRef<
HTMLButtonElement,
MultiSelectProps
>(
(
{
options,
onValueChange,
variant,
defaultValue = [],
placeholder = "Select options",
animation = 0,
maxCount = 3,
modalPopover = false,
asChild = false,
className,
...props
},
ref
) => {
const [selectedValues, setSelectedValues] =
React.useState<string[]>(defaultValue);
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const [isAnimating, setIsAnimating] = React.useState(false);
const handleInputKeyDown = (
event: React.KeyboardEvent<HTMLInputElement>
) => {
if (event.key === "Enter") {
setIsPopoverOpen(true);
} else if (event.key === "Backspace" && !event.currentTarget.value) {
const newSelectedValues = [...selectedValues];
newSelectedValues.pop();
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
}
};
const toggleOption = (option: string) => {
const newSelectedValues = selectedValues.includes(option)
? selectedValues.filter((value) => value !== option)
: [...selectedValues, option];
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
};
const handleClear = () => {
setSelectedValues([]);
onValueChange([]);
};
const handleTogglePopover = () => {
setIsPopoverOpen((prev) => !prev);
};
const clearExtraOptions = () => {
const newSelectedValues = selectedValues.slice(0, maxCount);
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
};
const toggleAll = () => {
if (selectedValues.length === options.length) {
handleClear();
} else {
const allValues = options.map((option) => option.value);
setSelectedValues(allValues);
onValueChange(allValues);
}
};
return (
<Popover
open={isPopoverOpen}
onOpenChange={setIsPopoverOpen}
modal={modalPopover}
>
<PopoverTrigger asChild>
<Button
ref={ref}
{...props}
onClick={handleTogglePopover}
className={cn(
"flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto",
className
)}
>
{selectedValues.length > 0 ? (
<div className="flex justify-between items-center w-full">
<div className="flex flex-wrap items-center">
{selectedValues.slice(0, maxCount).map((value) => {
const option = options.find((o) => o.value === value);
const IconComponent = option?.icon;
return (
<Badge
key={value}
className={cn(
isAnimating ? "animate-bounce" : "",
multiSelectVariants({ variant })
)}
style={{ animationDuration: `${animation}s` }}
>
{IconComponent && (
<IconComponent className="h-4 w-4 mr-2" />
)}
{option?.label}
<XCircle
className="ml-2 h-4 w-4 cursor-pointer"
onClick={(event) => {
event.stopPropagation();
toggleOption(value);
}}
/>
</Badge>
);
})}
{selectedValues.length > maxCount && (
<Badge
className={cn(
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
isAnimating ? "animate-bounce" : "",
multiSelectVariants({ variant })
)}
style={{ animationDuration: `${animation}s` }}
>
{`+ ${selectedValues.length - maxCount} more`}
<XCircle
className="ml-2 h-4 w-4 cursor-pointer"
onClick={(event) => {
event.stopPropagation();
clearExtraOptions();
}}
/>
</Badge>
)}
</div>
<div className="flex items-center justify-between">
<XIcon
className="h-4 mx-2 cursor-pointer text-muted-foreground"
onClick={(event) => {
event.stopPropagation();
handleClear();
}}
/>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
/>
<ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" />
</div>
</div>
) : (
<div className="flex items-center justify-between w-full mx-auto">
<span className="text-sm text-muted-foreground mx-3">
{placeholder}
</span>
<ChevronDown className="h-4 cursor-pointer text-muted-foreground mx-2" />
</div>
)}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0"
align="start"
onEscapeKeyDown={() => setIsPopoverOpen(false)}
>
<Command>
<CommandInput
placeholder="Search..."
onKeyDown={handleInputKeyDown}
/>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
<CommandItem
key="all"
onSelect={toggleAll}
className="cursor-pointer"
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
selectedValues.length === options.length
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible"
)}
>
<CheckIcon className="h-4 w-4" />
</div>
<span>(Select All)</span>
</CommandItem>
{options.map((option) => {
const isSelected = selectedValues.includes(option.value);
return (
<CommandItem
key={option.value}
onSelect={() => toggleOption(option.value)}
className="cursor-pointer"
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible"
)}
>
<CheckIcon className="h-4 w-4" />
</div>
{option.icon && (
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<span>{option.label}</span>
</CommandItem>
);
})}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<div className="flex items-center justify-between">
{selectedValues.length > 0 && (
<>
<CommandItem
onSelect={handleClear}
className="flex-1 justify-center cursor-pointer"
>
Clear
</CommandItem>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
/>
</>
)}
<CommandItem
onSelect={() => setIsPopoverOpen(false)}
className="flex-1 justify-center cursor-pointer max-w-full"
>
Close
</CommandItem>
</div>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
{animation > 0 && selectedValues.length > 0 && (
<WandSparkles
className={cn(
"cursor-pointer my-2 text-foreground bg-background w-3 h-3",
isAnimating ? "" : "text-muted-foreground"
)}
onClick={() => setIsAnimating(!isAnimating)}
/>
)}
</Popover>
);
}
);
MultiSelect.displayName = "MultiSelect";
@@ -8,6 +8,7 @@ import {Organization} from "@prisma/client";
export type organizationComboBoxProps = { export type organizationComboBoxProps = {
organizations: Organization[] organizations: Organization[]
defaultOrganization: Organization defaultOrganization: Organization
} }
@@ -31,6 +32,10 @@ export function OrganizationComboBox(props: organizationComboBoxProps) {
} }
return ( return (
<ComboBox values={values} defaultValue={defaultOrganization.id} onValueChange={updateSession}/> <ComboBox
sideBar={true}
values={values}
defaultValue={defaultOrganization.id}
onValueChange={updateSession}/>
) )
} }
@@ -11,12 +11,14 @@ export const ProjectCard = (props: projectCardProps) => {
const {data: project} = props; const {data: project} = props;
return ( return (
<Link href={`/dashboard/projects/${project.id}`}> <Link href={`/dashboard/projects/${project.id}`}>
<Card className="flex flex-row justify-between"> <Card className="flex flex-row justify-between">
<div className=""> <div className="">
<CardHeader>{project.name}</CardHeader> <CardHeader>{project.name}</CardHeader>
<CardContent> <CardContent>
{project.databases.length} databases
</CardContent> </CardContent>
</div> </div>
</Card> </Card>
@@ -0,0 +1,62 @@
"use server"
import {userAction} from "@/safe-actions";
import {prisma} from "@/prisma";
import {ProjectSchema} from "@/components/wrappers/Project/ProjectForm.schema";
import {z} from "zod";
import {ServerActionResult} from "@/types/action-type";
import {Projects} from "@prisma/client";
export const createProjectAction = userAction
.schema(
z.object({
data: ProjectSchema,
organizationId: z.string(),
})
)
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Projects>> => {
try {
const project = await prisma.project.create({
data: {
name: parsedInput.data.name,
slug: parsedInput.data.slug,
organizationId: parsedInput.organizationId,
}
})
for (const db of parsedInput.data.databases) {
await prisma.database.update({
where: {
id: db,
},
data:{
projectId: project.id,
}
})
}
return {
success: true,
value: project,
actionSuccess: {
message: "Project has been successfully created.",
messageParams: {projectName: parsedInput.data.name},
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "Failed to create Project.",
status: 500, // Optional: Use a meaningful status code
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: {projectName: parsedInput.data.name},
},
};
}
});
@@ -1,9 +1,10 @@
import {z} from "zod"; import {z} from "zod";
import Database from "@prisma/client"
export const ProjectSchema = z.object({ export const ProjectSchema = z.object({
name: z.string(), name: z.string(),
slug: z.string(), slug: z.string(),
databases: z.array(z.string()),
}); });
export type ProjectSchema = z.infer<typeof ProjectSchema>; export type ProjectSchema = z.infer<typeof ProjectSchema>;
@@ -0,0 +1,156 @@
"use client";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
useZodForm
} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Form} from "@/components/ui/form"
import {Button} from "@/components/ui/button";
import {useMutation} from "@tanstack/react-query";
import {ProjectSchema} from "@/components/wrappers/Project/ProjectForm.schema";
import {createProjectAction} from "@/components/wrappers/Project/ProjectForm.action";
import {useRouter} from "next/navigation";
import {Database, Organization} from "@prisma/client"
import {MultiSelect} from "@/components/wrappers/MultiSelect/MultiSelect";
import {ZodString} from "zod";
export type projectFormProps = {
defaultValues?: ProjectSchema;
databases: Database[],
organization: Organization,
projectId?: string;
}
export const ProjectForm = (props: projectFormProps) => {
const router = useRouter();
const isCreate = !Boolean(props.defaultValues)
const formatDatabasesList = (databases: Database[]) => {
return databases.map(database => ({
value: database.id,
label: `${database.name} | ${database.generatedId}`,
}));
};
const formatDefaultDatabases = (databases: ProjectSchema["databases"]): string[] => {
return databases.map(database => database.id);
};
const formattedDefaultValues = {
...props.defaultValues,
databases: !isCreate ? formatDefaultDatabases(props.defaultValues?.databases) : []
}
const form = useZodForm({
schema: ProjectSchema,
defaultValues: formattedDefaultValues,
});
const mutation = useMutation({
mutationFn: async (values: ProjectSchema) => {
console.log(values)
const projectCreated = await createProjectAction({data: values, organizationId: props.organization.id});
console.log(projectCreated)
//
// if (data) {
// toast.success(`Success`);
// router.push(`/dashboard/projects/${data.id}`);
// router.refresh()
// }
//
// if (validationErrors) {
// toast.success(`Error`);
//
// }
}
})
return (
<Card>
<CardHeader>
</CardHeader>
<CardContent>
<Form form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Project 1" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input
placeholder="project-1" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="databases"
render={({field}) => (
<FormItem>
<FormLabel>Databases</FormLabel>
<FormControl>
<MultiSelect
options={formatDatabasesList(props.databases)}
onValueChange={field.onChange}
defaultValue={field.value ?? []}
placeholder="Select databases"
variant="inverted"
animation={2}
// maxCount={100}
/>
</FormControl>
<FormDescription>Select databases you want to add to this project</FormDescription>
<FormMessage/>
</FormItem>
)
}
/>
<Button>
Create Project
</Button>
</Form>
</CardContent>
</Card>
)
}
+28 -12
View File
@@ -20,12 +20,14 @@ import {
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover" } from "@/components/ui/popover"
import {FormControl} from "@/components/ui/form"; import {FormControl} from "@/components/ui/form";
import {SidebarMenuButton} from "@/components/ui/sidebar";
export type comboBoxProps = { export type comboBoxProps = {
values: Array<{ value: string, label: string }> values: Array<{ value: string, label: string }>
defaultValue?: string defaultValue?: string
onValueChange?: any onValueChange?: any
searchField?: boolean searchField?: boolean
sideBar?: boolean
} }
@@ -41,19 +43,33 @@ export function ComboBox(props: comboBoxProps) {
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button {props.sideBar ?
variant="outline" <SidebarMenuButton>
role="combobox" {value
aria-expanded={open} ? choices.find((choice) => choice.value === value)?.label
className="w-full justify-between" : "Select choice..."}
> <ChevronDown className="ml-auto"/>
{value </SidebarMenuButton>
? choices.find((choice) => choice.value === value)?.label :
: "Select choice..."} <Button
<ChevronDown className="opacity-50"/> variant="outline"
</Button> role="combobox"
aria-expanded={open}
className="w-full justify-between"
>
{value
? choices.find((choice) => choice.value === value)?.label
: "Select choice..."}
<ChevronDown className="opacity-50"/>
</Button>
}
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0 popover-content-width-full"> <PopoverContent
className="p-0 popover-content-width-full"
>
<Command> <Command>
{searchField ? <CommandInput placeholder="Search choice..." className="h-9"/> : null} {searchField ? <CommandInput placeholder="Search choice..." className="h-9"/> : null}
<CommandList> <CommandList>
@@ -1,21 +0,0 @@
"use server"
import {userAction} from "@/safe-actions";
import {prisma} from "@/prisma";
import {ProjectSchema} from "@/components/wrappers/project/ProjectForm.schema";
export const createProjectAction = userAction
.schema(ProjectSchema)
.action(async ({parsedInput, ctx}) => {
// Verify if slug already exist
// await verifySlugUniqueness(parsedInput.slug);
console.log("ctx", ctx)
return prisma.project.create({
data: {
...parsedInput,
}
});
});
@@ -1,99 +0,0 @@
"use client";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import {FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Form} from "@/components/ui/form"
import {Button} from "@/components/ui/button";
import {useMutation} from "@tanstack/react-query";
import {ProjectSchema} from "@/components/wrappers/project/ProjectForm.schema";
import {createProjectAction} from "@/components/wrappers/project/ProjectForm.action";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {useSession} from "next-auth/react";
export type projectFormProps = {
defaultValues?: ProjectSchema;
}
export const ProjectForm = (props: projectFormProps) => {
const router = useRouter();
const {data: session} = useSession()
console.log("organization", session)
const form = useZodForm({
schema: ProjectSchema,
});
const mutation = useMutation({
mutationFn: async (values: ProjectSchema) => {
const {data, validationErrors} = await createProjectAction({...values, organizationId: organization?.id});
if (data) {
toast.success(`Success`);
router.push(`/dashboard/projects/${data.id}`);
router.refresh()
}
if (validationErrors) {
toast.success(`Error`);
}
}
})
return (
<Card>
<CardHeader>
</CardHeader>
<CardContent>
<Form form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Project 1" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input
placeholder="project-1" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<Button>
Create Project
</Button>
</Form>
</CardContent>
</Card>
)
}
+1 -1
View File
@@ -74,7 +74,7 @@ async function createDefaultOrganization() {
} }
}) })
if (!defaultOrganization) { if (!defaultOrganization) {
console.log("==== Creating default organization... ====\n") console.log("==== Creating default Organization... ====\n")
await prisma.organization.create({ await prisma.organization.create({
data: { data: {
...defaultOrganizationConf ...defaultOrganizationConf