feat: add channel feed notification

This commit is contained in:
moonrailgun 2024-07-13 15:49:34 +08:00
parent 2f6e92d166
commit 67bfda30bc
8 changed files with 184 additions and 23 deletions

View File

@ -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>

View File

@ -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();

View File

@ -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();

View File

@ -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;

View File

@ -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])
} }

View File

@ -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(),
})) }))

View File

@ -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(),
})) }))

View File

@ -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(