chore(release): 1.2.4-rc.1

This commit is contained in:
Théo LAGACHE
2026-01-28 15:54:03 +01:00
parent 4942591d7f
commit 2a6d631719
34 changed files with 329 additions and 331 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ function DialogContent({
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
"max-h-[90vh] overflow-y-auto",
"max-h-[90vh] overflow-y-auto overflow-x-hidden",
className
)}
{...props}
@@ -1,20 +1,22 @@
import Link from "next/link";
import {cn} from "@/lib/utils";
import {Plus} from "lucide-react";
import {forwardRef} from "react";
type EmptyStatePlaceholderProps = {
url?: string;
onClick?: () => void;
text: string;
className?: string;
};
} & React.HTMLAttributes<HTMLDivElement>;
export const EmptyStatePlaceholder = ({
export const EmptyStatePlaceholder = forwardRef<HTMLDivElement, EmptyStatePlaceholderProps>(({
url,
onClick,
text,
className,
}: EmptyStatePlaceholderProps) => {
...props
}, ref) => {
const Container = (
<div
className={cn(
@@ -37,5 +39,7 @@ export const EmptyStatePlaceholder = ({
);
}
return <div className={cn(className)}>{Container}</div>;
};
return <div className={cn(className)} ref={ref} {...props}>{Container}</div>;
});
EmptyStatePlaceholder.displayName = "EmptyStatePlaceholder";
@@ -15,7 +15,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, 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", {
const multiSelectVariants = cva("m-1 transition ease-in-out duration-300", {
variants: {
variant: {
default: "border-foreground/10 text-foreground bg-card hover:bg-card/80",
@@ -177,13 +177,13 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
return (
<Badge
key={value}
className={cn(isAnimating ? "animate-bounce" : "", multiSelectVariants({ variant }))}
className={cn(isAnimating ? "animate-bounce" : "", "max-w-full", multiSelectVariants({ variant }))}
style={{ animationDuration: `${animation}s` }}
>
{IconComponent && <IconComponent className="h-4 w-4 mr-2" />}
{option?.label}
<span className="truncate">{option?.label}</span>
<XCircle
className="ml-2 h-4 w-4 cursor-pointer"
className="ml-2 h-4 w-4 cursor-pointer shrink-0"
onClick={(event) => {
event.stopPropagation();
toggleOption(value);
@@ -196,6 +196,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
<Badge
className={cn(
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
"max-w-full",
isAnimating ? "animate-bounce" : "",
multiSelectVariants({ variant })
)}
@@ -203,7 +204,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
>
{`+ ${selectedValues.length - maxCount} more`}
<XCircle
className="ml-2 h-4 w-4 cursor-pointer"
className="ml-2 h-4 w-4 cursor-pointer shrink-0"
onClick={(event) => {
event.stopPropagation();
clearExtraOptions();
@@ -6,7 +6,7 @@ import {Trash2} from "lucide-react";
import {toast} from "sonner";
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
import {authClient} from "@/lib/auth/auth-client";
import {deleteOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
import {deleteOrganizationAction} from "@/features/organization/organization.action";
export type ButtonDeleteFleetProps = {
text?: string;
@@ -13,7 +13,7 @@ import {Input} from "@/components/ui/input";
import {authClient} from "@/lib/auth/auth-client";
import {toast} from "sonner";
import {OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
import {updateOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
import {updateOrganizationAction} from "@/features/organization/organization.action";
type UpdateOrganizationFormProps = {
onSuccessAction?: () => void;
@@ -1,48 +0,0 @@
"use server";
import {ActionError, userAction} from "@/lib/safe-actions/actions";
import {AgentSchema} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.schema";
import {z} from "zod";
import {eq, and, ne, count} from "drizzle-orm";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {slugify} from "@/utils/slugify";
const verifySlugUniqueness = async (slug: string, agentId?: string) => {
const conditions = agentId ? and(eq(drizzleDb.schemas.agent.slug, slug), ne(drizzleDb.schemas.agent.id, agentId)) : eq(drizzleDb.schemas.agent.slug, slug);
const [countResult] = await db.select({count: count()}).from(drizzleDb.schemas.agent).where(conditions);
if (countResult.count > 0) {
throw new ActionError("Slug already exists");
}
};
export const createAgentAction = userAction.schema(AgentSchema).action(async ({parsedInput}) => {
const slug = slugify(parsedInput.name);
await verifySlugUniqueness(slug);
const [createdAgent] = await db.insert(drizzleDb.schemas.agent).values({...parsedInput, slug: slug}).returning();
return {
data: createdAgent,
};
});
export const updateAgentAction = userAction
.schema(
z.object({
id: z.string(),
data: AgentSchema,
})
)
.action(async ({parsedInput}) => {
const slug = slugify(parsedInput.data.name);
await verifySlugUniqueness(slug, parsedInput.id);
const [updatedAgent] = await db.update(drizzleDb.schemas.agent).set({
...parsedInput.data,
slug: slug
}).where(eq(drizzleDb.schemas.agent.id, parsedInput.id)).returning();
return {
data: updatedAgent,
};
});
@@ -1,8 +0,0 @@
import { z } from "zod";
export const AgentSchema = z.object({
name: z.string(),
description: z.string(),
});
export type AgentType = z.infer<typeof AgentSchema>;
@@ -1,109 +0,0 @@
"use client";
import {Card, CardContent} 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 {useRouter} from "next/navigation";
import {useMutation} from "@tanstack/react-query";
import {TooltipProvider} from "@/components/ui/tooltip";
import {AgentSchema, AgentType} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.schema";
import {toast} from "sonner";
import {createAgentAction, updateAgentAction} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.action";
export type agentFormProps = {
defaultValues?: AgentType;
agentId?: string;
};
export const AgentForm = (props: agentFormProps) => {
const isCreate = !Boolean(props.defaultValues);
const form = useZodForm({
schema: AgentSchema,
defaultValues: props.defaultValues,
});
const router = useRouter();
const mutation = useMutation({
mutationFn: async (values: AgentType) => {
const createAgent = isCreate
? await createAgentAction(values)
: await updateAgentAction({
id: props.agentId ?? "-",
data: values,
});
const data = createAgent?.data?.data;
if (createAgent?.serverError || !data) {
toast.error(createAgent?.serverError);
return;
}
toast.success(`Success ${isCreate ? "creating" : "updating"} agent`);
router.refresh();
router.push(`/dashboard/agents/${data.id}`);
},
});
return (
<TooltipProvider>
<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="Agent 1" {...field} />
</FormControl>
<FormDescription>Your agent project name</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
defaultValue=""
name="description"
render={({field}) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input placeholder="This agent is for the client exemple.com" {...field}
value={field.value ?? ""}/>
</FormControl>
<FormDescription>Enter your project agent description</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit">
{isCreate ? "Create" : "Update"}
</Button>
</div>
</Form>
</TooltipProvider>
);
};
@@ -6,8 +6,8 @@ import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle} from "@/
import {Button} from "@/components/ui/button";
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
import {createOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
import {CreateOrganizationSchema, CreateOrganizationType} from "@/features/organization/organization.schema";
import {createOrganizationAction} from "@/features/organization/organization.action";
import {useRouter} from "next/navigation";
import {useState} from "react";
import {authClient} from "@/lib/auth/auth-client";
@@ -32,12 +32,12 @@ export function CreateOrganizationModal({
const router = useRouter();
const form = useZodForm({
schema: OrganizationSchema,
schema: CreateOrganizationSchema,
});
const mutation = useMutation({
mutationFn: async (values: OrganizationSchema) => {
mutationFn: async (values: CreateOrganizationType) => {
const result = await createOrganizationAction(values);
@@ -1,6 +1,6 @@
"use client";
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
import {deleteOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
import {deleteOrganizationAction} from "@/features/organization/organization.action";
import {useMutation} from "@tanstack/react-query";
import {useRouter} from "next/navigation";
import {toast} from "sonner";
@@ -1,14 +0,0 @@
import {z} from "zod";
export const OrganizationFormSchema = z.object({
name: z.string().min(5, 'Name must be at least 5 characters long').max(40, 'Name must be at most 40 characters long'),
slug: z.string()
.regex(/^[a-zA-Z0-9_-]*$/, 'Slug can only contain letters, numbers, underscores, and hyphens')
.min(5, 'Slug must be at least 5 characters long')
.max(20, 'Slug must be at most 20 characters long'),
users: z.array(z.string()),
});
export type OrganizationFormType = z.infer<typeof OrganizationFormSchema>;
@@ -1,167 +0,0 @@
"use client";
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 {useRouter} from "next/navigation";
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
import {
OrganizationFormSchema,
OrganizationFormType
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
import {
updateOrganizationAction
} from "@/components/wrappers/dashboard/organization/organization.action";
import {toast} from "sonner";
import {User as BetterAuthUser} from "better-auth";
import {User} from "@/db/schema/02_user";
import {authClient} from "@/lib/auth/auth-client";
export type organizationFormProps = {
defaultValues?: OrganizationWithMembers;
users: User[];
currentUser: BetterAuthUser;
};
export const OrganizationForm = (props: organizationFormProps) => {
const {data: activeOrganization, refetch: refetchActiveOrga} = authClient.useActiveOrganization();
const {data: organizations, refetch} = authClient.useListOrganizations();
const router = useRouter();
const isCreate = !Boolean(props.defaultValues);
const formatUsersList = (users: User[]) => {
return users
.filter((user) => user.id !== props.currentUser.id)
.map((user) => ({
value: user.id,
label: `${user.name} | ${user.email}`,
}));
};
const formatDefaultUsers = (members: MemberWithUser[]): string[] => {
return members
.filter((member) => member.userId !== props.currentUser.id)
.map((member) => member.userId);
};
const formattedDefaultValues = {
name: props.defaultValues?.name,
slug: props.defaultValues?.slug,
users: !isCreate ? formatDefaultUsers(props.defaultValues?.members as MemberWithUser[]) : [],
};
const form = useZodForm({
schema: OrganizationFormSchema,
defaultValues: formattedDefaultValues,
});
const mutation = useMutation({
mutationFn: (values: OrganizationFormType) => updateOrganizationAction({
data: values,
organizationId: props.defaultValues?.id ?? ""
}),
onSuccess: async (result) => {
if (result?.data?.success) {
toast.success(result.data.actionSuccess?.message || "Organization updated successfully.");
refetch()
refetchActiveOrga()
router.push("/dashboard/settings");
} else {
// @ts-ignore
const errorMsg = result?.data?.actionError?.message || result?.data?.actionError?.messageParams?.message || "Failed to update the organization.";
toast.error(errorMsg);
}
},
onError: (error: any) => {
console.error("Mutation network error:", error);
toast.error(error?.message || "A network error occurred.");
},
});
return (
<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="Organization 1" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input
placeholder="project-1"
{...field}
onChange={(e) => {
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
field.onChange(value);
}}
/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="users"
render={({field}) => (
<FormItem>
<FormLabel>Users</FormLabel>
<FormControl>
<MultiSelect
options={formatUsersList(props.users)}
onValueChange={field.onChange}
defaultValue={field.value ?? []}
placeholder="Select users"
variant="inverted"
animation={2}
// maxCount={100}
/>
</FormControl>
<FormDescription>Select users you want to add to this organization</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit">
{isCreate ? "Create" : "Update"}
</Button>
</div>
</Form>
);
};
@@ -1,224 +0,0 @@
"use server";
import {userAction} from "@/lib/safe-actions/actions";
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
import {ServerActionResult} from "@/types/action-type";
import {z} from "zod";
import {
OrganizationFormSchema
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
import {db} from "@/db";
import {and, eq, inArray, or} from "drizzle-orm";
import {auth, checkSlugOrganization, createOrganization} from "@/lib/auth/auth";
import {slugify} from "@/utils/slugify";
import {Organization} from "@/db/schema/03_organization";
import * as drizzleDb from "@/db";
export const createOrganizationAction = userAction.schema(OrganizationSchema).action(async ({parsedInput}): Promise<ServerActionResult<Organization>> => {
try {
const slug = slugify(parsedInput.name);
if (!await checkSlugOrganization(slug)) {
return {
success: false,
actionError: {
message: "Slug is already taken",
status: 500,
messageParams: {message: "Error creating the organization"},
},
};
}
let createdOrganization: Organization;
try {
createdOrganization = await createOrganization(parsedInput.name, slug) as unknown as Organization;
} catch (authError: any) {
console.error("Auth deletion failed:", authError);
return {
success: false,
actionError: {
message: authError.message || "Authentication service error.",
status: authError.status || 500,
cause: "auth_error",
messageParams: {message: authError.message},
},
};
}
return {
success: true,
value: createdOrganization,
actionSuccess: {
message: "Organization has been successfully created.",
messageParams: {organizationId: createdOrganization!.id},
},
};
} catch (error) {
console.error("Error creating organization:", error);
return {
success: false,
actionError: {
message: "Failed to create organization.",
status: 500,
messageParams: {message: "Error creating the organization"},
},
};
}
});
export const updateOrganizationAction = userAction
.schema(
z.object({
data: OrganizationFormSchema,
organizationId: z.string(),
})
)
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
try {
const newUserList = parsedInput.data.users;
const organization = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.id, parsedInput.organizationId),
with: {
members: true,
}
});
if (!organization) {
return {
success: false,
actionError: {
message: "Organization not found.",
status: 404,
cause: "not_found",
},
};
}
const existingItemIds = organization.members
.filter((member) => member.userId !== ctx.user.id)
.map((member) => member.userId);
const usersToAdd = newUserList.filter((id) => !existingItemIds.includes(id));
const usersToRemove = existingItemIds.filter((id) => !newUserList.includes(id));
if (usersToAdd.length > 0) {
for (const userToAdd of usersToAdd) {
await auth.api.addMember({
body: {
userId: userToAdd,
role: "member",
organizationId: organization.id,
},
});
}
}
if (usersToRemove.length > 0) {
await db.delete(drizzleDb.schemas.member).where(and(inArray(drizzleDb.schemas.member.userId, usersToRemove), eq(drizzleDb.schemas.member.organizationId, organization.id))).execute();
}
const updatedOrganization = await db
.update(drizzleDb.schemas.organization)
.set({
name: parsedInput.data.name,
slug: parsedInput.data.slug,
})
.where(eq(drizzleDb.schemas.organization.id, parsedInput.organizationId))
.returning()
.execute();
return {
success: true,
value: updatedOrganization as unknown as Organization,
actionSuccess: {
message: "Organization has been successfully updated.",
messageParams: {organizationId: organization.id},
},
};
} catch (error) {
console.error("Error updating organization:", error);
return {
success: false,
actionError: {
message: "Failed to update organization.",
status: 500,
cause: "server_error",
messageParams: {message: "Error updating the organization"},
},
};
}
});
export const deleteOrganizationAction = userAction.schema(
z.object({
id: z.string().optional(),
slug: z.string().optional(),
})
).action(
async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
try {
const conditions = [];
if (parsedInput.id) {
conditions.push(eq(drizzleDb.schemas.organization.id, parsedInput.id));
}
if (parsedInput.slug) {
conditions.push(eq(drizzleDb.schemas.organization.slug, parsedInput.slug));
}
const org = await db.query.organization.findFirst({
where: or(...conditions),
});
if (!org) {
return {
success: false,
actionError: {
message: "Organization not found.",
status: 404,
cause: "not_found",
},
};
}
let deletedOrganization: Organization;
try {
[deletedOrganization] = await db
.delete(drizzleDb.schemas.organization)
.where(eq(drizzleDb.schemas.organization.id, org.id))
.returning();
} catch (authError: any) {
console.error("Auth deletion failed:", authError);
return {
success: false,
actionError: {
message: authError.message || "Authentication service error.",
status: authError.status || 500,
cause: "auth_error",
messageParams: {message: authError.message},
},
};
}
return {
success: true,
value: deletedOrganization,
actionSuccess: {
message: "Organization has been successfully deleted.",
messageParams: {organizationId: deletedOrganization.id},
},
};
} catch (error) {
console.error("Unexpected error in deleteOrganizationAction:", error);
return {
success: false,
actionError: {
message: "Failed to delete organization due to a server error.",
status: 500,
cause: "server_error",
messageParams: {message: "Internal server error while deleting the organization"},
},
};
}
}
);
@@ -1,12 +0,0 @@
import { z } from "zod";
export const OrganizationSchema = z.object({
name: z.string().min(5, "Name must be at least 5 characters long").max(40, "Name must be at most 40 characters long"),
// slug: z
// .string()
// .regex(/^[a-zA-Z0-9_-]*$/, "Slug can only contain letters, numbers, underscores, and hyphens")
// .min(5, "Slug must be at least 5 characters long")
// .max(20, "Slug must be at most 20 characters long"),
});
export type OrganizationSchema = z.infer<typeof OrganizationSchema>;
@@ -1,20 +0,0 @@
"use client"
import {buttonVariants} from "@/components/ui/button";
import {GearIcon} from "@radix-ui/react-icons";
import Link from "next/link";
import {usePathname} from "next/navigation";
export type EditButtonSettings = {}
export const EditButtonSettings= (props:EditButtonSettings) => {
const pathname = usePathname();
return(
<Link className={buttonVariants({variant: "outline"})}
href={`${pathname}/edit/`}
>
<GearIcon className="w-7 h-7"/>
</Link>
)
}
@@ -1,152 +0,0 @@
"use server";
import {userAction} from "@/lib/safe-actions/actions";
import {ProjectSchema} from "@/components/wrappers/dashboard/projects/project-form/project-form.schema";
import {z} from "zod";
import {ServerActionResult} from "@/types/action-type";
import {db} from "@/db";
import {and, eq, inArray} from "drizzle-orm";
import {Project} from "@/db/schema/06_project";
import * as drizzleDb from "@/db";
import {Database} from "@/db/schema/07_database";
import {slugify} from "@/utils/slugify";
export const createProjectAction = userAction
.schema(
z.object({
data: ProjectSchema,
organizationId: z.string(),
})
)
.action(async ({parsedInput}): Promise<ServerActionResult<Project>> => {
try {
const slug = slugify(parsedInput.data.name);
const existingProject = await db.query.project.findFirst({
where: and(eq(drizzleDb.schemas.project.name, parsedInput.data.name)),
})
if (existingProject) {
return {
success: false,
actionError: {
message: "A project with this name already exists.",
status: 400,
messageParams: {projectName: parsedInput.data.name},
},
};
}
const [createdProject] = await db
.insert(drizzleDb.schemas.project)
.values({
name: parsedInput.data.name,
slug: slug,
organizationId: parsedInput.organizationId,
})
.returning();
if (parsedInput.data.databases.length > 0) {
await db
.update(drizzleDb.schemas.database)
.set({projectId: createdProject.id})
.where(inArray(drizzleDb.schemas.database.id, parsedInput.data.databases));
}
return {
success: true,
value: createdProject,
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,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: {projectName: parsedInput.data.name},
},
};
}
});
export const updateProjectAction = userAction
.schema(
z.object({
data: ProjectSchema,
organizationId: z.string(),
projectId: z.string(),
})
)
.action(async ({parsedInput}): Promise<ServerActionResult<Project>> => {
try {
const existing = await db.query.project.findFirst({
where: eq(drizzleDb.schemas.project.id, parsedInput.projectId),
with: {
databases: true,
},
});
if (!existing) {
throw new Error("Project not found.");
}
const existingDbIds = existing.databases.map((db: Database) => db.id);
const newDbIds = parsedInput.data.databases;
const databasesToAdd = newDbIds.filter((id) => !existingDbIds.includes(id));
const databasesToRemove = existingDbIds.filter((id: string) => !newDbIds.includes(id));
if (databasesToAdd.length > 0) {
await db.update(drizzleDb.schemas.database).set({projectId: parsedInput.projectId}).where(inArray(drizzleDb.schemas.database.id, databasesToAdd));
}
if (databasesToRemove.length > 0) {
await db.update(drizzleDb.schemas.database).set({
projectId: null,
backupPolicy: null
}).where(inArray(drizzleDb.schemas.database.id, databasesToRemove));
await db.delete(drizzleDb.schemas.retentionPolicy)
.where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databasesToRemove)).execute();
await db.delete(drizzleDb.schemas.alertPolicy)
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databasesToRemove)).execute();
}
// const slug = slugify(parsedInput.data.name);
const [updatedProject] = await db
.update(drizzleDb.schemas.project)
.set({
name: parsedInput.data.name,
// slug: slug,
})
.where(eq(drizzleDb.schemas.project.id, parsedInput.projectId))
.returning();
return {
success: true,
value: updatedProject,
actionSuccess: {
message: "Project has been successfully updated.",
messageParams: {projectName: parsedInput.data.name},
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "Failed to update project.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: {projectName: parsedInput.data.name},
},
};
}
});
@@ -1,8 +0,0 @@
import { z } from "zod";
export const ProjectSchema = z.object({
name: z.string(),
databases: z.array(z.string()),
});
export type ProjectType = z.infer<typeof ProjectSchema>;
@@ -1,131 +0,0 @@
"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, ProjectType } from "@/components/wrappers/dashboard/projects/project-form/project-form.schema";
import { createProjectAction, updateProjectAction } from "@/components/wrappers/dashboard/projects/project-form/project-form.action";
import { useRouter } from "next/navigation";
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
import { toast } from "sonner";
import {DatabaseWith} from "@/db/schema/07_database";
import {Organization} from "@/db/schema/03_organization";
export type projectFormProps = {
defaultValues?: ProjectType;
databases: DatabaseWith[];
organization: Organization;
projectId?: string;
};
export const ProjectForm = (props: projectFormProps) => {
const router = useRouter();
const isCreate = !Boolean(props.defaultValues);
const formatDatabasesList = (databases: DatabaseWith[]) => {
return databases.map((database) => ({
value: database.id,
label: `${database.name} (${database.agentDatabaseId}) | ${database.agent?.name}`,
}));
};
const formatDefaultDatabases = (databases: string[]): string[] => {
return databases;
};
const formattedDefaultValues = {
...props.defaultValues,
databases: !isCreate ? formatDefaultDatabases(props.defaultValues?.databases ?? []) : [],
};
const form = useZodForm({
schema: ProjectSchema,
defaultValues: formattedDefaultValues,
});
const mutation = useMutation({
mutationFn: async (values: ProjectType) => {
if (!isCreate && !props.projectId) {
throw new Error("Project ID is required for updates");
}
const project = isCreate
? await createProjectAction({
data: values,
organizationId: props.organization.id,
})
: await updateProjectAction({
data: values,
organizationId: props.organization.id,
projectId: props.projectId!,
});
if (project && project.data) {
if (project.data.success) {
project.data.actionSuccess && toast.success(project.data.actionSuccess.message);
router.refresh();
router.push(`/dashboard/projects/${project.data.value!.id}`);
} else {
project.data.actionError && toast.error(project.data.actionError.message || "Unknown error occurred.");
router.refresh();
}
} else {
toast.error("Failed to process request. No response received.");
router.refresh();
}
},
});
return (
<Form
form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Project 1" {...field} value={field.value ?? ""} />
</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>
)}
/>
<div className="flex justify-end">
<Button type="submit">
{isCreate ? "Create" : "Update"}
</Button>
</div>
</Form>
);
};