mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Merge branch 'main' into feat/github-updater
# Conflicts: # CITATION.cff # package.json
This commit is contained in:
@@ -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 {generateEdgeKey} from "@/utils/edge_key";
|
||||||
import {getServerUrl} from "@/utils/get-server-url";
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content";
|
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 }>) {
|
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
|
||||||
|
|
||||||
@@ -39,10 +41,11 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>)
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link className={buttonVariants({variant: "outline"})}
|
<AgentDialog agent={agent as AgentType & { id: string }}>
|
||||||
href={`/dashboard/agents/${agent.id}/edit`}>
|
<div className={buttonVariants({variant: "outline", className: "cursor-pointer"})}>
|
||||||
<GearIcon className="w-7 h-7"/>
|
<GearIcon className="w-7 h-7"/>
|
||||||
</Link>
|
</div>
|
||||||
|
</AgentDialog>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
|
<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 {AgentCard} from "@/components/wrappers/dashboard/agent/agent-card/agent-card";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import Link from "next/link";
|
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
@@ -10,6 +9,7 @@ import * as drizzleDb from "@/db";
|
|||||||
import {desc, eq, not} from "drizzle-orm";
|
import {desc, eq, not} from "drizzle-orm";
|
||||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
|
import {AgentDialog} from "@/features/agents/components/agent.dialog";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Agents",
|
title: "Agents",
|
||||||
@@ -36,9 +36,9 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
<PageTitle>Agents</PageTitle>
|
<PageTitle>Agents</PageTitle>
|
||||||
{agents.length > 0 && (
|
{agents.length > 0 && (
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<Link href={"/dashboard/agents/new"}>
|
<AgentDialog>
|
||||||
<Button>+ Create Agent</Button>
|
<Button>+ Create Agent</Button>
|
||||||
</Link>
|
</AgentDialog>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
@@ -46,12 +46,13 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
{agents.length > 0 ? (
|
{agents.length > 0 ? (
|
||||||
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1}/>
|
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1}/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyStatePlaceholder
|
<AgentDialog>
|
||||||
url={"/dashboard/agents/new"}
|
<EmptyStatePlaceholder
|
||||||
text={"Create new Agent"}
|
text={"Create new Agent"}
|
||||||
/>
|
/>
|
||||||
|
</AgentDialog>
|
||||||
)}
|
)}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</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 {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 {buttonVariants} from "@/components/ui/button";
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
import {GearIcon} from "@radix-ui/react-icons";
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
import {
|
||||||
ButtonDeleteProject
|
ButtonDeleteProject
|
||||||
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
|
} 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 {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
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<{
|
export default async function RoutePage(props: PageParams<{
|
||||||
@@ -51,6 +53,19 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
redirect("/dashboard/projects");
|
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";
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -63,10 +78,15 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
{!isMember && (
|
{!isMember && (
|
||||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link className={buttonVariants({variant: "outline"})}
|
<ProjectDialog
|
||||||
href={`/dashboard/projects/${proj.id}/edit`}>
|
databases={availableDatabases}
|
||||||
<GearIcon className="w-7 h-7"/>
|
organization={org}
|
||||||
</Link>
|
project={proj as ProjectWith}
|
||||||
|
>
|
||||||
|
<div className={buttonVariants({variant: "outline", className: "cursor-pointer"})}>
|
||||||
|
<GearIcon className="w-7 h-7"/>
|
||||||
|
</div>
|
||||||
|
</ProjectDialog>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
||||||
@@ -95,4 +115,4 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</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 {PageParams} from "@/types/next";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
@@ -10,6 +8,8 @@ import {notFound} from "next/navigation";
|
|||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
import {Metadata} from "next";
|
import {Metadata} from "next";
|
||||||
|
import {ProjectDialog} from "@/features/projects/components/project.dialog";
|
||||||
|
import {DatabaseWith} from "@/db/schema/07_database";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Projects",
|
title: "Projects",
|
||||||
@@ -36,6 +36,19 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
});
|
});
|
||||||
const isMember = activeMember?.role === "member";
|
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 (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
@@ -43,9 +56,9 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
<PageTitle>Projects</PageTitle>
|
<PageTitle>Projects</PageTitle>
|
||||||
{(projects.length > 0 && !isMember) && (
|
{(projects.length > 0 && !isMember) && (
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<Link href={`/dashboard/projects/new`}>
|
<ProjectDialog databases={availableDatabases} organization={organization}>
|
||||||
<Button>+ Create Project</Button>
|
<Button>+ Create Project</Button>
|
||||||
</Link>
|
</ProjectDialog>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
@@ -62,9 +75,11 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
) : isMember ? (
|
) : isMember ? (
|
||||||
<EmptyStatePlaceholder text="No project available"/>
|
<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>
|
</PageContent>
|
||||||
</Page>
|
</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 {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
||||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
||||||
import {DeleteOrganizationButton} from "@/components/wrappers/dashboard/organization/delete-organization-button";
|
import {DeleteOrganizationButton} from "@/components/wrappers/dashboard/organization/delete-organization-button";
|
||||||
import {
|
import {EditOrganizationDialog} from "@/features/organization/components/edit-organization.dialog";
|
||||||
EditButtonSettings
|
import {Button} from "@/components/ui/button";
|
||||||
} from "@/components/wrappers/dashboard/organization/settings/edit-button-settings/edit-button-settings";
|
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 = {
|
export const metadata: Metadata = {
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
@@ -22,7 +26,7 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
const activeMember = await getActiveMember()
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
if (!organization || !activeMember) {
|
if (!organization || !activeMember || !user) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +34,30 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
const storageChannels = await getOrganizationStorageChannels(organization.id)
|
const storageChannels = await getOrganizationStorageChannels(organization.id)
|
||||||
const permissions = computeOrganizationPermissions(activeMember);
|
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 (
|
return (
|
||||||
<Page>
|
<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 md:justify-between w-full ">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{permissions.canManageSettings && organization.slug !== "default" && (
|
{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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -62,4 +98,4 @@ export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
|||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.2.4-rc.8",
|
"version": "1.2.4-rc.8",
|
||||||
|
"version": "1.2.4-rc.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 8887",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function DialogContent({
|
|||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
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",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
import {Plus} from "lucide-react";
|
import {Plus} from "lucide-react";
|
||||||
|
import {forwardRef} from "react";
|
||||||
|
|
||||||
type EmptyStatePlaceholderProps = {
|
type EmptyStatePlaceholderProps = {
|
||||||
url?: string;
|
url?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
text: string;
|
text: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
} & React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
export const EmptyStatePlaceholder = ({
|
export const EmptyStatePlaceholder = forwardRef<HTMLDivElement, EmptyStatePlaceholderProps>(({
|
||||||
url,
|
url,
|
||||||
onClick,
|
onClick,
|
||||||
text,
|
text,
|
||||||
className,
|
className,
|
||||||
}: EmptyStatePlaceholderProps) => {
|
...props
|
||||||
|
}, ref) => {
|
||||||
const Container = (
|
const Container = (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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.
|
* Variants for the multi-select component to handle different styles.
|
||||||
* Uses class-variance-authority (cva) to define different styles based on "variant" prop.
|
* 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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "border-foreground/10 text-foreground bg-card hover:bg-card/80",
|
default: "border-foreground/10 text-foreground bg-card hover:bg-card/80",
|
||||||
@@ -177,13 +177,13 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
key={value}
|
key={value}
|
||||||
className={cn(isAnimating ? "animate-bounce" : "", multiSelectVariants({ variant }))}
|
className={cn(isAnimating ? "animate-bounce" : "", "max-w-full", multiSelectVariants({ variant }))}
|
||||||
style={{ animationDuration: `${animation}s` }}
|
style={{ animationDuration: `${animation}s` }}
|
||||||
>
|
>
|
||||||
{IconComponent && <IconComponent className="h-4 w-4 mr-2" />}
|
{IconComponent && <IconComponent className="h-4 w-4 mr-2" />}
|
||||||
{option?.label}
|
<span className="truncate">{option?.label}</span>
|
||||||
<XCircle
|
<XCircle
|
||||||
className="ml-2 h-4 w-4 cursor-pointer"
|
className="ml-2 h-4 w-4 cursor-pointer shrink-0"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
toggleOption(value);
|
toggleOption(value);
|
||||||
@@ -196,6 +196,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|||||||
<Badge
|
<Badge
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
|
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
|
||||||
|
"max-w-full",
|
||||||
isAnimating ? "animate-bounce" : "",
|
isAnimating ? "animate-bounce" : "",
|
||||||
multiSelectVariants({ variant })
|
multiSelectVariants({ variant })
|
||||||
)}
|
)}
|
||||||
@@ -203,7 +204,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|||||||
>
|
>
|
||||||
{`+ ${selectedValues.length - maxCount} more`}
|
{`+ ${selectedValues.length - maxCount} more`}
|
||||||
<XCircle
|
<XCircle
|
||||||
className="ml-2 h-4 w-4 cursor-pointer"
|
className="ml-2 h-4 w-4 cursor-pointer shrink-0"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
clearExtraOptions();
|
clearExtraOptions();
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import {Trash2} from "lucide-react";
|
|||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||||
import {authClient} from "@/lib/auth/auth-client";
|
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 = {
|
export type ButtonDeleteFleetProps = {
|
||||||
text?: string;
|
text?: string;
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ import {Input} from "@/components/ui/input";
|
|||||||
import {authClient} from "@/lib/auth/auth-client";
|
import {authClient} from "@/lib/auth/auth-client";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
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 = {
|
type UpdateOrganizationFormProps = {
|
||||||
onSuccessAction?: () => void;
|
onSuccessAction?: () => void;
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle} from "@/
|
|||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
||||||
import {Input} from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
|
import {CreateOrganizationSchema, CreateOrganizationType} from "@/features/organization/organization.schema";
|
||||||
import {createOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
import {createOrganizationAction} from "@/features/organization/organization.action";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {authClient} from "@/lib/auth/auth-client";
|
import {authClient} from "@/lib/auth/auth-client";
|
||||||
@@ -32,12 +32,12 @@ export function CreateOrganizationModal({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const form = useZodForm({
|
const form = useZodForm({
|
||||||
schema: OrganizationSchema,
|
schema: CreateOrganizationSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (values: OrganizationSchema) => {
|
mutationFn: async (values: CreateOrganizationType) => {
|
||||||
|
|
||||||
const result = await createOrganizationAction(values);
|
const result = await createOrganizationAction(values);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
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 {useMutation} from "@tanstack/react-query";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
|
|||||||
-14
@@ -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>;
|
|
||||||
-20
@@ -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
-1
@@ -1,6 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import {ActionError, userAction} from "@/lib/safe-actions/actions";
|
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 {z} from "zod";
|
||||||
import {eq, and, ne, count} from "drizzle-orm";
|
import {eq, and, ne, count} from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const AgentSchema = z.object({
|
export const AgentSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string().nonempty("Name is required"),
|
||||||
description: z.string(),
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+9
-4
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {Card, CardContent} from "@/components/ui/card";
|
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
FormDescription,
|
||||||
@@ -16,13 +15,14 @@ import {Button} from "@/components/ui/button";
|
|||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {useMutation} from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
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 {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 = {
|
export type agentFormProps = {
|
||||||
defaultValues?: AgentType;
|
defaultValues?: AgentType;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
|
onSuccess?: (data: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AgentForm = (props: agentFormProps) => {
|
export const AgentForm = (props: agentFormProps) => {
|
||||||
@@ -52,7 +52,12 @@ export const AgentForm = (props: agentFormProps) => {
|
|||||||
}
|
}
|
||||||
toast.success(`Success ${isCreate ? "creating" : "updating"} agent`);
|
toast.success(`Success ${isCreate ? "creating" : "updating"} agent`);
|
||||||
router.refresh();
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+12
-7
@@ -16,13 +16,13 @@ import {useMutation} from "@tanstack/react-query";
|
|||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
|
import {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
|
||||||
import {
|
import {
|
||||||
OrganizationFormSchema,
|
UpdateOrganizationSchema,
|
||||||
OrganizationFormType
|
UpdateOrganizationType
|
||||||
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
|
} from "@/features/organization/organization.schema";
|
||||||
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
|
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||||
import {
|
import {
|
||||||
updateOrganizationAction
|
updateOrganizationAction
|
||||||
} from "@/components/wrappers/dashboard/organization/organization.action";
|
} from "@/features/organization/organization.action";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {User as BetterAuthUser} from "better-auth";
|
import {User as BetterAuthUser} from "better-auth";
|
||||||
import {User} from "@/db/schema/02_user";
|
import {User} from "@/db/schema/02_user";
|
||||||
@@ -32,6 +32,7 @@ export type organizationFormProps = {
|
|||||||
defaultValues?: OrganizationWithMembers;
|
defaultValues?: OrganizationWithMembers;
|
||||||
users: User[];
|
users: User[];
|
||||||
currentUser: BetterAuthUser;
|
currentUser: BetterAuthUser;
|
||||||
|
onSuccess?: (data: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OrganizationForm = (props: organizationFormProps) => {
|
export const OrganizationForm = (props: organizationFormProps) => {
|
||||||
@@ -64,12 +65,12 @@ export const OrganizationForm = (props: organizationFormProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const form = useZodForm({
|
const form = useZodForm({
|
||||||
schema: OrganizationFormSchema,
|
schema: UpdateOrganizationSchema,
|
||||||
defaultValues: formattedDefaultValues,
|
defaultValues: formattedDefaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: (values: OrganizationFormType) => updateOrganizationAction({
|
mutationFn: (values: UpdateOrganizationType) => updateOrganizationAction({
|
||||||
data: values,
|
data: values,
|
||||||
organizationId: props.defaultValues?.id ?? ""
|
organizationId: props.defaultValues?.id ?? ""
|
||||||
}),
|
}),
|
||||||
@@ -78,7 +79,11 @@ export const OrganizationForm = (props: organizationFormProps) => {
|
|||||||
toast.success(result.data.actionSuccess?.message || "Organization updated successfully.");
|
toast.success(result.data.actionSuccess?.message || "Organization updated successfully.");
|
||||||
refetch()
|
refetch()
|
||||||
refetchActiveOrga()
|
refetchActiveOrga()
|
||||||
router.push("/dashboard/settings");
|
if (props.onSuccess) {
|
||||||
|
props.onSuccess(result.data.value);
|
||||||
|
} else {
|
||||||
|
router.push("/dashboard/settings");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const errorMsg = result?.data?.actionError?.message || result?.data?.actionError?.messageParams?.message || "Failed to update the organization.";
|
const errorMsg = result?.data?.actionError?.message || result?.data?.actionError?.messageParams?.message || "Failed to update the organization.";
|
||||||
+3
-6
@@ -1,12 +1,9 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import {userAction} from "@/lib/safe-actions/actions";
|
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 {ServerActionResult} from "@/types/action-type";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
import {
|
|
||||||
OrganizationFormSchema
|
|
||||||
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
|
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {and, eq, inArray, or} from "drizzle-orm";
|
import {and, eq, inArray, or} from "drizzle-orm";
|
||||||
import {auth, checkSlugOrganization, createOrganization} from "@/lib/auth/auth";
|
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 {Organization} from "@/db/schema/03_organization";
|
||||||
import * as drizzleDb from "@/db";
|
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 {
|
try {
|
||||||
const slug = slugify(parsedInput.name);
|
const slug = slugify(parsedInput.name);
|
||||||
if (!await checkSlugOrganization(slug)) {
|
if (!await checkSlugOrganization(slug)) {
|
||||||
@@ -69,7 +66,7 @@ export const createOrganizationAction = userAction.schema(OrganizationSchema).ac
|
|||||||
export const updateOrganizationAction = userAction
|
export const updateOrganizationAction = userAction
|
||||||
.schema(
|
.schema(
|
||||||
z.object({
|
z.object({
|
||||||
data: OrganizationFormSchema,
|
data: UpdateOrganizationSchema,
|
||||||
organizationId: z.string(),
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+15
-5
@@ -1,13 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|
||||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Form } from "@/components/ui/form";
|
import { Form } from "@/components/ui/form";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { ProjectSchema, ProjectType } from "@/components/wrappers/dashboard/projects/project-form/project-form.schema";
|
import { ProjectSchema, ProjectType } from "@/features/projects/projects.schema";
|
||||||
import { createProjectAction, updateProjectAction } from "@/components/wrappers/dashboard/projects/project-form/project-form.action";
|
import { createProjectAction, updateProjectAction } from "@/features/projects/projects.action";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
|
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -19,15 +18,22 @@ export type projectFormProps = {
|
|||||||
databases: DatabaseWith[];
|
databases: DatabaseWith[];
|
||||||
organization: Organization;
|
organization: Organization;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
onSuccess?: (data: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const ProjectForm = (props: projectFormProps) => {
|
export const ProjectForm = (props: projectFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isCreate = !Boolean(props.defaultValues);
|
const isCreate = !Boolean(props.defaultValues);
|
||||||
const formatDatabasesList = (databases: DatabaseWith[]) => {
|
const formatDatabasesList = (databases: DatabaseWith[]) => {
|
||||||
return databases.map((database) => ({
|
return databases.map((database) => ({
|
||||||
value: database.id,
|
value: database.id,
|
||||||
label: `${database.name} (${database.agentDatabaseId}) | ${database.agent?.name}`,
|
label: `${database.name} | ${database.agent?.name}`,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,7 +71,11 @@ export const ProjectForm = (props: projectFormProps) => {
|
|||||||
if (project.data.success) {
|
if (project.data.success) {
|
||||||
project.data.actionSuccess && toast.success(project.data.actionSuccess.message);
|
project.data.actionSuccess && toast.success(project.data.actionSuccess.message);
|
||||||
router.refresh();
|
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 {
|
} else {
|
||||||
project.data.actionError && toast.error(project.data.actionError.message || "Unknown error occurred.");
|
project.data.actionError && toast.error(project.data.actionError.message || "Unknown error occurred.");
|
||||||
router.refresh();
|
router.refresh();
|
||||||
+1
-3
@@ -1,7 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import {userAction} from "@/lib/safe-actions/actions";
|
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 {z} from "zod";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
@@ -119,13 +119,11 @@ export const updateProjectAction = userAction
|
|||||||
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databasesToRemove)).execute();
|
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databasesToRemove)).execute();
|
||||||
|
|
||||||
}
|
}
|
||||||
// const slug = slugify(parsedInput.data.name);
|
|
||||||
|
|
||||||
const [updatedProject] = await db
|
const [updatedProject] = await db
|
||||||
.update(drizzleDb.schemas.project)
|
.update(drizzleDb.schemas.project)
|
||||||
.set({
|
.set({
|
||||||
name: parsedInput.data.name,
|
name: parsedInput.data.name,
|
||||||
// slug: slug,
|
|
||||||
})
|
})
|
||||||
.where(eq(drizzleDb.schemas.project.id, parsedInput.projectId))
|
.where(eq(drizzleDb.schemas.project.id, parsedInput.projectId))
|
||||||
.returning();
|
.returning();
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const ProjectSchema = z.object({
|
export const ProjectSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string().nonempty("Name is required"),
|
||||||
databases: z.array(z.string()),
|
databases: z.array(z.string()),
|
||||||
});
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user