feat: add channel feed notification
This commit is contained in:
parent
2f6e92d166
commit
67bfda30bc
@ -16,9 +16,19 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '../ui/select';
|
||||||
|
import { NotificationPicker } from '../notification/NotificationPicker';
|
||||||
|
|
||||||
const addFormSchema = z.object({
|
const addFormSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
notificationIds: z.array(z.string()).default([]),
|
||||||
|
notifyFrequency: z.enum(['event', 'day', 'week', 'month']),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FeedChannelEditFormValues = z.infer<typeof addFormSchema>;
|
export type FeedChannelEditFormValues = z.infer<typeof addFormSchema>;
|
||||||
@ -35,6 +45,8 @@ export const FeedChannelEditForm: React.FC<FeedChannelEditFormProps> =
|
|||||||
resolver: zodResolver(addFormSchema),
|
resolver: zodResolver(addFormSchema),
|
||||||
defaultValues: props.defaultValues ?? {
|
defaultValues: props.defaultValues ?? {
|
||||||
name: 'New Channel',
|
name: 'New Channel',
|
||||||
|
notificationIds: [],
|
||||||
|
notifyFrequency: 'day',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -66,6 +78,61 @@ export const FeedChannelEditForm: React.FC<FeedChannelEditFormProps> =
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="notificationIds"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Notification')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<NotificationPicker
|
||||||
|
className="w-full"
|
||||||
|
{...field}
|
||||||
|
allowClear={true}
|
||||||
|
mode="multiple"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t('Select Notification for send')}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="notifyFrequency"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Notification Frequency')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="event">
|
||||||
|
{t('Every Event')}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="day">{t('Every Day')}</SelectItem>
|
||||||
|
<SelectItem value="week">
|
||||||
|
{t('Every Week')}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="month">
|
||||||
|
{t('Every Month')}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
|
@ -7,7 +7,7 @@ import { PlusOutlined } from '@ant-design/icons';
|
|||||||
import { useTranslation } from '@i18next-toolkit/react';
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
|
|
||||||
interface NotificationPickerProps extends SelectProps<string> {}
|
interface NotificationPickerProps extends SelectProps<string[]> {}
|
||||||
export const NotificationPicker: React.FC<NotificationPickerProps> = React.memo(
|
export const NotificationPicker: React.FC<NotificationPickerProps> = React.memo(
|
||||||
(props) => {
|
(props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -26,8 +26,8 @@ function PageComponent() {
|
|||||||
|
|
||||||
const onSubmit = useEvent(async (values: FeedChannelEditFormValues) => {
|
const onSubmit = useEvent(async (values: FeedChannelEditFormValues) => {
|
||||||
const res = await createMutation.mutateAsync({
|
const res = await createMutation.mutateAsync({
|
||||||
|
...values,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
name: values.name,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
utils.feed.channels.refetch();
|
utils.feed.channels.refetch();
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "FeedChannel" ADD COLUMN "notifyFrequency" TEXT NOT NULL DEFAULT 'day';
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_FeedChannelToNotification" (
|
||||||
|
"A" VARCHAR(30) NOT NULL,
|
||||||
|
"B" VARCHAR(30) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "_FeedChannelToNotification_AB_unique" ON "_FeedChannelToNotification"("A", "B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_FeedChannelToNotification_B_index" ON "_FeedChannelToNotification"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_FeedChannelToNotification" ADD CONSTRAINT "_FeedChannelToNotification_A_fkey" FOREIGN KEY ("A") REFERENCES "FeedChannel"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_FeedChannelToNotification" ADD CONSTRAINT "_FeedChannelToNotification_B_fkey" FOREIGN KEY ("B") REFERENCES "Notification"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -280,7 +280,8 @@ model Notification {
|
|||||||
|
|
||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
|
|
||||||
monitors Monitor[]
|
monitors Monitor[]
|
||||||
|
feedChannels FeedChannel[]
|
||||||
|
|
||||||
@@index([workspaceId])
|
@@index([workspaceId])
|
||||||
}
|
}
|
||||||
@ -446,14 +447,16 @@ model SurveyResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model FeedChannel {
|
model FeedChannel {
|
||||||
id String @id @default(cuid()) @db.VarChar(30)
|
id String @id @default(cuid()) @db.VarChar(30)
|
||||||
workspaceId String @db.VarChar(30)
|
workspaceId String @db.VarChar(30)
|
||||||
name String
|
name String
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
notifyFrequency String @default("day")
|
||||||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||||||
|
|
||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||||
events FeedEvent[]
|
events FeedEvent[]
|
||||||
|
notifications Notification[]
|
||||||
|
|
||||||
@@index([workspaceId])
|
@@index([workspaceId])
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import * as z from "zod"
|
import * as z from "zod"
|
||||||
import * as imports from "./schemas"
|
import * as imports from "./schemas"
|
||||||
import { CompleteWorkspace, RelatedWorkspaceModelSchema, CompleteFeedEvent, RelatedFeedEventModelSchema } from "./index"
|
import { CompleteWorkspace, RelatedWorkspaceModelSchema, CompleteFeedEvent, RelatedFeedEventModelSchema, CompleteNotification, RelatedNotificationModelSchema } from "./index"
|
||||||
|
|
||||||
export const FeedChannelModelSchema = z.object({
|
export const FeedChannelModelSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
workspaceId: z.string(),
|
workspaceId: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
notifyFrequency: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
})
|
})
|
||||||
@ -13,6 +14,7 @@ export const FeedChannelModelSchema = z.object({
|
|||||||
export interface CompleteFeedChannel extends z.infer<typeof FeedChannelModelSchema> {
|
export interface CompleteFeedChannel extends z.infer<typeof FeedChannelModelSchema> {
|
||||||
workspace: CompleteWorkspace
|
workspace: CompleteWorkspace
|
||||||
events: CompleteFeedEvent[]
|
events: CompleteFeedEvent[]
|
||||||
|
notifications: CompleteNotification[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,4 +25,5 @@ export interface CompleteFeedChannel extends z.infer<typeof FeedChannelModelSche
|
|||||||
export const RelatedFeedChannelModelSchema: z.ZodSchema<CompleteFeedChannel> = z.lazy(() => FeedChannelModelSchema.extend({
|
export const RelatedFeedChannelModelSchema: z.ZodSchema<CompleteFeedChannel> = z.lazy(() => FeedChannelModelSchema.extend({
|
||||||
workspace: RelatedWorkspaceModelSchema,
|
workspace: RelatedWorkspaceModelSchema,
|
||||||
events: RelatedFeedEventModelSchema.array(),
|
events: RelatedFeedEventModelSchema.array(),
|
||||||
|
notifications: RelatedNotificationModelSchema.array(),
|
||||||
}))
|
}))
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as z from "zod"
|
import * as z from "zod"
|
||||||
import * as imports from "./schemas"
|
import * as imports from "./schemas"
|
||||||
import { CompleteWorkspace, RelatedWorkspaceModelSchema, CompleteMonitor, RelatedMonitorModelSchema } from "./index"
|
import { CompleteWorkspace, RelatedWorkspaceModelSchema, CompleteMonitor, RelatedMonitorModelSchema, CompleteFeedChannel, RelatedFeedChannelModelSchema } from "./index"
|
||||||
|
|
||||||
// Helper schema for JSON fields
|
// Helper schema for JSON fields
|
||||||
type Literal = boolean | number | string
|
type Literal = boolean | number | string
|
||||||
@ -23,6 +23,7 @@ export const NotificationModelSchema = z.object({
|
|||||||
export interface CompleteNotification extends z.infer<typeof NotificationModelSchema> {
|
export interface CompleteNotification extends z.infer<typeof NotificationModelSchema> {
|
||||||
workspace: CompleteWorkspace
|
workspace: CompleteWorkspace
|
||||||
monitors: CompleteMonitor[]
|
monitors: CompleteMonitor[]
|
||||||
|
feedChannels: CompleteFeedChannel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,4 +34,5 @@ export interface CompleteNotification extends z.infer<typeof NotificationModelSc
|
|||||||
export const RelatedNotificationModelSchema: z.ZodSchema<CompleteNotification> = z.lazy(() => NotificationModelSchema.extend({
|
export const RelatedNotificationModelSchema: z.ZodSchema<CompleteNotification> = z.lazy(() => NotificationModelSchema.extend({
|
||||||
workspace: RelatedWorkspaceModelSchema,
|
workspace: RelatedWorkspaceModelSchema,
|
||||||
monitors: RelatedMonitorModelSchema.array(),
|
monitors: RelatedMonitorModelSchema.array(),
|
||||||
|
feedChannels: RelatedFeedChannelModelSchema.array(),
|
||||||
}))
|
}))
|
||||||
|
@ -67,18 +67,39 @@ export const feedRouter = router({
|
|||||||
channelId: z.string(),
|
channelId: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.output(FeedChannelModelSchema.nullable())
|
.output(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
notificationIds: z.array(z.string()),
|
||||||
|
})
|
||||||
|
.merge(FeedChannelModelSchema)
|
||||||
|
.nullable()
|
||||||
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { channelId, workspaceId } = input;
|
const { channelId, workspaceId } = input;
|
||||||
|
|
||||||
const channel = prisma.feedChannel.findFirst({
|
const channel = await prisma.feedChannel.findFirst({
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
id: channelId,
|
id: channelId,
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
notifications: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return channel;
|
if (!channel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...channel,
|
||||||
|
notificationIds: channel?.notifications.map((n) => n.id),
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
updateChannelInfo: workspaceProcedure
|
updateChannelInfo: workspaceProcedure
|
||||||
.meta(
|
.meta(
|
||||||
@ -91,28 +112,48 @@ export const feedRouter = router({
|
|||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
channelId: z.string(),
|
channelId: z.string(),
|
||||||
|
notificationIds: z.array(z.string()).default([]),
|
||||||
})
|
})
|
||||||
.merge(
|
.merge(
|
||||||
FeedChannelModelSchema.pick({
|
FeedChannelModelSchema.pick({
|
||||||
name: true,
|
name: true,
|
||||||
|
notifyFrequency: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.output(FeedChannelModelSchema.nullable())
|
.output(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
notificationIds: z.array(z.string()),
|
||||||
|
})
|
||||||
|
.merge(FeedChannelModelSchema)
|
||||||
|
.nullable()
|
||||||
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { channelId, workspaceId, name } = input;
|
const { channelId, workspaceId, name, notifyFrequency, notificationIds } =
|
||||||
|
input;
|
||||||
|
|
||||||
const channel = prisma.feedChannel.update({
|
const channel = await prisma.feedChannel.update({
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
id: channelId,
|
id: channelId,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
|
notifyFrequency,
|
||||||
|
notifications: {
|
||||||
|
set: notificationIds.map((id) => ({
|
||||||
|
id,
|
||||||
|
})),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return channel;
|
if (!channel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...channel, notificationIds };
|
||||||
}),
|
}),
|
||||||
fetchEventsByCursor: workspaceProcedure
|
fetchEventsByCursor: workspaceProcedure
|
||||||
.meta(
|
.meta(
|
||||||
@ -161,20 +202,45 @@ export const feedRouter = router({
|
|||||||
.input(
|
.input(
|
||||||
FeedChannelModelSchema.pick({
|
FeedChannelModelSchema.pick({
|
||||||
name: true,
|
name: true,
|
||||||
})
|
notifyFrequency: true,
|
||||||
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
notificationIds: z.array(z.string()).default([]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
notificationIds: z.array(z.string()),
|
||||||
|
})
|
||||||
|
.merge(FeedChannelModelSchema)
|
||||||
)
|
)
|
||||||
.output(FeedChannelModelSchema)
|
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { name, workspaceId } = input;
|
const { name, workspaceId, notifyFrequency, notificationIds } = input;
|
||||||
|
|
||||||
const channel = await prisma.feedChannel.create({
|
const channel = await prisma.feedChannel.create({
|
||||||
data: {
|
data: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
name,
|
name,
|
||||||
|
notifyFrequency,
|
||||||
|
notifications: {
|
||||||
|
connect: notificationIds.map((id) => ({ id })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
notifications: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return channel;
|
return {
|
||||||
|
...channel,
|
||||||
|
notificationIds: channel?.notifications.map((n) => n.id),
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
deleteChannel: workspaceOwnerProcedure
|
deleteChannel: workspaceOwnerProcedure
|
||||||
.meta(
|
.meta(
|
||||||
|
Loading…
Reference in New Issue
Block a user