diff --git a/src/client/components/feed/FeedArchivePageButton.tsx b/src/client/components/feed/FeedArchivePageButton.tsx new file mode 100644 index 0000000..cd61c08 --- /dev/null +++ b/src/client/components/feed/FeedArchivePageButton.tsx @@ -0,0 +1,136 @@ +import React, { useMemo } from 'react'; +import { Button } from '../ui/button'; +import { LuArchive, LuArchiveRestore } from 'react-icons/lu'; +import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; +import { AppRouterOutput, trpc } from '@/api/trpc'; +import { useCurrentWorkspaceId } from '@/store/user'; +import { DynamicVirtualList } from '../DynamicVirtualList'; +import { get } from 'lodash-es'; +import { FeedEventItem } from './FeedEventItem'; +import { useTranslation } from '@i18next-toolkit/react'; +import { Separator } from '../ui/separator'; +import { useEvent } from '@/hooks/useEvent'; +import { toast } from 'sonner'; +import { AlertConfirm } from '../AlertConfirm'; + +type FeedItem = AppRouterOutput['feed']['fetchEventsByCursor']['items'][number]; + +interface FeedArchivePageButtonProps { + channelId: string; +} + +export const FeedArchivePageButton: React.FC = + React.memo((props) => { + const workspaceId = useCurrentWorkspaceId(); + const channelId = props.channelId; + const { t } = useTranslation(); + const unarchiveEventMutation = trpc.feed.unarchiveEvent.useMutation(); + const clearAllArchivedEventsMutation = + trpc.feed.clearAllArchivedEvents.useMutation(); + const trpcUtils = trpc.useUtils(); + + const { + data, + isInitialLoading, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + } = trpc.feed.fetchEventsByCursor.useInfiniteQuery( + { + workspaceId, + channelId, + archived: true, + }, + { + refetchOnMount: false, + refetchOnWindowFocus: false, + getNextPageParam: (lastPage) => lastPage.nextCursor, + } + ); + + const handleUnArchive = useEvent(async (event: FeedItem) => { + await unarchiveEventMutation.mutateAsync({ + workspaceId, + channelId, + eventId: event.id, + }); + trpcUtils.feed.fetchEventsByCursor.refetch(); + toast.success(t('Event unarchived')); + }); + + const handleClear = useEvent(async () => { + const count = await clearAllArchivedEventsMutation.mutateAsync({ + workspaceId, + channelId, + }); + trpcUtils.feed.fetchEventsByCursor.refetch(); + toast.success(t('{{count}} events cleared', { count })); + }); + + const fullEvents = useMemo( + () => data?.pages.flatMap((p) => p.items) ?? [], + [data] + ); + + return ( + + + + + +
+

{t('Archived Events')}

+ + + + +
+ + + +
+ get(fullEvents, [index, 'id'])} + renderItem={(item) => ( + handleUnArchive(item)} + > + + + } + /> + )} + renderEmpty={() => ( +
+
+ {isInitialLoading + ? t('Loading...') + : t('No archived events')} +
+
+ )} + /> +
+
+
+ ); + }); +FeedArchivePageButton.displayName = 'FeedArchivePageButton'; diff --git a/src/client/components/feed/FeedEventItem.tsx b/src/client/components/feed/FeedEventItem.tsx index 41c83f1..da8b4c0 100644 --- a/src/client/components/feed/FeedEventItem.tsx +++ b/src/client/components/feed/FeedEventItem.tsx @@ -6,11 +6,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; import { MarkdownViewer } from '../MarkdownEditor'; import { FeedIcon } from './FeedIcon'; 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 = @@ -19,22 +15,8 @@ type FeedEventItemType = export const FeedEventItem: React.FC<{ className?: string; event: FeedEventItemType; -}> = 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')); - }); - + actions?: React.ReactNode; +}> = React.memo(({ className, event, actions }) => { return (
- + {actions}
diff --git a/src/client/routes/feed/$channelId/index.tsx b/src/client/routes/feed/$channelId/index.tsx index 1cc441b..f3383ae 100644 --- a/src/client/routes/feed/$channelId/index.tsx +++ b/src/client/routes/feed/$channelId/index.tsx @@ -1,4 +1,9 @@ -import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc'; +import { + AppRouterOutput, + defaultErrorHandler, + defaultSuccessHandler, + trpc, +} from '@/api/trpc'; import { CommonHeader } from '@/components/CommonHeader'; import { CommonWrapper } from '@/components/CommonWrapper'; import { useCurrentWorkspaceId } from '@/store/user'; @@ -7,7 +12,7 @@ import { useTranslation } from '@i18next-toolkit/react'; import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { useEvent } from '@/hooks/useEvent'; import { AlertConfirm } from '@/components/AlertConfirm'; -import { LuPencil, LuTrash, LuWebhook } from 'react-icons/lu'; +import { LuArchive, LuPencil, LuTrash, LuWebhook } from 'react-icons/lu'; import { Button } from '@/components/ui/button'; import { FeedApiGuide } from '@/components/feed/FeedApiGuide'; import { FeedEventItem } from '@/components/feed/FeedEventItem'; @@ -17,12 +22,16 @@ import { useSocketSubscribeList } from '@/api/socketio'; import { useMemo } from 'react'; import { DynamicVirtualList } from '@/components/DynamicVirtualList'; import { get, reverse } from 'lodash-es'; +import { FeedArchivePageButton } from '@/components/feed/FeedArchivePageButton'; +import { toast } from 'sonner'; export const Route = createFileRoute('/feed/$channelId/')({ beforeLoad: routeAuthBeforeLoad, component: PageComponent, }); +type FeedItem = AppRouterOutput['feed']['fetchEventsByCursor']['items'][number]; + function PageComponent() { const { channelId } = Route.useParams<{ channelId: string }>(); const workspaceId = useCurrentWorkspaceId(); @@ -38,6 +47,7 @@ function PageComponent() { hasNextPage, fetchNextPage, isFetchingNextPage, + refetch, } = trpc.feed.fetchEventsByCursor.useInfiniteQuery( { workspaceId, @@ -56,6 +66,7 @@ function PageComponent() { const trpcUtils = trpc.useUtils(); const navigate = useNavigate(); + const archiveEventMutation = trpc.feed.archiveEvent.useMutation(); const handleDelete = useEvent(async () => { await deleteMutation.mutateAsync({ workspaceId, channelId }); trpcUtils.feed.channels.refetch(); @@ -77,6 +88,16 @@ function PageComponent() { [realtimeEvents, data] ); + const handleArchive = useEvent(async (event: FeedItem) => { + await archiveEventMutation.mutateAsync({ + workspaceId, + channelId: event.channelId, + eventId: event.id, + }); + trpcUtils.feed.fetchEventsByCursor.refetch(); + toast.success(t('Event archived')); + }); + return ( )} + + + } + /> )} renderEmpty={() => (
diff --git a/src/server/trpc/routers/feed/index.ts b/src/server/trpc/routers/feed/index.ts index 263b50d..fda32be 100644 --- a/src/server/trpc/routers/feed/index.ts +++ b/src/server/trpc/routers/feed/index.ts @@ -173,6 +173,7 @@ export const feedRouter = router({ channelId: z.string(), limit: z.number().min(1).max(100).default(50), cursor: z.string().optional(), + archived: z.boolean().default(false), }) ) .output( @@ -182,12 +183,12 @@ export const feedRouter = router({ }) ) .query(async ({ input }) => { - const { channelId, cursor, limit } = input; + const { channelId, cursor, limit, archived } = input; const { items, nextCursor } = await fetchDataByCursor(prisma.feedEvent, { where: { channelId, - archived: false, + archived, }, limit, cursor, @@ -362,6 +363,31 @@ export const feedRouter = router({ }, }); }), + clearAllArchivedEvents: workspaceOwnerProcedure + .meta( + buildFeedPublicOpenapi({ + method: 'PATCH', + path: '/{channelId}/clearAllArchivedEvents', + }) + ) + .input( + z.object({ + channelId: z.string(), + }) + ) + .output(z.number()) + .mutation(async ({ input }) => { + const { channelId } = input; + + const res = await prisma.feedEvent.deleteMany({ + where: { + channelId, + archived: true, + }, + }); + + return res.count; + }), integration: feedIntegrationRouter, });