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;
@@ -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,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,6 +1,6 @@
"use server";
import {ActionError, userAction} from "@/lib/safe-actions/actions";
import {AgentSchema} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.schema";
import {AgentSchema} from "@/features/agents/agents.schema";
import {z} from "zod";
import {eq, and, ne, count} from "drizzle-orm";
import {db} from "@/db";
@@ -1,7 +1,7 @@
import { z } from "zod";
export const AgentSchema = z.object({
name: z.string(),
name: z.string().nonempty("Name is required"),
description: z.string(),
});
@@ -0,0 +1,42 @@
"use client";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {AgentForm} from "@/features/agents/components/agent.form";
import {useState} from "react";
import {Button} from "@/components/ui/button";
import {Plus} from "lucide-react";
import {AgentType} from "@/features/agents/agents.schema";
type AgentDialogProps = {
children?: React.ReactNode;
agent?: AgentType & { id: string };
};
export const AgentDialog = ({children, agent}: AgentDialogProps) => {
const [open, setOpen] = useState(false);
const isEdit = !!agent;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{children ? children : <Button><Plus className="mr-2 h-4 w-4"/> Create Agent</Button>}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{isEdit ? `Edit ${agent.name}` : "Create new agent"}</DialogTitle>
</DialogHeader>
<AgentForm
onSuccess={() => setOpen(false)}
defaultValues={agent}
agentId={agent?.id}
/>
</DialogContent>
</Dialog>
);
};
@@ -1,6 +1,5 @@
"use client";
import {Card, CardContent} from "@/components/ui/card";
import {
FormControl,
FormDescription,
@@ -16,13 +15,14 @@ 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 {AgentSchema, AgentType} from "@/features/agents/agents.schema";
import {toast} from "sonner";
import {createAgentAction, updateAgentAction} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.action";
import {createAgentAction, updateAgentAction} from "@/features/agents/agents.action";
export type agentFormProps = {
defaultValues?: AgentType;
agentId?: string;
onSuccess?: (data: any) => void;
};
export const AgentForm = (props: agentFormProps) => {
@@ -52,7 +52,12 @@ export const AgentForm = (props: agentFormProps) => {
}
toast.success(`Success ${isCreate ? "creating" : "updating"} agent`);
router.refresh();
router.push(`/dashboard/agents/${data.id}`);
if (props.onSuccess) {
props.onSuccess(data);
} else {
router.push(`/dashboard/agents/${data.id}`);
}
},
});
@@ -0,0 +1,50 @@
"use client";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {OrganizationForm} from "@/features/organization/components/organization.form";
import {useState} from "react";
import {Button, buttonVariants} from "@/components/ui/button";
import {GearIcon} from "@radix-ui/react-icons";
import {OrganizationWithMembers} from "@/db/schema/03_organization";
import {User} from "@/db/schema/02_user";
import {User as BetterAuthUser} from "better-auth";
type EditOrganizationDialogProps = {
children?: React.ReactNode;
organization: OrganizationWithMembers;
users: User[];
currentUser: BetterAuthUser;
};
export const EditOrganizationDialog = ({children, organization, users, currentUser}: EditOrganizationDialogProps) => {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{children ? children : (
<Button variant="outline">
<GearIcon className="w-4 h-4 mr-2"/> Edit
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit {organization.name}</DialogTitle>
</DialogHeader>
<OrganizationForm
onSuccess={() => setOpen(false)}
defaultValues={organization}
users={users}
currentUser={currentUser}
/>
</DialogContent>
</Dialog>
);
};
@@ -16,13 +16,13 @@ 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";
UpdateOrganizationSchema,
UpdateOrganizationType
} from "@/features/organization/organization.schema";
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
import {
updateOrganizationAction
} from "@/components/wrappers/dashboard/organization/organization.action";
} from "@/features/organization/organization.action";
import {toast} from "sonner";
import {User as BetterAuthUser} from "better-auth";
import {User} from "@/db/schema/02_user";
@@ -32,6 +32,7 @@ export type organizationFormProps = {
defaultValues?: OrganizationWithMembers;
users: User[];
currentUser: BetterAuthUser;
onSuccess?: (data: any) => void;
};
export const OrganizationForm = (props: organizationFormProps) => {
@@ -64,12 +65,12 @@ export const OrganizationForm = (props: organizationFormProps) => {
};
const form = useZodForm({
schema: OrganizationFormSchema,
schema: UpdateOrganizationSchema,
defaultValues: formattedDefaultValues,
});
const mutation = useMutation({
mutationFn: (values: OrganizationFormType) => updateOrganizationAction({
mutationFn: (values: UpdateOrganizationType) => updateOrganizationAction({
data: values,
organizationId: props.defaultValues?.id ?? ""
}),
@@ -78,7 +79,11 @@ export const OrganizationForm = (props: organizationFormProps) => {
toast.success(result.data.actionSuccess?.message || "Organization updated successfully.");
refetch()
refetchActiveOrga()
router.push("/dashboard/settings");
if (props.onSuccess) {
props.onSuccess(result.data.value);
} else {
router.push("/dashboard/settings");
}
} else {
// @ts-ignore
const errorMsg = result?.data?.actionError?.message || result?.data?.actionError?.messageParams?.message || "Failed to update the organization.";
@@ -1,12 +1,9 @@
"use server";
import {userAction} from "@/lib/safe-actions/actions";
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
import {CreateOrganizationSchema, UpdateOrganizationSchema} from "@/features/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";
@@ -14,7 +11,7 @@ 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>> => {
export const createOrganizationAction = userAction.schema(CreateOrganizationSchema).action(async ({parsedInput}): Promise<ServerActionResult<Organization>> => {
try {
const slug = slugify(parsedInput.name);
if (!await checkSlugOrganization(slug)) {
@@ -69,7 +66,7 @@ export const createOrganizationAction = userAction.schema(OrganizationSchema).ac
export const updateOrganizationAction = userAction
.schema(
z.object({
data: OrganizationFormSchema,
data: UpdateOrganizationSchema,
organizationId: z.string(),
})
)
@@ -0,0 +1,17 @@
import { z } from "zod";
export const CreateOrganizationSchema = 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"),
});
export const UpdateOrganizationSchema = 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 CreateOrganizationType = z.infer<typeof CreateOrganizationSchema>;
export type UpdateOrganizationType = z.infer<typeof UpdateOrganizationSchema>;
@@ -0,0 +1,51 @@
"use client";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {ProjectForm} from "@/features/projects/components/project.form";
import {useState} from "react";
import {Button} from "@/components/ui/button";
import {Plus} from "lucide-react";
import {DatabaseWith} from "@/db/schema/07_database";
import {Organization} from "@/db/schema/03_organization";
import {ProjectWith} from "@/db/schema/06_project";
type ProjectDialogProps = {
children?: React.ReactNode;
databases: DatabaseWith[];
organization: Organization;
project?: ProjectWith;
};
export const ProjectDialog = ({children, databases, organization, project}: ProjectDialogProps) => {
const [open, setOpen] = useState(false);
const isEdit = !!project;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{children ? children : <Button><Plus className="mr-2 h-4 w-4"/> Create Project</Button>}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{isEdit ? `Edit ${project.name}` : "Create new project"}</DialogTitle>
</DialogHeader>
<ProjectForm
onSuccess={() => setOpen(false)}
databases={databases}
organization={organization}
defaultValues={project ? {
...project,
databases: project.databases.map(db => db.id)
} : undefined}
projectId={project?.id}
/>
</DialogContent>
</Dialog>
);
};
@@ -1,13 +1,12 @@
"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 { ProjectSchema, ProjectType } from "@/features/projects/projects.schema";
import { createProjectAction, updateProjectAction } from "@/features/projects/projects.action";
import { useRouter } from "next/navigation";
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
import { toast } from "sonner";
@@ -19,6 +18,7 @@ export type projectFormProps = {
databases: DatabaseWith[];
organization: Organization;
projectId?: string;
onSuccess?: (data: any) => void;
};
export const ProjectForm = (props: projectFormProps) => {
@@ -27,7 +27,7 @@ export const ProjectForm = (props: projectFormProps) => {
const formatDatabasesList = (databases: DatabaseWith[]) => {
return databases.map((database) => ({
value: database.id,
label: `${database.name} (${database.agentDatabaseId}) | ${database.agent?.name}`,
label: `${database.name} | ${database.agent?.name}`,
}));
};
@@ -65,7 +65,11 @@ export const ProjectForm = (props: projectFormProps) => {
if (project.data.success) {
project.data.actionSuccess && toast.success(project.data.actionSuccess.message);
router.refresh();
router.push(`/dashboard/projects/${project.data.value!.id}`);
if (props.onSuccess) {
props.onSuccess(project.data.value);
} else {
router.push(`/dashboard/projects/${project.data.value!.id}`);
}
} else {
project.data.actionError && toast.error(project.data.actionError.message || "Unknown error occurred.");
router.refresh();
@@ -1,7 +1,7 @@
"use server";
import {userAction} from "@/lib/safe-actions/actions";
import {ProjectSchema} from "@/components/wrappers/dashboard/projects/project-form/project-form.schema";
import {ProjectSchema} from "@/features/projects/projects.schema";
import {z} from "zod";
import {ServerActionResult} from "@/types/action-type";
import {db} from "@/db";
@@ -119,13 +119,11 @@ export const updateProjectAction = userAction
.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();
@@ -1,7 +1,7 @@
import { z } from "zod";
export const ProjectSchema = z.object({
name: z.string(),
name: z.string().nonempty("Name is required"),
databases: z.array(z.string()),
});