mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Refactoring the sidebar.
This commit is contained in:
+34
-23
@@ -1,38 +1,49 @@
|
||||
"use client"
|
||||
|
||||
import {CreateOrganizationModal} from "@/components/wrappers/dashboard/organization/create-organisation-modal";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {useState} from "react";
|
||||
import {Plus} from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger
|
||||
} from "@/components/ui/dialog";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {AdminOrganizationForm} from "@/components/wrappers/dashboard/admin/organization/admin-organization-form";
|
||||
import {Plus} from "lucide-react";
|
||||
|
||||
type AdminOrganizationAddModalProps = {}
|
||||
|
||||
|
||||
export const AdminOrganizationAddModal = (props: AdminOrganizationAddModalProps) => {
|
||||
const router = useRouter();
|
||||
const {data: organizations, refetch} = authClient.useListOrganizations();
|
||||
const {data: activeOrganization, refetch: refetchActiveOrga} = authClient.useActiveOrganization();
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
|
||||
if (!organizations) return null;
|
||||
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleReload = () => {
|
||||
refetch();
|
||||
refetchActiveOrga();
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpenModal(true);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus/> add
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>add organization</DialogTitle>
|
||||
<DialogDescription>
|
||||
your description
|
||||
</DialogDescription>
|
||||
<AdminOrganizationForm onSuccess={() => setOpen(false)}/>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<>
|
||||
|
||||
<Button onClick={handleOpen}>
|
||||
<Plus/> Create a new organization
|
||||
</Button>
|
||||
<CreateOrganizationModal
|
||||
redirect={"/dashboard/admin/organizations"}
|
||||
open={openModal}
|
||||
onSuccess={handleReload}
|
||||
onOpenChange={setOpenModal}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export function organizationsListColumns(): ColumnDef<OrganizationWithMembers>[]
|
||||
<ButtonDeleteOrganization organisationId={row.original.id}/>
|
||||
)}
|
||||
<Link className={buttonVariants({variant: "outline"})}
|
||||
href={`admin/organization/${row.original.id}`}>
|
||||
href={`organizations/${row.original.id}`}>
|
||||
<Settings/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
"use server";
|
||||
import { z } from "zod";
|
||||
import { EmailFormSchema } from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
|
||||
export const updateEmailSettingsAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
data: EmailFormSchema,
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const { name, data } = parsedInput;
|
||||
|
||||
const [updatedSettings] = await db
|
||||
.update(drizzleDb.schemas.setting)
|
||||
.set({
|
||||
...data,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.setting.name, name))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
data: updatedSettings,
|
||||
};
|
||||
});
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const EmailFormSchema = z.object({
|
||||
smtpPassword: z.string(),
|
||||
smtpFrom: z.string(),
|
||||
smtpHost: z.string(),
|
||||
smtpPort: z.string(),
|
||||
smtpUser: z.string(),
|
||||
});
|
||||
|
||||
export type EmailFormType = z.infer<typeof EmailFormSchema>;
|
||||
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent} from "@/components/ui/card";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useZodForm
|
||||
} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Form} from "@/components/ui/form";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
|
||||
import {
|
||||
EmailFormSchema,
|
||||
EmailFormType
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
import {
|
||||
updateEmailSettingsAction
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
|
||||
export type EmailFormProps = {
|
||||
defaultValues?: EmailFormType;
|
||||
};
|
||||
|
||||
export const EmailForm = (props: EmailFormProps) => {
|
||||
const form = useZodForm({
|
||||
schema: EmailFormSchema,
|
||||
defaultValues: props.defaultValues,
|
||||
});
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: EmailFormType) => {
|
||||
const updateEmailSettings = await updateEmailSettingsAction({name: "system", data: values});
|
||||
const data = updateEmailSettings?.data?.data;
|
||||
if (updateEmailSettings?.serverError || !data) {
|
||||
toast.error(updateEmailSettings?.serverError);
|
||||
return;
|
||||
}
|
||||
toast.success(`Success updating email informations`);
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4 mt-3"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpFrom"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>From Email *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"exemple@portabase.com"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"The email from where the email will be send"}</FormDescription>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpHost"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server Host *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"ssl0.ovh.net"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"Your email server host"}</FormDescription>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpPort"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server Port *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"465"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"Your email server port (send)"}</FormDescription>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpPassword"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput placeholder="Password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"Your email server password"}</FormDescription>
|
||||
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpUser"
|
||||
defaultValue=""
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>User Email *</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"exemple@portabase.com"} {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>{"The email server user"}</FormDescription>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
"use client"
|
||||
import {EmailForm} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form";
|
||||
import {Setting} from "@/db/schema/01_setting";
|
||||
import {EmailFormType} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.schema";
|
||||
|
||||
|
||||
export type SettingsEmailSectionProps = {
|
||||
settings: Setting;
|
||||
};
|
||||
|
||||
export const SettingsEmailSection = (props: SettingsEmailSectionProps) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col h-full ">
|
||||
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings as EmailFormType : undefined}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+62
-4
@@ -14,7 +14,7 @@ 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 {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
|
||||
import {
|
||||
EmailFormSchema,
|
||||
@@ -26,17 +26,25 @@ import {
|
||||
} from "@/components/wrappers/dashboard/admin/tabs/admin-email-tab/email-form/email-form.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {Send} from "lucide-react";
|
||||
import {sendEmail} from "@/lib/email/email-helper";
|
||||
import {render} from "@react-email/render";
|
||||
import TestEmailSettings from "@/components/emails/email-settings-test";
|
||||
import {cn} from "@/lib/utils";
|
||||
|
||||
export type EmailFormProps = {
|
||||
defaultValues?: EmailFormType;
|
||||
};
|
||||
|
||||
export const EmailForm = (props: EmailFormProps) => {
|
||||
const router = useRouter();
|
||||
const form = useZodForm({
|
||||
schema: EmailFormSchema,
|
||||
defaultValues: props.defaultValues,
|
||||
});
|
||||
const router = useRouter();
|
||||
const isDirty = form.formState.isDirty;
|
||||
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: EmailFormType) => {
|
||||
@@ -48,16 +56,39 @@ export const EmailForm = (props: EmailFormProps) => {
|
||||
}
|
||||
toast.success(`Success updating email informations`);
|
||||
router.refresh();
|
||||
form.reset(data)
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const mutationSendEmailTest = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!props.defaultValues?.smtpUser || !props.defaultValues?.smtpFrom) {
|
||||
toast.error("SMTP is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
const email = await sendEmail({
|
||||
to: props.defaultValues.smtpUser,
|
||||
|
||||
subject: "Portabase",
|
||||
html: await render(TestEmailSettings(), {}),
|
||||
from: props.defaultValues.smtpFrom,
|
||||
});
|
||||
if (email.response) {
|
||||
toast.success("Test Email Successfully sent !");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4 mt-3"
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
@@ -139,7 +170,34 @@ export const EmailForm = (props: EmailFormProps) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end gap-4">
|
||||
<div className="flex justify-between gap-4 mt-5">
|
||||
{props.defaultValues?.smtpFrom && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<ButtonWithLoading
|
||||
type="button"
|
||||
disabled={isDirty || mutationSendEmailTest.isPending}
|
||||
isPending={mutationSendEmailTest.isPending}
|
||||
onClick={async () => {
|
||||
await mutationSendEmailTest.mutateAsync();
|
||||
}}
|
||||
icon={<Send/>}
|
||||
size="default"
|
||||
>
|
||||
Send email test
|
||||
</ButtonWithLoading>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
{isDirty && (
|
||||
<TooltipContent className={cn(!isDirty && "hidden")}>
|
||||
You must save changes before testing the email settings.
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Button>Save</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import {Home, Settings, Target, Users, Pickaxe, Euro, BanknoteX, Layers, ChartArea, ShieldHalf} from "lucide-react";
|
||||
import {
|
||||
Home,
|
||||
Settings,
|
||||
Users,
|
||||
Layers,
|
||||
ChartArea,
|
||||
ShieldHalf,
|
||||
Building, UserRoundCog, Mail, PackageOpen
|
||||
} from "lucide-react";
|
||||
import {SidebarGroupItem, SidebarMenuCustomBase} from "@/components/wrappers/dashboard/common/sidebar/menu-sidebar";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
|
||||
@@ -9,13 +17,12 @@ export const SidebarMenuCustomMain = () => {
|
||||
|
||||
const BASE_URL = `/dashboard`;
|
||||
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
const { data: organizations } = authClient.useListOrganizations();
|
||||
const {data: activeOrganization} = authClient.useActiveOrganization();
|
||||
const {data: organizations} = authClient.useListOrganizations();
|
||||
const {data: session, isPending, error} = authClient.useSession();
|
||||
const member = authClient.useActiveMember();
|
||||
|
||||
|
||||
|
||||
if (isPending) return null;
|
||||
|
||||
if (error || !session) {
|
||||
@@ -23,13 +30,13 @@ export const SidebarMenuCustomMain = () => {
|
||||
}
|
||||
|
||||
const groupContentApplication: SidebarGroupItem["group_content"] = [
|
||||
{ title: "Dashboard", url: "/home", icon: Home },
|
||||
{title: "Dashboard", url: "/home", icon: Home, type: "item"},
|
||||
];
|
||||
|
||||
const groupContent: SidebarGroupItem["group_content"] = [
|
||||
{ title: "Projects", url: "/projects", icon: Layers, details:true },
|
||||
{ title: "Statistics", url: "/statistics", icon: ChartArea },
|
||||
{ title: "Settings", url: "/settings", icon: Settings, details:true }
|
||||
{title: "Projects", url: "/projects", icon: Layers, details: true, type: "item"},
|
||||
{title: "Statistics", url: "/statistics", icon: ChartArea, type: "item"},
|
||||
{title: "Settings", url: "/settings", icon: Settings, details: true, type: "item"}
|
||||
];
|
||||
|
||||
// if (activeOrganization && (member?.data?.role === "admin" || member?.data?.role === "owner")) {
|
||||
@@ -55,10 +62,37 @@ export const SidebarMenuCustomMain = () => {
|
||||
label: "Administration",
|
||||
type: "list",
|
||||
group_content: [
|
||||
{title: "Agents", url: "/agents", icon: ShieldHalf, details: true},
|
||||
{title: "Administration panel", url: "/admin", icon: Settings, details: true},
|
||||
]
|
||||
})
|
||||
{
|
||||
title: "Agents",
|
||||
url: "/agents",
|
||||
icon: ShieldHalf,
|
||||
details: true,
|
||||
type: "item"
|
||||
},
|
||||
{
|
||||
title: "Access management",
|
||||
url: "/admin",
|
||||
icon: UserRoundCog,
|
||||
details: true,
|
||||
type: "collapse",
|
||||
submenu: [
|
||||
{title: "Users", url: "/admin/users", icon: Users, type: "item"},
|
||||
{title: "Organizations", url: "/admin/organizations", icon: Building, type: "item"},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
url: "/admin/settings",
|
||||
icon: Settings,
|
||||
details: true,
|
||||
type: "collapse",
|
||||
submenu: [
|
||||
{title: "Email", url: "/admin/settings/email", icon: Mail, type: "item"},
|
||||
{title: "Storage", url: "/admin/settings/storage", icon: PackageOpen, type: "item"},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,27 +10,15 @@ import {
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem
|
||||
SidebarMenuSubItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import {
|
||||
ChevronDown,
|
||||
MoreHorizontal
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger
|
||||
} from "@radix-ui/react-collapsible";
|
||||
import { ChevronRight, ChevronDown, MoreHorizontal } from "lucide-react";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export type SidebarItem = {
|
||||
@@ -42,6 +30,7 @@ export type SidebarItem = {
|
||||
dropdown?: SidebarItem[];
|
||||
submenu?: SidebarItem[];
|
||||
details?: boolean;
|
||||
type: "item" | "collapse";
|
||||
};
|
||||
|
||||
export type SidebarGroupItem = {
|
||||
@@ -64,140 +53,134 @@ export const SidebarMenuCustomBase = ({ baseUrl, items }: SidebarMenuCustomBaseP
|
||||
|
||||
const findActiveUrl = () => {
|
||||
const normalizedPathname = normalize(pathname);
|
||||
console.log(normalizedPathname);
|
||||
for (const group of items) {
|
||||
for (const item of group.group_content) {
|
||||
const itemUrl = normalize(item.url);
|
||||
|
||||
const fullUrl = item.not_from_base_url
|
||||
? itemUrl
|
||||
: normalize(`${baseUrl}${itemUrl}`);
|
||||
|
||||
if (
|
||||
normalizedPathname === fullUrl ||
|
||||
(item.details && normalizedPathname.startsWith(`${fullUrl}/`))
|
||||
) {
|
||||
return itemUrl;
|
||||
}
|
||||
|
||||
if (item.submenu) {
|
||||
for (const subItem of item.submenu) {
|
||||
const subItemUrl = normalize(subItem.url);
|
||||
const subFullUrl = normalize(`${baseUrl}${subItemUrl}`);
|
||||
|
||||
if (
|
||||
normalizedPathname === subFullUrl ||
|
||||
(subItem.details && normalizedPathname.startsWith(`${subFullUrl}/`))
|
||||
) {
|
||||
if (normalizedPathname === subFullUrl || (subItem.details && normalizedPathname.startsWith(`${subFullUrl}/`))) {
|
||||
return subItemUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const itemUrl = normalize(item.url);
|
||||
const fullUrl = item.not_from_base_url ? itemUrl : normalize(`${baseUrl}${itemUrl}`);
|
||||
|
||||
if (normalizedPathname === fullUrl || (item.details && normalizedPathname.startsWith(`${fullUrl}/`))) {
|
||||
return itemUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const activeUrl = findActiveUrl();
|
||||
setActiveItem(activeUrl);
|
||||
setActiveItem(findActiveUrl());
|
||||
}, [pathname, baseUrl, items]);
|
||||
|
||||
const isSubActive = (item: SidebarItem) => {
|
||||
if (!item.submenu) return false;
|
||||
return item.submenu.some((sub) => sub.url === activeItem);
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
{items.map((group, index) => (
|
||||
<Collapsible key={index} defaultOpen disabled={group.type === "list"}>
|
||||
<Collapsible key={index} defaultOpen className="group/group-collapsible">
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel asChild>
|
||||
<CollapsibleTrigger>
|
||||
{group.label ?? "Missing label"}
|
||||
<CollapsibleTrigger className="flex w-full items-center text-sm font-medium text-sidebar-foreground/70">
|
||||
{group.label}
|
||||
{group.type === "collapse" && (
|
||||
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||
<ChevronDown className="ml-auto h-4 w-4 transition-transform group-data-[state=open]/group-collapsible:rotate-180" />
|
||||
)}
|
||||
</CollapsibleTrigger>
|
||||
</SidebarGroupLabel>
|
||||
<CollapsibleContent>
|
||||
<SidebarGroupContent>
|
||||
{group.group_content.map((item, index) => (
|
||||
<SidebarMenuItem key={index} className="mb-1">
|
||||
<SidebarMenuButton asChild>
|
||||
<Link
|
||||
href={
|
||||
item.redirect || item.not_from_base_url
|
||||
? item.url
|
||||
: `${baseUrl}${item.url}`
|
||||
}
|
||||
target={item.redirect ? "_blank" : ""}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: "lg",
|
||||
variant: activeItem === item.url ? "secondary" : "ghost"
|
||||
}),
|
||||
"justify-start p-0",
|
||||
"hover:bg-gray-700"
|
||||
)}
|
||||
onClick={() => setActiveItem(item.url)}
|
||||
>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
{group.group_content.map((item, idx) => {
|
||||
if (item.type === "collapse" && item.submenu) {
|
||||
const isChildActive = isSubActive(item);
|
||||
|
||||
{item.submenu && (
|
||||
<SidebarMenuSub>
|
||||
{item.submenu.map((sub, idx) => (
|
||||
<SidebarMenuSubItem key={idx}>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<Link
|
||||
href={`${baseUrl}${sub.url}`}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: "lg",
|
||||
variant: activeItem === sub.url ? "secondary" : "ghost"
|
||||
}),
|
||||
"justify-start p-0",
|
||||
"hover:bg-gray-700"
|
||||
)}
|
||||
onClick={() => setActiveItem(sub.url)}
|
||||
>
|
||||
<sub.icon />
|
||||
{sub.title}
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
)}
|
||||
return (
|
||||
<Collapsible key={idx} asChild defaultOpen={isChildActive} className="group/collapsible">
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton className={cn(buttonVariants({ variant: "ghost", size: "lg" }), "justify-between")}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.submenu.map((sub, subIdx) => (
|
||||
<SidebarMenuSubItem key={subIdx}>
|
||||
<SidebarMenuSubButton asChild isActive={activeItem === sub.url}>
|
||||
<Link
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost", size: "sm" }),
|
||||
"w-full justify-start"
|
||||
)}
|
||||
href={`${baseUrl}${sub.url}`}
|
||||
onClick={() => setActiveItem(sub.url)}
|
||||
>
|
||||
<sub.icon />
|
||||
<span>{sub.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
{item.dropdown && (
|
||||
<SidebarMenuAction>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<MoreHorizontal />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="right" align="start">
|
||||
{item.dropdown.map((dropdown, idx) => (
|
||||
<DropdownMenuItem key={idx} asChild>
|
||||
<Link
|
||||
href={
|
||||
dropdown.redirect || dropdown.not_from_base_url
|
||||
? dropdown.url
|
||||
: `${baseUrl}${dropdown.url}`
|
||||
}
|
||||
className="justify-start p-0"
|
||||
target={dropdown.redirect ? "_blank" : ""}
|
||||
onClick={() => setActiveItem(dropdown.url)}
|
||||
>
|
||||
<span>{dropdown.title}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuAction>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
return (
|
||||
<SidebarMenuItem key={idx}>
|
||||
<SidebarMenuButton asChild isActive={activeItem === item.url} tooltip={item.title}>
|
||||
<Link
|
||||
href={item.redirect || item.not_from_base_url ? item.url : `${baseUrl}${item.url}`}
|
||||
target={item.redirect ? "_blank" : ""}
|
||||
onClick={() => setActiveItem(item.url)}
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "lg" }), "justify-start")}
|
||||
>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
{item.dropdown && (
|
||||
<SidebarMenuAction showOnHover>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<MoreHorizontal />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="right" align="start">
|
||||
{item.dropdown.map((dropdown, dIdx) => (
|
||||
<DropdownMenuItem key={dIdx} asChild>
|
||||
<Link
|
||||
href={
|
||||
dropdown.redirect || dropdown.not_from_base_url
|
||||
? dropdown.url
|
||||
: `${baseUrl}${dropdown.url}`
|
||||
}
|
||||
target={dropdown.redirect ? "_blank" : ""}
|
||||
>
|
||||
<span>{dropdown.title}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuAction>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
</SidebarGroupContent>
|
||||
</CollapsibleContent>
|
||||
</SidebarGroup>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client"
|
||||
import {Filter, Megaphone} from "lucide-react";
|
||||
import {Megaphone} from "lucide-react";
|
||||
|
||||
import {useState} from "react";
|
||||
import {
|
||||
@@ -26,16 +26,18 @@ type AlertPolicyModalProps = {
|
||||
export const AlertPolicyModal = ({database, notificationChannels, organizationId}: AlertPolicyModalProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const notificationsChannelsIds = notificationChannels.map(channel => channel.id);
|
||||
const activePolicies = database.alertPolicies?.filter((policy) => notificationsChannelsIds.some(()=> policy.notificationChannelId));
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" onClick={() => setOpen(true)} className="relative">
|
||||
<Megaphone/>
|
||||
{database.alertPolicies && database.alertPolicies.length > 0 && (
|
||||
{ activePolicies && activePolicies.length > 0 && (
|
||||
<Badge
|
||||
className="absolute -top-1.5 -right-1.5 h-4 w-4 rounded-full p-0 text-[10px] flex items-center justify-center"
|
||||
>
|
||||
{database.alertPolicies.length}
|
||||
{activePolicies.length}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -17,10 +17,16 @@ export type createOrganizationModalProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess?: () => void;
|
||||
redirect?: string;
|
||||
|
||||
};
|
||||
|
||||
export function CreateOrganizationModal({open, onOpenChange, onSuccess}: createOrganizationModalProps) {
|
||||
export function CreateOrganizationModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSuccess,
|
||||
redirect = "/dashboard/home"
|
||||
}: createOrganizationModalProps) {
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
@@ -41,7 +47,8 @@ export function CreateOrganizationModal({open, onOpenChange, onSuccess}: createO
|
||||
await authClient.organization.setActive({organizationSlug: result.data.value.slug});
|
||||
onSuccess?.();
|
||||
toast.success(result.data.actionSuccess?.message || "Organization Created.");
|
||||
router.replace(`/dashboard/home`);
|
||||
form.reset()
|
||||
router.replace(redirect);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
const errorMsg = result?.data?.actionError?.message || result?.data?.actionError?.messageParams?.message || "Failed to create the organization.";
|
||||
|
||||
@@ -116,6 +116,9 @@ export const updateProjectAction = userAction
|
||||
await db.delete(drizzleDb.schemas.retentionPolicy)
|
||||
.where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databasesToRemove)).execute();
|
||||
|
||||
await db.delete(drizzleDb.schemas.alertPolicy)
|
||||
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databasesToRemove)).execute();
|
||||
|
||||
}
|
||||
// const slug = slugify(parsedInput.data.name);
|
||||
|
||||
|
||||
@@ -78,6 +78,16 @@ export async function dispatchNotification(
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (!channel.enabled) {
|
||||
return {
|
||||
success: false,
|
||||
channelId: channelId || "",
|
||||
provider: null,
|
||||
error: "Channel not active¬",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await dispatchViaProvider(
|
||||
channel.provider,
|
||||
channel.config,
|
||||
|
||||
@@ -41,6 +41,10 @@ export const sendEmail = async (data: Payload) => {
|
||||
throw new Error("SMTP system settings not found.");
|
||||
}
|
||||
|
||||
const emailsArray = data.to.split(",")
|
||||
.map(email => email.trim());
|
||||
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
pool: true,
|
||||
host: settings.smtpHost ?? "",
|
||||
@@ -54,6 +58,7 @@ export const sendEmail = async (data: Payload) => {
|
||||
|
||||
return await transporter.sendMail({
|
||||
...data,
|
||||
to: emailsArray,
|
||||
from: settings.smtpFrom ?? undefined,
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user