mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Ready for release 1.1.4
This commit is contained in:
+6
-1
@@ -5,6 +5,7 @@ import {NotifierCard} from "@/components/wrappers/dashboard/common/notifier/noti
|
||||
import {useState} from "react";
|
||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal";
|
||||
|
||||
type NotificationChannelsSectionProps = {
|
||||
notificationChannels: NotificationChannelWith[]
|
||||
@@ -22,8 +23,10 @@ export const NotificationChannelsSection = ({
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<NotifierAddEditModal open={isAddModalOpen} onOpenChangeAction={setIsAddModalOpen} adminView={false} trigger={false}/>
|
||||
{hasNotifiers ? (
|
||||
<div className="h-full">
|
||||
|
||||
<CardsWithPagination
|
||||
data={notificationChannels}
|
||||
cardItem={NotifierCard}
|
||||
@@ -36,7 +39,9 @@ export const NotificationChannelsSection = ({
|
||||
) : (
|
||||
<EmptyStatePlaceholder
|
||||
text="No notification channels configured yet"
|
||||
onClick={() => setIsAddModalOpen(true)}
|
||||
onClick={() => {
|
||||
setIsAddModalOpen(true)
|
||||
}}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -54,16 +54,21 @@ export function notificationLogsColumns(): ColumnDef<NotificationLogWithRelation
|
||||
},
|
||||
{
|
||||
accessorKey: "policy",
|
||||
header: "Event Kinds",
|
||||
cell: ({row}) => {
|
||||
const eventKinds = row.original.policy?.eventKinds ?? [];
|
||||
header: "Event Kind",
|
||||
cell: ({ row }) => {
|
||||
const eventKind = row.original.policy?.event;
|
||||
|
||||
if (!eventKind) {
|
||||
return (
|
||||
<div className="text-muted-foreground italic">
|
||||
No event
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{eventKinds.map((eventKind, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<Badge>{eventKind}</Badge>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge>{eventKind}</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,6 +26,11 @@ import {
|
||||
} from "@/components/wrappers/dashboard/admin/settings/email/email-form/email-form.action";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {sendEmail} from "@/lib/email/email-helper";
|
||||
import {render} from "@react-email/components";
|
||||
import EmailSettingsTest from "@/components/emails/email-settings-test";
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {Send} from "lucide-react";
|
||||
|
||||
export type EmailFormProps = {
|
||||
defaultValues?: EmailFormType;
|
||||
@@ -36,6 +41,8 @@ export const EmailForm = (props: EmailFormProps) => {
|
||||
schema: EmailFormSchema,
|
||||
defaultValues: props.defaultValues,
|
||||
});
|
||||
const isDirty = form.formState.isDirty;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
@@ -47,10 +54,45 @@ export const EmailForm = (props: EmailFormProps) => {
|
||||
return;
|
||||
}
|
||||
toast.success(`Success updating email informations`);
|
||||
form.reset(data);
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
const mutationSendEmailTest = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!props.defaultValues?.smtpUser || !props.defaultValues?.smtpFrom) {
|
||||
toast.error("SMTP is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const email = await sendEmail({
|
||||
to: props.defaultValues.smtpUser,
|
||||
subject: "Portabase",
|
||||
html: await render(EmailSettingsTest(), {}),
|
||||
from: props.defaultValues.smtpFrom,
|
||||
});
|
||||
|
||||
if (email?.response) {
|
||||
toast.success("Test Email successfully sent!");
|
||||
}
|
||||
|
||||
return email;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(`Email sending failed: ${error.message}`);
|
||||
} else {
|
||||
toast.error("Unknown error while sending test email");
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Card>
|
||||
@@ -139,8 +181,56 @@ export const EmailForm = (props: EmailFormProps) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button>Save</Button>
|
||||
<div className="flex justify-between gap-4">
|
||||
|
||||
{/*{props.defaultValues?.smtpFrom && (*/}
|
||||
{/* <ButtonWithLoading*/}
|
||||
{/* type="button"*/}
|
||||
{/* isPending={mutationSendEmailTest.isPending}*/}
|
||||
{/* onClick={async () => {*/}
|
||||
{/* await mutationSendEmailTest.mutateAsync();*/}
|
||||
{/* }}*/}
|
||||
{/* icon={<Send/>}*/}
|
||||
{/* size="default"*/}
|
||||
{/* className="bg-green-600 hover:bg-green-700 text-white font-medium shadow-sm transition-all"*/}
|
||||
|
||||
{/* >Send email test</ButtonWithLoading>*/}
|
||||
{/*)}*/}
|
||||
|
||||
|
||||
|
||||
{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"
|
||||
className="bg-green-600 hover:bg-green-700 text-white font-medium shadow-sm transition-all"
|
||||
|
||||
>
|
||||
Send email test
|
||||
</ButtonWithLoading>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
{isDirty && (
|
||||
<TooltipContent>
|
||||
You must save changes before testing the email settings.
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
>Save</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</CardContent>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {NotifierForm} from "@/components/wrappers/dashboard/common/notifier/noti
|
||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||
import {useIsMobile} from "@/hooks/use-mobile";
|
||||
import {useState} from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||
import {
|
||||
NotifierChannelOrganisationForm
|
||||
@@ -23,6 +23,7 @@ type OrganizationNotifierAddModalProps = {
|
||||
onOpenChangeAction?: (open: boolean) => void;
|
||||
adminView?: boolean;
|
||||
organizations?: OrganizationWithMembers[]
|
||||
trigger?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,33 +33,41 @@ export const NotifierAddEditModal = ({
|
||||
open = false,
|
||||
onOpenChangeAction,
|
||||
adminView,
|
||||
organizations
|
||||
organizations ,
|
||||
trigger= true
|
||||
}: OrganizationNotifierAddModalProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const [openInternal, setOpen] = useState(open);
|
||||
|
||||
const isCreate = !Boolean(notificationChannel);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(open);
|
||||
},[open])
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={openInternal} onOpenChange={(state) => {
|
||||
onOpenChangeAction?.(state);
|
||||
setOpen(state);
|
||||
}}>
|
||||
<DialogTrigger asChild>
|
||||
{isCreate ?
|
||||
<Button>
|
||||
<Plus/>{!isMobile && `Add notification channel`}
|
||||
</Button>
|
||||
:
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<Pencil className="h-4 w-4"/>
|
||||
</Button>
|
||||
}
|
||||
</DialogTrigger>
|
||||
{trigger && (
|
||||
<DialogTrigger asChild>
|
||||
{isCreate ?
|
||||
<Button>
|
||||
<Plus/>{!isMobile && `Add notification channel`}
|
||||
</Button>
|
||||
:
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
>
|
||||
<Pencil className="h-4 w-4"/>
|
||||
</Button>
|
||||
}
|
||||
</DialogTrigger>
|
||||
)}
|
||||
|
||||
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle> {isCreate ? "Add" : "Edit"} Notification Channel</DialogTitle>
|
||||
|
||||
@@ -35,8 +35,10 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
const router = useRouter()
|
||||
const organizationNotificationChannels = notificationChannels.map(channel => channel.id) ?? [];
|
||||
|
||||
|
||||
|
||||
const formattedAlertPoliciesList = (alertPolicies: AlertPolicy[]) => {
|
||||
return alertPolicies.map((alertPolicy) => ({
|
||||
return alertPolicies.filter((alertPolicy)=> organizationNotificationChannels.includes(alertPolicy.notificationChannelId)).map((alertPolicy) => ({
|
||||
notificationChannelId: alertPolicy.notificationChannelId,
|
||||
eventKinds: alertPolicy.eventKinds,
|
||||
enabled: alertPolicy.enabled
|
||||
@@ -273,11 +275,6 @@ export const AlertPolicyForm = ({database, notificationChannels, organizationId,
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div tabIndex={0} className="inline-flex rounded-md">
|
||||
{/*<Switch*/}
|
||||
{/* checked={true}*/}
|
||||
{/* onCheckedChange={async () => {*/}
|
||||
{/* }}*/}
|
||||
{/*/>*/}
|
||||
|
||||
|
||||
<FormField
|
||||
|
||||
@@ -26,14 +26,22 @@ 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));
|
||||
|
||||
|
||||
const notificationsChannelsFiltered = notificationChannels
|
||||
.filter((channel) => channel.enabled)
|
||||
|
||||
const notificationsChannelsIds = notificationsChannelsFiltered
|
||||
.map(channel => channel.id);
|
||||
|
||||
const activePolicies = database.alertPolicies?.filter((policy) => notificationsChannelsIds.includes(policy.notificationChannelId));
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" onClick={() => setOpen(true)} className="relative">
|
||||
<Megaphone/>
|
||||
{ activePolicies && activePolicies.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"
|
||||
>
|
||||
@@ -52,7 +60,7 @@ export const AlertPolicyModal = ({database, notificationChannels, organizationId
|
||||
<Separator className="mt-3 mb-3"/>
|
||||
<AlertPolicyForm
|
||||
organizationId={organizationId}
|
||||
notificationChannels={notificationChannels}
|
||||
notificationChannels={notificationsChannelsFiltered}
|
||||
database={database}
|
||||
onSuccess={() => setOpen(false)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user