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
+2 -2
View File
@@ -26,5 +26,5 @@ keywords:
- web-ui
- agent
license: Apache-2.0
version: 1.2.3
date-released: "2026-01-24"
version: 1.2.4-rc.1
date-released: "2026-01-28"
@@ -1,32 +0,0 @@
import { PageParams } from "@/types/next";
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { AgentForm } from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
import { notFound } from "next/navigation";
import { db } from "@/db";
export default async function RoutePage(
props: PageParams<{
agentId: string;
}>
) {
const { agentId } = await props.params;
const agent = await db.query.agent.findFirst({
where: (fields, { eq }) => eq(fields.id, agentId),
});
if (!agent) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>Edit {agent.name}</PageTitle>
</PageHeader>
<PageContent>
<AgentForm defaultValues={agent} agentId={agent.id} />
</PageContent>
</Page>
);
}
@@ -12,6 +12,8 @@ import {capitalizeFirstLetter} from "@/utils/text";
import {generateEdgeKey} from "@/utils/edge_key";
import {getServerUrl} from "@/utils/get-server-url";
import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content";
import {AgentDialog} from "@/features/agents/components/agent.dialog";
import {AgentType} from "@/features/agents/agents.schema";
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
@@ -39,10 +41,11 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
</div>
<div className="flex items-center gap-2 md:justify-between w-full ">
<div className="flex items-center gap-2">
<Link className={buttonVariants({variant: "outline"})}
href={`/dashboard/agents/${agent.id}/edit`}>
<GearIcon className="w-7 h-7"/>
</Link>
<AgentDialog agent={agent as AgentType & { id: string }}>
<div className={buttonVariants({variant: "outline", className: "cursor-pointer"})}>
<GearIcon className="w-7 h-7"/>
</div>
</AgentDialog>
</div>
<div className="flex items-center gap-2">
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
@@ -1,21 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {AgentForm} from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
import {Metadata} from "next";
export const metadata: Metadata = {
title: "Create Agent",
};
export default async function RoutePage(props: PageParams<{}>) {
return (
<Page>
<PageHeader>
<PageTitle>Create new agent</PageTitle>
</PageHeader>
<PageContent>
<AgentForm/>
</PageContent>
</Page>
);
}
@@ -2,7 +2,6 @@ import {PageParams} from "@/types/next";
import {AgentCard} from "@/components/wrappers/dashboard/agent/agent-card/agent-card";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button";
import Link from "next/link";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {db} from "@/db";
@@ -10,6 +9,7 @@ import * as drizzleDb from "@/db";
import {desc, eq, not} from "drizzle-orm";
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
import {Metadata} from "next";
import {AgentDialog} from "@/features/agents/components/agent.dialog";
export const metadata: Metadata = {
title: "Agents",
@@ -36,9 +36,9 @@ export default async function RoutePage(props: PageParams<{}>) {
<PageTitle>Agents</PageTitle>
{agents.length > 0 && (
<PageActions>
<Link href={"/dashboard/agents/new"}>
<AgentDialog>
<Button>+ Create Agent</Button>
</Link>
</AgentDialog>
</PageActions>
)}
</PageHeader>
@@ -46,12 +46,13 @@ export default async function RoutePage(props: PageParams<{}>) {
{agents.length > 0 ? (
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1}/>
) : (
<EmptyStatePlaceholder
url={"/dashboard/agents/new"}
text={"Create new Agent"}
/>
<AgentDialog>
<EmptyStatePlaceholder
text={"Create new Agent"}
/>
</AgentDialog>
)}
</PageContent>
</Page>
);
}
}
@@ -1,65 +0,0 @@
import {notFound} from "next/navigation";
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {DatabaseWith} from "@/db/schema/07_database";
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
const {projectId} = await props.params;
const proj = await db.query.project.findFirst({
where: eq(drizzleDb.schemas.project.id, projectId),
with: {
databases: true,
},
});
if (!proj) {
notFound();
}
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, "default"),
});
if (!org) {
notFound();
}
const availableDatabases = (
await db.query.database.findMany({
where: (db, {or, eq, isNull}) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
)
// .filter((db) => db.project !== null) as DatabaseWith[];
return (
<Page>
<PageHeader>
<PageTitle>Edit {proj.name}</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm
organization={org}
databases={availableDatabases as DatabaseWith[]}
defaultValues={{...proj, databases: proj.databases.map((db) => db.id)}}
projectId={proj.id}
/>
</PageContent>
</Page>
);
}
@@ -1,8 +1,7 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {Page, PageContent, PageTitle} from "@/features/layout/page";
import {buttonVariants} from "@/components/ui/button";
import {GearIcon} from "@radix-ui/react-icons";
import Link from "next/link";
import {
ButtonDeleteProject
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
@@ -15,6 +14,9 @@ import {eq} from "drizzle-orm";
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db";
import {capitalizeFirstLetter} from "@/utils/text";
import {ProjectDialog} from "@/features/projects/components/project.dialog";
import {DatabaseWith} from "@/db/schema/07_database";
import {ProjectWith} from "@/db/schema/06_project";
export default async function RoutePage(props: PageParams<{
@@ -51,6 +53,19 @@ export default async function RoutePage(props: PageParams<{
redirect("/dashboard/projects");
}
const availableDatabases = (
await db.query.database.findMany({
where: (db, {or, eq, isNull}) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
) as DatabaseWith[];
const isMember = activeMember?.role === "member";
return (
@@ -63,10 +78,15 @@ export default async function RoutePage(props: PageParams<{
{!isMember && (
<div className="flex items-center gap-2 md:justify-between w-full ">
<div className="flex items-center gap-2">
<Link className={buttonVariants({variant: "outline"})}
href={`/dashboard/projects/${proj.id}/edit`}>
<GearIcon className="w-7 h-7"/>
</Link>
<ProjectDialog
databases={availableDatabases}
organization={org}
project={proj as ProjectWith}
>
<div className={buttonVariants({variant: "outline", className: "cursor-pointer"})}>
<GearIcon className="w-7 h-7"/>
</div>
</ProjectDialog>
</div>
<div className="flex items-center gap-2">
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
@@ -95,4 +115,4 @@ export default async function RoutePage(props: PageParams<{
</PageContent>
</Page>
);
}
}
@@ -1,50 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
import {notFound} from "next/navigation";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db";
import {DatabaseWith} from "@/db/schema/07_database";
export default async function RoutePage(props: PageParams<{}>) {
const organization = await getOrganization({});
if (!organization) {
notFound();
}
const availableDatabases = (
await db.query.database.findMany({
where: (db, {isNull}) => isNull(db.projectId),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
).filter((db) => db.project == null) as DatabaseWith[];
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
});
if (!org) notFound();
return (
<Page>
<PageHeader>
<PageTitle>Create new project</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm databases={availableDatabases} organization={org}/>
</PageContent>
</Page>
);
}
@@ -1,5 +1,3 @@
import Link from "next/link";
import {PageParams} from "@/types/next";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button";
@@ -10,6 +8,8 @@ import {notFound} from "next/navigation";
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
import {Metadata} from "next";
import {ProjectDialog} from "@/features/projects/components/project.dialog";
import {DatabaseWith} from "@/db/schema/07_database";
export const metadata: Metadata = {
title: "Projects",
@@ -36,6 +36,19 @@ export default async function RoutePage(props: PageParams<{}>) {
});
const isMember = activeMember?.role === "member";
const availableDatabases = (
await db.query.database.findMany({
where: (db, {isNull}) => isNull(db.projectId),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
).filter((db) => db.project == null) as DatabaseWith[];
return (
<Page>
@@ -43,9 +56,9 @@ export default async function RoutePage(props: PageParams<{}>) {
<PageTitle>Projects</PageTitle>
{(projects.length > 0 && !isMember) && (
<PageActions>
<Link href={`/dashboard/projects/new`}>
<ProjectDialog databases={availableDatabases} organization={organization}>
<Button>+ Create Project</Button>
</Link>
</ProjectDialog>
</PageActions>
)}
</PageHeader>
@@ -62,9 +75,11 @@ export default async function RoutePage(props: PageParams<{}>) {
) : isMember ? (
<EmptyStatePlaceholder text="No project available"/>
) : (
<EmptyStatePlaceholder url="/dashboard/projects/new" text="Create new Project"/>
<ProjectDialog databases={availableDatabases} organization={organization}>
<EmptyStatePlaceholder text="Create new Project"/>
</ProjectDialog>
)}
</PageContent>
</Page>
);
}
}
@@ -1,37 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/organization-form/organization-form";
import {getOrganization} from "@/lib/auth/auth";
import {db} from "@/db";
import {isNull} from "drizzle-orm";
import {currentUser} from "@/lib/auth/current-user";
export default async function RoutePage(props: PageParams<{
}>) {
const user = await currentUser();
const organization = await getOrganization({});
const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt)
});
if (!user || !users || !organization || organization.slug == "default") {
notFound();
}
return(
<Page>
<PageHeader>
<PageTitle>
Edit {organization.name}
</PageTitle>
</PageHeader>
<PageContent>
<OrganizationForm currentUser={user} users={users} defaultValues={organization}/>
</PageContent>
</Page>
)
}
@@ -9,9 +9,13 @@ import {getOrganizationChannels} from "@/db/services/notification-channel";
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
import {DeleteOrganizationButton} from "@/components/wrappers/dashboard/organization/delete-organization-button";
import {
EditButtonSettings
} from "@/components/wrappers/dashboard/organization/settings/edit-button-settings/edit-button-settings";
import {EditOrganizationDialog} from "@/features/organization/components/edit-organization.dialog";
import {Button} from "@/components/ui/button";
import {GearIcon} from "@radix-ui/react-icons";
import {db} from "@/db";
import {isNull} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {eq} from "drizzle-orm";
export const metadata: Metadata = {
title: "Settings",
@@ -22,7 +26,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
const user = await currentUser();
const activeMember = await getActiveMember()
if (!organization || !activeMember) {
if (!organization || !activeMember || !user) {
notFound();
}
@@ -30,6 +34,30 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
const storageChannels = await getOrganizationStorageChannels(organization.id)
const permissions = computeOrganizationPermissions(activeMember);
// Fetch users for the form
const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt)
});
// Re-fetch organization with members to match type expectations if needed,
// although getOrganization returns OrganizationWithMembers usually, let's verify or cast.
// getOrganization returns Organization type from database schema, which includes relations if defined in auth lib.
// However, OrganizationForm expects OrganizationWithMembers.
// Let's ensure we have members.
const organizationWithMembers = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.id, organization.id),
with: {
members: {
with: {
user: true
}
}
}
});
if (!organizationWithMembers) notFound();
return (
<Page>
@@ -41,7 +69,15 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
<div className="flex items-center gap-2 md:justify-between w-full ">
<div className="flex items-center gap-2">
{permissions.canManageSettings && organization.slug !== "default" && (
<EditButtonSettings/>
<EditOrganizationDialog
organization={organizationWithMembers}
users={users}
currentUser={user}
>
<Button variant="outline">
<GearIcon className="w-7 h-7"/>
</Button>
</EditOrganizationDialog>
)}
</div>
<div className="flex items-center gap-2">
@@ -62,4 +98,4 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
</PageContent>
</Page>
)
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "portabase",
"version": "1.2.3",
"version": "1.2.4-rc.1",
"private": true,
"scripts": {
"dev": "next dev --turbopack -p 8887",
+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()),
});