fix(dashboard): improvements to notifications templates
This commit is contained in:
@@ -21,17 +21,16 @@ export default function Page({
|
|||||||
.withDefault('available')
|
.withDefault('available')
|
||||||
.parseServerSide(searchParams.tab);
|
.parseServerSide(searchParams.tab);
|
||||||
return (
|
return (
|
||||||
<Padding>
|
<Padding className="col gap-8">
|
||||||
<PageTabs className="mb-4">
|
<div className="col gap-4">
|
||||||
<PageTabsLink href="?tab=available" isActive={tab === 'available'}>
|
<h2 className="text-3xl font-semibold">Your integrations</h2>
|
||||||
Available
|
<ActiveIntegrations />
|
||||||
</PageTabsLink>
|
</div>
|
||||||
<PageTabsLink href="?tab=installed" isActive={tab === 'installed'}>
|
|
||||||
Installed
|
<div className="col gap-4">
|
||||||
</PageTabsLink>
|
<h2 className="text-3xl font-semibold">Available integrations</h2>
|
||||||
</PageTabs>
|
<AllIntegrations />
|
||||||
{tab === 'installed' && <ActiveIntegrations />}
|
</div>
|
||||||
{tab === 'available' && <AllIntegrations />}
|
|
||||||
</Padding>
|
</Padding>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type WithLabel = {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
error?: string | undefined;
|
error?: string | undefined;
|
||||||
info?: string;
|
info?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
type InputWithLabelProps = InputProps & Omit<WithLabel, 'children'>;
|
type InputWithLabelProps = InputProps & Omit<WithLabel, 'children'>;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function useColumns() {
|
|||||||
const { title, isReadAt } = row.original;
|
const { title, isReadAt } = row.original;
|
||||||
return (
|
return (
|
||||||
<div className="row gap-2 items-center">
|
<div className="row gap-2 items-center">
|
||||||
{isReadAt === null && <PingBadge>Unread</PingBadge>}
|
{/* {isReadAt === null && <PingBadge>Unread</PingBadge>} */}
|
||||||
<span className="max-w-md truncate font-medium">{title}</span>
|
<span className="max-w-md truncate font-medium">{title}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -46,6 +46,22 @@ export function useColumns() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'integration',
|
||||||
|
header: 'Integration',
|
||||||
|
cell({ row }) {
|
||||||
|
const integration = row.original.integration;
|
||||||
|
return <div>{integration?.name}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'notificationRule',
|
||||||
|
header: 'Rule',
|
||||||
|
cell({ row }) {
|
||||||
|
const rule = row.original.notificationRule;
|
||||||
|
return <div>{rule?.name}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'country',
|
accessorKey: 'country',
|
||||||
header: 'Country',
|
header: 'Country',
|
||||||
@@ -121,16 +137,8 @@ export function useColumns() {
|
|||||||
header: 'Created at',
|
header: 'Created at',
|
||||||
cell({ row }) {
|
cell({ row }) {
|
||||||
const date = row.original.createdAt;
|
const date = row.original.createdAt;
|
||||||
const rule = row.original.integration?.notificationRules[0];
|
|
||||||
return (
|
return (
|
||||||
<div className="col gap-1">
|
|
||||||
<div>{isToday(date) ? formatTime(date) : formatDateTime(date)}</div>
|
<div>{isToday(date) ? formatTime(date) : formatDateTime(date)}</div>
|
||||||
{rule && (
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
Rule: {rule.name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -62,7 +62,12 @@ export function Tooltiper({
|
|||||||
if (disabled) return children;
|
if (disabled) return children;
|
||||||
return (
|
return (
|
||||||
<Tooltip delayDuration={delayDuration}>
|
<Tooltip delayDuration={delayDuration}>
|
||||||
<TooltipTrigger asChild={asChild} className={className} onClick={onClick}>
|
<TooltipTrigger
|
||||||
|
asChild={asChild}
|
||||||
|
className={className}
|
||||||
|
onClick={onClick}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
|
|||||||
@@ -164,7 +164,48 @@ export default function AddNotificationRule({ rule }: Props) {
|
|||||||
|
|
||||||
<WithLabel
|
<WithLabel
|
||||||
label="Template"
|
label="Template"
|
||||||
info="Customize your notification. Exisiting variables: $EVENT_NAME, $RULE_NAME"
|
info={
|
||||||
|
<div className="text-base leading-normal max-w-sm p-2 prose text-left text-white [&_code]:text-white">
|
||||||
|
<p>
|
||||||
|
Customize your notification message. You can grab any property
|
||||||
|
from your event.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>{'{{name}}'}</code> - The name of the event
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>{'{{rule_name}}'}</code> - The name of the rule
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>{'{{properties.your.property}}'}</code> - Get the value
|
||||||
|
of a custom property
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div className="flex gap-x-2 flex-wrap">
|
||||||
|
And many more...
|
||||||
|
<code>profileId</code>
|
||||||
|
<code>createdAt</code>
|
||||||
|
<code>country</code>
|
||||||
|
<code>city</code>
|
||||||
|
<code>os</code>
|
||||||
|
<code>osVersion</code>
|
||||||
|
<code>browser</code>
|
||||||
|
<code>browserVersion</code>
|
||||||
|
<code>device</code>
|
||||||
|
<code>brand</code>
|
||||||
|
<code>model</code>
|
||||||
|
<code>path</code>
|
||||||
|
<code>origin</code>
|
||||||
|
<code>referrer</code>
|
||||||
|
<code>referrerName</code>
|
||||||
|
<code>referrerType</code>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Textarea
|
<Textarea
|
||||||
{...form.register('template')}
|
{...form.register('template')}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "notifications" ADD COLUMN "notificationRuleId" UUID;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_notificationRuleId_fkey" FOREIGN KEY ("notificationRuleId") REFERENCES "notification_rules"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -392,6 +392,7 @@ model NotificationRule {
|
|||||||
template String?
|
template String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
notifications Notification[]
|
||||||
|
|
||||||
@@map("notification_rules")
|
@@map("notification_rules")
|
||||||
}
|
}
|
||||||
@@ -409,6 +410,8 @@ model Notification {
|
|||||||
sendToEmail Boolean @default(false)
|
sendToEmail Boolean @default(false)
|
||||||
integration Integration? @relation(fields: [integrationId], references: [id])
|
integration Integration? @relation(fields: [integrationId], references: [id])
|
||||||
integrationId String? @db.Uuid
|
integrationId String? @db.Uuid
|
||||||
|
notificationRuleId String? @db.Uuid
|
||||||
|
notificationRule NotificationRule? @relation(fields: [notificationRuleId], references: [id])
|
||||||
/// [IPrismaNotificationPayload]
|
/// [IPrismaNotificationPayload]
|
||||||
payload Json?
|
payload Json?
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ import { getProjectByIdCached } from './project.service';
|
|||||||
|
|
||||||
type ICreateNotification = Pick<
|
type ICreateNotification = Pick<
|
||||||
Notification,
|
Notification,
|
||||||
'projectId' | 'title' | 'message' | 'integrationId' | 'payload'
|
| 'projectId'
|
||||||
|
| 'title'
|
||||||
|
| 'message'
|
||||||
|
| 'integrationId'
|
||||||
|
| 'payload'
|
||||||
|
| 'notificationRuleId'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type INotificationPayload =
|
export type INotificationPayload =
|
||||||
@@ -118,6 +123,7 @@ export async function createNotification(notification: ICreateNotification) {
|
|||||||
projectId: notification.projectId,
|
projectId: notification.projectId,
|
||||||
payload: notification.payload || Prisma.DbNull,
|
payload: notification.payload || Prisma.DbNull,
|
||||||
...getIntegration(notification.integrationId),
|
...getIntegration(notification.integrationId),
|
||||||
|
notificationRuleId: notification.notificationRuleId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,9 +208,23 @@ function notificationTemplateEvent({
|
|||||||
rule: INotificationRuleCached;
|
rule: INotificationRuleCached;
|
||||||
}) {
|
}) {
|
||||||
if (!rule.template) return `You received a new "${payload.name}" event`;
|
if (!rule.template) return `You received a new "${payload.name}" event`;
|
||||||
return rule.template
|
let template = rule.template
|
||||||
.replaceAll('$EVENT_NAME', payload.name)
|
.replaceAll('$EVENT_NAME', payload.name)
|
||||||
.replaceAll('$RULE_NAME', rule.name);
|
.replaceAll('$RULE_NAME', rule.name)
|
||||||
|
.replaceAll('{{rule_name}}', rule.name);
|
||||||
|
|
||||||
|
// Replace all {{xxx}} placeholders with their values
|
||||||
|
const placeholderMatches = template.match(/{{[^}]+}}/g) || [];
|
||||||
|
for (const match of placeholderMatches) {
|
||||||
|
const path = match.slice(2, -2); // Remove {{ and }}
|
||||||
|
const value = pathOr('', path.split('.'), payload);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
template = template.replaceAll(match, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
function notificationTemplateFunnel({
|
function notificationTemplateFunnel({
|
||||||
@@ -253,6 +273,7 @@ export async function checkNotificationRulesForEvent(
|
|||||||
createNotification({
|
createNotification({
|
||||||
...notification,
|
...notification,
|
||||||
integrationId: integration.id,
|
integrationId: integration.id,
|
||||||
|
notificationRuleId: rule.id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -261,6 +282,7 @@ export async function checkNotificationRulesForEvent(
|
|||||||
createNotification({
|
createNotification({
|
||||||
...notification,
|
...notification,
|
||||||
integrationId: APP_NOTIFICATION_INTEGRATION_ID,
|
integrationId: APP_NOTIFICATION_INTEGRATION_ID,
|
||||||
|
notificationRuleId: rule.id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -270,6 +292,7 @@ export async function checkNotificationRulesForEvent(
|
|||||||
createNotification({
|
createNotification({
|
||||||
...notification,
|
...notification,
|
||||||
integrationId: EMAIL_NOTIFICATION_INTEGRATION_ID,
|
integrationId: EMAIL_NOTIFICATION_INTEGRATION_ID,
|
||||||
|
notificationRuleId: rule.id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -325,13 +348,18 @@ export async function checkNotificationRulesForSessionEnd(
|
|||||||
// Generate notification promises
|
// Generate notification promises
|
||||||
return [
|
return [
|
||||||
...rule.integrations.map((integration) =>
|
...rule.integrations.map((integration) =>
|
||||||
createNotification({ ...notification, integrationId: integration.id }),
|
createNotification({
|
||||||
|
...notification,
|
||||||
|
integrationId: integration.id,
|
||||||
|
notificationRuleId: rule.id,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
...(rule.sendToApp
|
...(rule.sendToApp
|
||||||
? [
|
? [
|
||||||
createNotification({
|
createNotification({
|
||||||
...notification,
|
...notification,
|
||||||
integrationId: APP_NOTIFICATION_INTEGRATION_ID,
|
integrationId: APP_NOTIFICATION_INTEGRATION_ID,
|
||||||
|
notificationRuleId: rule.id,
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
@@ -340,6 +368,7 @@ export async function checkNotificationRulesForSessionEnd(
|
|||||||
createNotification({
|
createNotification({
|
||||||
...notification,
|
...notification,
|
||||||
integrationId: EMAIL_NOTIFICATION_INTEGRATION_ID,
|
integrationId: EMAIL_NOTIFICATION_INTEGRATION_ID,
|
||||||
|
notificationRuleId: rule.id,
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|||||||
@@ -21,19 +21,19 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
return db.notification.findMany({
|
return db.notification.findMany({
|
||||||
where: {
|
where: {
|
||||||
projectId: input.projectId,
|
projectId: input.projectId,
|
||||||
sendToApp: true,
|
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
integration: {
|
integration: {
|
||||||
include: {
|
|
||||||
notificationRules: {
|
|
||||||
select: {
|
select: {
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
notificationRule: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user