diff --git a/CITATION.cff b/CITATION.cff index 17655d75..154995bd 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -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" diff --git a/app/(customer)/dashboard/(admin)/agents/[agentId]/edit/page.tsx b/app/(customer)/dashboard/(admin)/agents/[agentId]/edit/page.tsx deleted file mode 100644 index 474bc98c..00000000 --- a/app/(customer)/dashboard/(admin)/agents/[agentId]/edit/page.tsx +++ /dev/null @@ -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 ( - - - Edit {agent.name} - - - - - - ); -} diff --git a/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx b/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx index 19a978e6..b1080be8 100644 --- a/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx +++ b/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx @@ -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 }>)
- - - + +
+ +
+
diff --git a/app/(customer)/dashboard/(admin)/agents/new/page.tsx b/app/(customer)/dashboard/(admin)/agents/new/page.tsx deleted file mode 100644 index d752c535..00000000 --- a/app/(customer)/dashboard/(admin)/agents/new/page.tsx +++ /dev/null @@ -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 ( - - - Create new agent - - - - - - ); -} diff --git a/app/(customer)/dashboard/(admin)/agents/page.tsx b/app/(customer)/dashboard/(admin)/agents/page.tsx index 230026a7..21d81a66 100644 --- a/app/(customer)/dashboard/(admin)/agents/page.tsx +++ b/app/(customer)/dashboard/(admin)/agents/page.tsx @@ -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<{}>) { Agents {agents.length > 0 && ( - + - + )} @@ -46,12 +46,13 @@ export default async function RoutePage(props: PageParams<{}>) { {agents.length > 0 ? ( ) : ( - + + + )} ); -} +} \ No newline at end of file diff --git a/app/(customer)/dashboard/(organization)/projects/[projectId]/edit/page.tsx b/app/(customer)/dashboard/(organization)/projects/[projectId]/edit/page.tsx deleted file mode 100644 index 5cd2bf72..00000000 --- a/app/(customer)/dashboard/(organization)/projects/[projectId]/edit/page.tsx +++ /dev/null @@ -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 ( - - - Edit {proj.name} - - - db.id)}} - projectId={proj.id} - /> - - - ); -} diff --git a/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx b/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx index 7e8223ca..ed50900e 100644 --- a/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/[projectId]/page.tsx @@ -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 && (
- - - + +
+ +
+
@@ -95,4 +115,4 @@ export default async function RoutePage(props: PageParams<{ ); -} +} \ No newline at end of file diff --git a/app/(customer)/dashboard/(organization)/projects/new/page.tsx b/app/(customer)/dashboard/(organization)/projects/new/page.tsx deleted file mode 100644 index 7cfb3f1d..00000000 --- a/app/(customer)/dashboard/(organization)/projects/new/page.tsx +++ /dev/null @@ -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 ( - - - Create new project - - - - - - ); -} diff --git a/app/(customer)/dashboard/(organization)/projects/page.tsx b/app/(customer)/dashboard/(organization)/projects/page.tsx index 3160ea9d..395a1c4d 100644 --- a/app/(customer)/dashboard/(organization)/projects/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/page.tsx @@ -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 ( @@ -43,9 +56,9 @@ export default async function RoutePage(props: PageParams<{}>) { Projects {(projects.length > 0 && !isMember) && ( - + - + )} @@ -62,9 +75,11 @@ export default async function RoutePage(props: PageParams<{}>) { ) : isMember ? ( ) : ( - + + + )} ); -} +} \ No newline at end of file diff --git a/app/(customer)/dashboard/(organization)/settings/edit/page.tsx b/app/(customer)/dashboard/(organization)/settings/edit/page.tsx deleted file mode 100644 index 3d0ba2c8..00000000 --- a/app/(customer)/dashboard/(organization)/settings/edit/page.tsx +++ /dev/null @@ -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( - - - - Edit {organization.name} - - - - - - - ) -} \ No newline at end of file diff --git a/app/(customer)/dashboard/(organization)/settings/page.tsx b/app/(customer)/dashboard/(organization)/settings/page.tsx index 02bc5d12..0c3382be 100644 --- a/app/(customer)/dashboard/(organization)/settings/page.tsx +++ b/app/(customer)/dashboard/(organization)/settings/page.tsx @@ -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 ( @@ -41,7 +69,15 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
{permissions.canManageSettings && organization.slug !== "default" && ( - + + + )}
@@ -62,4 +98,4 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) { ) -} \ No newline at end of file +} diff --git a/package.json b/package.json index 6ef1a702..7f48ede0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 473b84ca..8b7de8d6 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -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} diff --git a/src/components/wrappers/common/empty-state-placeholder.tsx b/src/components/wrappers/common/empty-state-placeholder.tsx index a7e482c5..3339053c 100644 --- a/src/components/wrappers/common/empty-state-placeholder.tsx +++ b/src/components/wrappers/common/empty-state-placeholder.tsx @@ -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; -export const EmptyStatePlaceholder = ({ +export const EmptyStatePlaceholder = forwardRef(({ url, onClick, text, className, - }: EmptyStatePlaceholderProps) => { + ...props + }, ref) => { const Container = (
{Container}
; -}; + return
{Container}
; +}); + +EmptyStatePlaceholder.displayName = "EmptyStatePlaceholder"; \ No newline at end of file diff --git a/src/components/wrappers/common/multiselect/multi-select.tsx b/src/components/wrappers/common/multiselect/multi-select.tsx index cb679e06..4e533301 100644 --- a/src/components/wrappers/common/multiselect/multi-select.tsx +++ b/src/components/wrappers/common/multiselect/multi-select.tsx @@ -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 return ( {IconComponent && } - {option?.label} + {option?.label} { event.stopPropagation(); toggleOption(value); @@ -196,6 +196,7 @@ export const MultiSelect = React.forwardRef > {`+ ${selectedValues.length - maxCount} more`} { event.stopPropagation(); clearExtraOptions(); diff --git a/src/components/wrappers/dashboard/admin/organizations/organization/button-delete-organization.tsx b/src/components/wrappers/dashboard/admin/organizations/organization/button-delete-organization.tsx index 86b4bd97..f1dd75ae 100644 --- a/src/components/wrappers/dashboard/admin/organizations/organization/button-delete-organization.tsx +++ b/src/components/wrappers/dashboard/admin/organizations/organization/button-delete-organization.tsx @@ -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; diff --git a/src/components/wrappers/dashboard/admin/organizations/organization/details/update-organization-form.tsx b/src/components/wrappers/dashboard/admin/organizations/organization/details/update-organization-form.tsx index 9bb6bd4c..00035a45 100644 --- a/src/components/wrappers/dashboard/admin/organizations/organization/details/update-organization-form.tsx +++ b/src/components/wrappers/dashboard/admin/organizations/organization/details/update-organization-form.tsx @@ -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; diff --git a/src/components/wrappers/dashboard/organization/create-organisation-modal.tsx b/src/components/wrappers/dashboard/organization/create-organisation-modal.tsx index 2cbe3ec4..5532a996 100644 --- a/src/components/wrappers/dashboard/organization/create-organisation-modal.tsx +++ b/src/components/wrappers/dashboard/organization/create-organisation-modal.tsx @@ -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); diff --git a/src/components/wrappers/dashboard/organization/delete-organization-button.tsx b/src/components/wrappers/dashboard/organization/delete-organization-button.tsx index 6a5dc069..8fb2870f 100644 --- a/src/components/wrappers/dashboard/organization/delete-organization-button.tsx +++ b/src/components/wrappers/dashboard/organization/delete-organization-button.tsx @@ -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"; diff --git a/src/components/wrappers/dashboard/organization/organization-form/organization-form.schema.ts b/src/components/wrappers/dashboard/organization/organization-form/organization-form.schema.ts deleted file mode 100644 index 9d136cc5..00000000 --- a/src/components/wrappers/dashboard/organization/organization-form/organization-form.schema.ts +++ /dev/null @@ -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; diff --git a/src/components/wrappers/dashboard/organization/organization.schema.ts b/src/components/wrappers/dashboard/organization/organization.schema.ts deleted file mode 100644 index 6e2db975..00000000 --- a/src/components/wrappers/dashboard/organization/organization.schema.ts +++ /dev/null @@ -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; diff --git a/src/components/wrappers/dashboard/organization/settings/edit-button-settings/edit-button-settings.tsx b/src/components/wrappers/dashboard/organization/settings/edit-button-settings/edit-button-settings.tsx deleted file mode 100644 index 99cb339a..00000000 --- a/src/components/wrappers/dashboard/organization/settings/edit-button-settings/edit-button-settings.tsx +++ /dev/null @@ -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( - - - - ) -} \ No newline at end of file diff --git a/src/components/wrappers/dashboard/agent/agent-form/agent-form.action.ts b/src/features/agents/agents.action.ts similarity index 94% rename from src/components/wrappers/dashboard/agent/agent-form/agent-form.action.ts rename to src/features/agents/agents.action.ts index 77a94df3..93fa782a 100644 --- a/src/components/wrappers/dashboard/agent/agent-form/agent-form.action.ts +++ b/src/features/agents/agents.action.ts @@ -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"; diff --git a/src/components/wrappers/dashboard/agent/agent-form/agent-form.schema.ts b/src/features/agents/agents.schema.ts similarity index 74% rename from src/components/wrappers/dashboard/agent/agent-form/agent-form.schema.ts rename to src/features/agents/agents.schema.ts index c05b27ed..2d36a1b5 100644 --- a/src/components/wrappers/dashboard/agent/agent-form/agent-form.schema.ts +++ b/src/features/agents/agents.schema.ts @@ -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(), }); diff --git a/src/features/agents/components/agent.dialog.tsx b/src/features/agents/components/agent.dialog.tsx new file mode 100644 index 00000000..67d095d6 --- /dev/null +++ b/src/features/agents/components/agent.dialog.tsx @@ -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 ( + + + {children ? children : } + + + + {isEdit ? `Edit ${agent.name}` : "Create new agent"} + + setOpen(false)} + defaultValues={agent} + agentId={agent?.id} + /> + + + ); +}; \ No newline at end of file diff --git a/src/components/wrappers/dashboard/agent/agent-form/agent-form.tsx b/src/features/agents/components/agent.form.tsx similarity index 90% rename from src/components/wrappers/dashboard/agent/agent-form/agent-form.tsx rename to src/features/agents/components/agent.form.tsx index ee85a77f..8fd796ed 100644 --- a/src/components/wrappers/dashboard/agent/agent-form/agent-form.tsx +++ b/src/features/agents/components/agent.form.tsx @@ -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}`); + } }, }); diff --git a/src/features/organization/components/edit-organization.dialog.tsx b/src/features/organization/components/edit-organization.dialog.tsx new file mode 100644 index 00000000..4904faf8 --- /dev/null +++ b/src/features/organization/components/edit-organization.dialog.tsx @@ -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 ( + + + {children ? children : ( + + )} + + + + Edit {organization.name} + + setOpen(false)} + defaultValues={organization} + users={users} + currentUser={currentUser} + /> + + + ); +}; diff --git a/src/components/wrappers/dashboard/organization/organization-form/organization-form.tsx b/src/features/organization/components/organization.form.tsx similarity index 91% rename from src/components/wrappers/dashboard/organization/organization-form/organization-form.tsx rename to src/features/organization/components/organization.form.tsx index 8f22800a..872ff70b 100644 --- a/src/components/wrappers/dashboard/organization/organization-form/organization-form.tsx +++ b/src/features/organization/components/organization.form.tsx @@ -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."; diff --git a/src/components/wrappers/dashboard/organization/organization.action.ts b/src/features/organization/organization.action.ts similarity index 94% rename from src/components/wrappers/dashboard/organization/organization.action.ts rename to src/features/organization/organization.action.ts index 59ef73db..ecccc329 100644 --- a/src/components/wrappers/dashboard/organization/organization.action.ts +++ b/src/features/organization/organization.action.ts @@ -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> => { +export const createOrganizationAction = userAction.schema(CreateOrganizationSchema).action(async ({parsedInput}): Promise> => { 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(), }) ) diff --git a/src/features/organization/organization.schema.ts b/src/features/organization/organization.schema.ts new file mode 100644 index 00000000..c23c4425 --- /dev/null +++ b/src/features/organization/organization.schema.ts @@ -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; +export type UpdateOrganizationType = z.infer; diff --git a/src/features/projects/components/project.dialog.tsx b/src/features/projects/components/project.dialog.tsx new file mode 100644 index 00000000..7f7ad305 --- /dev/null +++ b/src/features/projects/components/project.dialog.tsx @@ -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 ( + + + {children ? children : } + + + + {isEdit ? `Edit ${project.name}` : "Create new project"} + + setOpen(false)} + databases={databases} + organization={organization} + defaultValues={project ? { + ...project, + databases: project.databases.map(db => db.id) + } : undefined} + projectId={project?.id} + /> + + + ); +}; \ No newline at end of file diff --git a/src/components/wrappers/dashboard/projects/project-form/project-form.tsx b/src/features/projects/components/project.form.tsx similarity index 90% rename from src/components/wrappers/dashboard/projects/project-form/project-form.tsx rename to src/features/projects/components/project.form.tsx index 81026bb9..9b41d404 100644 --- a/src/components/wrappers/dashboard/projects/project-form/project-form.tsx +++ b/src/features/projects/components/project.form.tsx @@ -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(); diff --git a/src/components/wrappers/dashboard/projects/project-form/project-form.action.ts b/src/features/projects/projects.action.ts similarity index 96% rename from src/components/wrappers/dashboard/projects/project-form/project-form.action.ts rename to src/features/projects/projects.action.ts index af44a47e..24097443 100644 --- a/src/components/wrappers/dashboard/projects/project-form/project-form.action.ts +++ b/src/features/projects/projects.action.ts @@ -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(); diff --git a/src/components/wrappers/dashboard/projects/project-form/project-form.schema.ts b/src/features/projects/projects.schema.ts similarity index 76% rename from src/components/wrappers/dashboard/projects/project-form/project-form.schema.ts rename to src/features/projects/projects.schema.ts index ac98c212..ef7c0266 100644 --- a/src/components/wrappers/dashboard/projects/project-form/project-form.schema.ts +++ b/src/features/projects/projects.schema.ts @@ -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()), });