feat: add archive feature

This commit is contained in:
moonrailgun 2024-08-31 00:58:44 +08:00
parent 33de808f3e
commit 3270164710
5 changed files with 94 additions and 4 deletions

View File

@ -1,4 +1,4 @@
import { AppRouterOutput } from '@/api/trpc'; import { AppRouterOutput, trpc } from '@/api/trpc';
import React from 'react'; import React from 'react';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@ -6,6 +6,12 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
import { MarkdownViewer } from '../MarkdownEditor'; import { MarkdownViewer } from '../MarkdownEditor';
import { FeedIcon } from './FeedIcon'; import { FeedIcon } from './FeedIcon';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { Button } from '../ui/button';
import { LuArchive } from 'react-icons/lu';
import { useCurrentWorkspaceId } from '@/store/user';
import { useEvent } from '@/hooks/useEvent';
import { toast } from 'sonner';
import { useTranslation } from '@i18next-toolkit/react';
type FeedEventItemType = type FeedEventItemType =
AppRouterOutput['feed']['fetchEventsByCursor']['items'][number]; AppRouterOutput['feed']['fetchEventsByCursor']['items'][number];
@ -14,6 +20,21 @@ export const FeedEventItem: React.FC<{
className?: string; className?: string;
event: FeedEventItemType; event: FeedEventItemType;
}> = React.memo(({ className, event }) => { }> = React.memo(({ className, event }) => {
const workspaceId = useCurrentWorkspaceId();
const archiveEventMutation = trpc.feed.archiveEvent.useMutation();
const trpcUtils = trpc.useUtils();
const { t } = useTranslation();
const handleArchive = useEvent(async () => {
await archiveEventMutation.mutateAsync({
workspaceId,
channelId: event.channelId,
eventId: event.id,
});
trpcUtils.feed.fetchEventsByCursor.refetch();
toast.success(t('Event archived'));
});
return ( return (
<div <div
className={cn( className={cn(
@ -21,16 +42,26 @@ export const FeedEventItem: React.FC<{
className className
)} )}
> >
<div className="flex-1 gap-2 overflow-hidden"> <div className="relative flex-1 gap-2 overflow-hidden">
<div className="mb-2 flex w-full items-center gap-2 overflow-hidden text-sm"> <div className="mb-2 flex w-full items-center gap-2 overflow-hidden text-sm">
<div className="border-muted rounded-lg border p-2"> <div className="border-muted rounded-lg border p-2">
<FeedIcon source={event.source} size={24} /> <FeedIcon source={event.source} size={24} />
</div> </div>
<div>
<div className="overflow-hidden">
<MarkdownViewer value={event.eventContent} /> <MarkdownViewer value={event.eventContent} />
</div> </div>
</div> </div>
<Button
size="icon"
variant="secondary"
className="absolute right-0 top-0 h-6 w-6 overflow-hidden"
onClick={handleArchive}
>
<LuArchive size={12} />
</Button>
<div className="flex justify-between"> <div className="flex justify-between">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Badge>{event.source}</Badge> <Badge>{event.source}</Badge>
@ -46,7 +77,7 @@ export const FeedEventItem: React.FC<{
<TooltipTrigger className="cursor-default self-end text-xs opacity-60"> <TooltipTrigger className="cursor-default self-end text-xs opacity-60">
<div>{dayjs(event.createdAt).fromNow()}</div> <div>{dayjs(event.createdAt).fromNow()}</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent side="left">
<p>{dayjs(event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p> <p>{dayjs(event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "FeedEvent" ADD COLUMN "archived" BOOLEAN NOT NULL DEFAULT false;

View File

@ -531,6 +531,7 @@ model FeedEvent {
senderName String? // use for display who senderName String? // use for display who
url String? // url link url String? // url link
important Boolean important Boolean
archived Boolean @default(false) @db.Boolean
channel FeedChannel @relation(fields: [channelId], references: [id], onUpdate: Cascade, onDelete: Cascade) channel FeedChannel @relation(fields: [channelId], references: [id], onUpdate: Cascade, onDelete: Cascade)

View File

@ -15,6 +15,7 @@ export const FeedEventModelSchema = z.object({
senderName: z.string().nullish(), senderName: z.string().nullish(),
url: z.string().nullish(), url: z.string().nullish(),
important: z.boolean(), important: z.boolean(),
archived: z.boolean(),
}) })
export interface CompleteFeedEvent extends z.infer<typeof FeedEventModelSchema> { export interface CompleteFeedEvent extends z.infer<typeof FeedEventModelSchema> {

View File

@ -187,6 +187,7 @@ export const feedRouter = router({
const { items, nextCursor } = await fetchDataByCursor(prisma.feedEvent, { const { items, nextCursor } = await fetchDataByCursor(prisma.feedEvent, {
where: { where: {
channelId, channelId,
archived: false,
}, },
limit, limit,
cursor, cursor,
@ -307,6 +308,60 @@ export const feedRouter = router({
return event; return event;
}), }),
archiveEvent: workspaceOwnerProcedure
.meta(
buildFeedPublicOpenapi({
method: 'PATCH',
path: '/{channelId}/{eventId}/archive',
})
)
.input(
z.object({
channelId: z.string(),
eventId: z.string(),
})
)
.output(z.void())
.mutation(async ({ input }) => {
const { channelId, eventId } = input;
await prisma.feedEvent.update({
data: {
archived: true,
},
where: {
id: eventId,
channelId,
},
});
}),
unarchiveEvent: workspaceOwnerProcedure
.meta(
buildFeedPublicOpenapi({
method: 'PATCH',
path: '/{channelId}/{eventId}/unarchive',
})
)
.input(
z.object({
channelId: z.string(),
eventId: z.string(),
})
)
.output(z.void())
.mutation(async ({ input }) => {
const { channelId, eventId } = input;
await prisma.feedEvent.update({
data: {
archived: false,
},
where: {
id: eventId,
channelId,
},
});
}),
integration: feedIntegrationRouter, integration: feedIntegrationRouter,
}); });