From 67bfda30bc95b5b8d11ff3994c6d097106e2c248 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sat, 13 Jul 2024 15:49:34 +0800 Subject: [PATCH] feat: add channel feed notification --- .../components/feed/FeedChannelEditForm.tsx | 67 ++++++++++++++ .../notification/NotificationPicker.tsx | 2 +- src/client/routes/feed/add.tsx | 2 +- .../migration.sql | 20 +++++ src/server/prisma/schema.prisma | 19 ++-- src/server/prisma/zod/feedchannel.ts | 5 +- src/server/prisma/zod/notification.ts | 4 +- src/server/trpc/routers/feed/index.ts | 88 ++++++++++++++++--- 8 files changed, 184 insertions(+), 23 deletions(-) create mode 100644 src/server/prisma/migrations/20240713071153_add_feed_channel_notification/migration.sql diff --git a/src/client/components/feed/FeedChannelEditForm.tsx b/src/client/components/feed/FeedChannelEditForm.tsx index f7cd4c0..7ce6060 100644 --- a/src/client/components/feed/FeedChannelEditForm.tsx +++ b/src/client/components/feed/FeedChannelEditForm.tsx @@ -16,9 +16,19 @@ import { Input } from '@/components/ui/input'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import React from 'react'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '../ui/select'; +import { NotificationPicker } from '../notification/NotificationPicker'; const addFormSchema = z.object({ name: z.string(), + notificationIds: z.array(z.string()).default([]), + notifyFrequency: z.enum(['event', 'day', 'week', 'month']), }); export type FeedChannelEditFormValues = z.infer; @@ -35,6 +45,8 @@ export const FeedChannelEditForm: React.FC = resolver: zodResolver(addFormSchema), defaultValues: props.defaultValues ?? { name: 'New Channel', + notificationIds: [], + notifyFrequency: 'day', }, }); @@ -66,6 +78,61 @@ export const FeedChannelEditForm: React.FC = )} /> + + ( + + {t('Notification')} + + + + + {t('Select Notification for send')} + + + + )} + /> + + ( + + {t('Notification Frequency')} + + + + + + )} + /> diff --git a/src/client/components/notification/NotificationPicker.tsx b/src/client/components/notification/NotificationPicker.tsx index c4e0c51..be1a93d 100644 --- a/src/client/components/notification/NotificationPicker.tsx +++ b/src/client/components/notification/NotificationPicker.tsx @@ -7,7 +7,7 @@ import { PlusOutlined } from '@ant-design/icons'; import { useTranslation } from '@i18next-toolkit/react'; import { useNavigate } from '@tanstack/react-router'; -interface NotificationPickerProps extends SelectProps {} +interface NotificationPickerProps extends SelectProps {} export const NotificationPicker: React.FC = React.memo( (props) => { const { t } = useTranslation(); diff --git a/src/client/routes/feed/add.tsx b/src/client/routes/feed/add.tsx index 678b073..de9e036 100644 --- a/src/client/routes/feed/add.tsx +++ b/src/client/routes/feed/add.tsx @@ -26,8 +26,8 @@ function PageComponent() { const onSubmit = useEvent(async (values: FeedChannelEditFormValues) => { const res = await createMutation.mutateAsync({ + ...values, workspaceId, - name: values.name, }); utils.feed.channels.refetch(); diff --git a/src/server/prisma/migrations/20240713071153_add_feed_channel_notification/migration.sql b/src/server/prisma/migrations/20240713071153_add_feed_channel_notification/migration.sql new file mode 100644 index 0000000..a7cd73c --- /dev/null +++ b/src/server/prisma/migrations/20240713071153_add_feed_channel_notification/migration.sql @@ -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; diff --git a/src/server/prisma/schema.prisma b/src/server/prisma/schema.prisma index 26b99cf..a6c25c8 100644 --- a/src/server/prisma/schema.prisma +++ b/src/server/prisma/schema.prisma @@ -280,7 +280,8 @@ model Notification { workspace Workspace @relation(fields: [workspaceId], references: [id], onUpdate: Cascade, onDelete: Cascade) - monitors Monitor[] + monitors Monitor[] + feedChannels FeedChannel[] @@index([workspaceId]) } @@ -446,14 +447,16 @@ model SurveyResult { } model FeedChannel { - id String @id @default(cuid()) @db.VarChar(30) - workspaceId String @db.VarChar(30) - name String - createdAt DateTime @default(now()) @db.Timestamptz(6) - updatedAt DateTime @updatedAt @db.Timestamptz(6) + id String @id @default(cuid()) @db.VarChar(30) + workspaceId String @db.VarChar(30) + name String + notifyFrequency String @default("day") + createdAt DateTime @default(now()) @db.Timestamptz(6) + updatedAt DateTime @updatedAt @db.Timestamptz(6) - workspace Workspace @relation(fields: [workspaceId], references: [id], onUpdate: Cascade, onDelete: Cascade) - events FeedEvent[] + workspace Workspace @relation(fields: [workspaceId], references: [id], onUpdate: Cascade, onDelete: Cascade) + events FeedEvent[] + notifications Notification[] @@index([workspaceId]) } diff --git a/src/server/prisma/zod/feedchannel.ts b/src/server/prisma/zod/feedchannel.ts index bfdbf94..e4eb07d 100644 --- a/src/server/prisma/zod/feedchannel.ts +++ b/src/server/prisma/zod/feedchannel.ts @@ -1,11 +1,12 @@ import * as z from "zod" 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({ id: z.string(), workspaceId: z.string(), name: z.string(), + notifyFrequency: z.string(), createdAt: z.date(), updatedAt: z.date(), }) @@ -13,6 +14,7 @@ export const FeedChannelModelSchema = z.object({ export interface CompleteFeedChannel extends z.infer { workspace: CompleteWorkspace events: CompleteFeedEvent[] + notifications: CompleteNotification[] } /** @@ -23,4 +25,5 @@ export interface CompleteFeedChannel extends z.infer = z.lazy(() => FeedChannelModelSchema.extend({ workspace: RelatedWorkspaceModelSchema, events: RelatedFeedEventModelSchema.array(), + notifications: RelatedNotificationModelSchema.array(), })) diff --git a/src/server/prisma/zod/notification.ts b/src/server/prisma/zod/notification.ts index 88de6e4..61643c8 100644 --- a/src/server/prisma/zod/notification.ts +++ b/src/server/prisma/zod/notification.ts @@ -1,6 +1,6 @@ import * as z from "zod" 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 type Literal = boolean | number | string @@ -23,6 +23,7 @@ export const NotificationModelSchema = z.object({ export interface CompleteNotification extends z.infer { workspace: CompleteWorkspace monitors: CompleteMonitor[] + feedChannels: CompleteFeedChannel[] } /** @@ -33,4 +34,5 @@ export interface CompleteNotification extends z.infer = z.lazy(() => NotificationModelSchema.extend({ workspace: RelatedWorkspaceModelSchema, monitors: RelatedMonitorModelSchema.array(), + feedChannels: RelatedFeedChannelModelSchema.array(), })) diff --git a/src/server/trpc/routers/feed/index.ts b/src/server/trpc/routers/feed/index.ts index b5894ef..240a138 100644 --- a/src/server/trpc/routers/feed/index.ts +++ b/src/server/trpc/routers/feed/index.ts @@ -67,18 +67,39 @@ export const feedRouter = router({ channelId: z.string(), }) ) - .output(FeedChannelModelSchema.nullable()) + .output( + z + .object({ + notificationIds: z.array(z.string()), + }) + .merge(FeedChannelModelSchema) + .nullable() + ) .query(async ({ input }) => { const { channelId, workspaceId } = input; - const channel = prisma.feedChannel.findFirst({ + const channel = await prisma.feedChannel.findFirst({ where: { workspaceId, id: channelId, }, + include: { + notifications: { + select: { + id: true, + }, + }, + }, }); - return channel; + if (!channel) { + return null; + } + + return { + ...channel, + notificationIds: channel?.notifications.map((n) => n.id), + }; }), updateChannelInfo: workspaceProcedure .meta( @@ -91,28 +112,48 @@ export const feedRouter = router({ z .object({ channelId: z.string(), + notificationIds: z.array(z.string()).default([]), }) .merge( FeedChannelModelSchema.pick({ name: true, + notifyFrequency: true, }) ) ) - .output(FeedChannelModelSchema.nullable()) + .output( + z + .object({ + notificationIds: z.array(z.string()), + }) + .merge(FeedChannelModelSchema) + .nullable() + ) .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: { workspaceId, id: channelId, }, data: { name, + notifyFrequency, + notifications: { + set: notificationIds.map((id) => ({ + id, + })), + }, }, }); - return channel; + if (!channel) { + return null; + } + + return { ...channel, notificationIds }; }), fetchEventsByCursor: workspaceProcedure .meta( @@ -161,20 +202,45 @@ export const feedRouter = router({ .input( FeedChannelModelSchema.pick({ 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 }) => { - const { name, workspaceId } = input; + const { name, workspaceId, notifyFrequency, notificationIds } = input; const channel = await prisma.feedChannel.create({ data: { workspaceId, 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 .meta(