mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on project.
This commit is contained in:
+10
-12
@@ -6,6 +6,7 @@ import {env} from "@/env.mjs";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
|
||||
|
||||
|
||||
export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({
|
||||
adapter: PrismaAdapter(prisma),
|
||||
theme: {
|
||||
@@ -78,22 +79,20 @@ export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({
|
||||
// // session.user.authMethod = user.authMethod;
|
||||
// return session
|
||||
// },
|
||||
async jwt({token, trigger, session, user}) {
|
||||
async jwt({ token, trigger, session, user }) {
|
||||
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}) {
|
||||
session.user = token.user;
|
||||
// session.user = token;
|
||||
async session({ session, token, user }) {
|
||||
session.user = token;
|
||||
return session;
|
||||
},
|
||||
async signIn({account, user, profile}) {
|
||||
async signIn({ account, user, profile }) {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {email: user.email},
|
||||
where: { email: user.email },
|
||||
});
|
||||
|
||||
if (!existingUser) {
|
||||
@@ -126,7 +125,7 @@ export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({
|
||||
|
||||
// Update auth method if user exists
|
||||
await prisma.user.update({
|
||||
where: {id: existingUser.id},
|
||||
where: { id: existingUser.id },
|
||||
data: {
|
||||
authMethod: account.provider,
|
||||
},
|
||||
@@ -136,5 +135,4 @@ export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({
|
||||
},
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -3,7 +3,6 @@ import {User} from "@prisma/client";
|
||||
|
||||
export const currentUser = async () => {
|
||||
const session = await baseAuth();
|
||||
console.log("mysession", session)
|
||||
if (!session?.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import {
|
||||
SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenu, SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
import {LoggedInButton} from "@/components/wrappers/Dashboard/LoggedInButton/LoggedInButton";
|
||||
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";
|
||||
|
||||
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";
|
||||
+6
-1
@@ -8,6 +8,7 @@ import {Organization} from "@prisma/client";
|
||||
export type organizationComboBoxProps = {
|
||||
organizations: Organization[]
|
||||
defaultOrganization: Organization
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +32,10 @@ export function OrganizationComboBox(props: organizationComboBoxProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ComboBox values={values} defaultValue={defaultOrganization.id} onValueChange={updateSession}/>
|
||||
<ComboBox
|
||||
sideBar={true}
|
||||
values={values}
|
||||
defaultValue={defaultOrganization.id}
|
||||
onValueChange={updateSession}/>
|
||||
)
|
||||
}
|
||||
+2
@@ -11,12 +11,14 @@ export const ProjectCard = (props: projectCardProps) => {
|
||||
|
||||
const {data: project} = props;
|
||||
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/projects/${project.id}`}>
|
||||
<Card className="flex flex-row justify-between">
|
||||
<div className="">
|
||||
<CardHeader>{project.name}</CardHeader>
|
||||
<CardContent>
|
||||
{project.databases.length} databases
|
||||
</CardContent>
|
||||
</div>
|
||||
</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},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
import {z} from "zod";
|
||||
|
||||
import Database from "@prisma/client"
|
||||
|
||||
export const ProjectSchema = z.object({
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
databases: z.array(z.string()),
|
||||
});
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -20,12 +20,14 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import {FormControl} from "@/components/ui/form";
|
||||
import {SidebarMenuButton} from "@/components/ui/sidebar";
|
||||
|
||||
export type comboBoxProps = {
|
||||
values: Array<{ value: string, label: string }>
|
||||
defaultValue?: string
|
||||
onValueChange?: any
|
||||
searchField?: boolean
|
||||
sideBar?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -41,19 +43,33 @@ export function ComboBox(props: comboBoxProps) {
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
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>
|
||||
{props.sideBar ?
|
||||
<SidebarMenuButton>
|
||||
{value
|
||||
? choices.find((choice) => choice.value === value)?.label
|
||||
: "Select choice..."}
|
||||
<ChevronDown className="ml-auto"/>
|
||||
</SidebarMenuButton>
|
||||
:
|
||||
<Button
|
||||
variant="outline"
|
||||
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>
|
||||
<PopoverContent className="p-0 popover-content-width-full">
|
||||
<PopoverContent
|
||||
|
||||
className="p-0 popover-content-width-full"
|
||||
>
|
||||
<Command>
|
||||
{searchField ? <CommandInput placeholder="Search choice..." className="h-9"/> : null}
|
||||
<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
@@ -74,7 +74,7 @@ async function createDefaultOrganization() {
|
||||
}
|
||||
})
|
||||
if (!defaultOrganization) {
|
||||
console.log("==== Creating default organization... ====\n")
|
||||
console.log("==== Creating default Organization... ====\n")
|
||||
await prisma.organization.create({
|
||||
data: {
|
||||
...defaultOrganizationConf
|
||||
|
||||
Reference in New Issue
Block a user