feat: add VirtualList support for feed events
This commit is contained in:
parent
1b859e3176
commit
caf7e9ca72
@ -5,44 +5,51 @@ import dayjs from 'dayjs';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
import { MarkdownViewer } from '../MarkdownEditor';
|
||||
import { FeedIcon } from './FeedIcon';
|
||||
import { cn } from '@/utils/style';
|
||||
|
||||
type FeedEventItemType = AppRouterOutput['feed']['events'][number];
|
||||
|
||||
export const FeedEventItem: React.FC<{ event: FeedEventItemType }> = React.memo(
|
||||
({ event }) => {
|
||||
return (
|
||||
<div className="border-muted flex items-center rounded-lg border px-4 py-2">
|
||||
<div className="flex-1 gap-2">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm">
|
||||
<div className="border-muted rounded-lg border p-2">
|
||||
<FeedIcon source={event.source} size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<MarkdownViewer value={event.eventContent} />
|
||||
</div>
|
||||
export const FeedEventItem: React.FC<{
|
||||
className?: string;
|
||||
event: FeedEventItemType;
|
||||
}> = React.memo(({ className, event }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-muted flex items-center rounded-lg border px-4 py-2',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 gap-2">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm">
|
||||
<div className="border-muted rounded-lg border p-2">
|
||||
<FeedIcon source={event.source} size={24} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge>{event.source}</Badge>
|
||||
|
||||
<Badge variant="secondary">{event.eventName}</Badge>
|
||||
|
||||
{event.tags.map((tag) => (
|
||||
<Badge variant="outline">{tag}</Badge>
|
||||
))}
|
||||
<div>
|
||||
<MarkdownViewer value={event.eventContent} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="cursor-default self-end text-xs opacity-60">
|
||||
<div>{dayjs(event.createdAt).fromNow()}</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{dayjs(event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge>{event.source}</Badge>
|
||||
|
||||
<Badge variant="secondary">{event.eventName}</Badge>
|
||||
|
||||
{event.tags.map((tag) => (
|
||||
<Badge variant="outline">{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="cursor-default self-end text-xs opacity-60">
|
||||
<div>{dayjs(event.createdAt).fromNow()}</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{dayjs(event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
FeedEventItem.displayName = 'FeedEventItem';
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
@ -16,6 +15,7 @@ import { FeedIntegration } from '@/components/feed/FeedIntegration';
|
||||
import { DialogWrapper } from '@/components/DialogWrapper';
|
||||
import { useSocketSubscribeList } from '@/api/socketio';
|
||||
import { useMemo } from 'react';
|
||||
import { SimpleVirtualList } from '@/components/SimpleVirtualList';
|
||||
|
||||
export const Route = createFileRoute('/feed/$channelId/')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
@ -30,10 +30,18 @@ function PageComponent() {
|
||||
workspaceId,
|
||||
channelId,
|
||||
});
|
||||
const { data: events = [] } = trpc.feed.events.useQuery({
|
||||
workspaceId,
|
||||
channelId,
|
||||
});
|
||||
|
||||
const { data, hasNextPage, fetchNextPage, isFetchingNextPage } =
|
||||
trpc.feed.fetchEventsByCursor.useInfiniteQuery(
|
||||
{
|
||||
workspaceId,
|
||||
channelId,
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
}
|
||||
);
|
||||
|
||||
const deleteMutation = trpc.feed.deleteChannel.useMutation({
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
@ -55,8 +63,8 @@ function PageComponent() {
|
||||
});
|
||||
|
||||
const fullEvents = useMemo(
|
||||
() => [...realtimeEvents, ...events],
|
||||
[realtimeEvents, events]
|
||||
() => [...realtimeEvents, ...(data?.pages.flatMap((p) => p.items) ?? [])],
|
||||
[realtimeEvents, data]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -102,19 +110,21 @@ function PageComponent() {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{fullEvents && fullEvents.length === 0 ? (
|
||||
<div className="w-full overflow-hidden p-4">
|
||||
<FeedApiGuide channelId={channelId} />
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-full overflow-hidden p-4">
|
||||
<div className="space-y-2">
|
||||
{(fullEvents ?? []).map((event) => (
|
||||
<FeedEventItem key={event.id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
<div className="h-full w-full overflow-hidden p-4">
|
||||
<SimpleVirtualList
|
||||
allData={fullEvents}
|
||||
estimateSize={100}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
onFetchNextPage={fetchNextPage}
|
||||
renderItem={(item) => <FeedEventItem className="mb-2" event={item} />}
|
||||
renderEmpty={() => (
|
||||
<div className="w-full overflow-hidden p-4">
|
||||
<FeedApiGuide channelId={channelId} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CommonWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
import { prisma } from '../../../model/_client';
|
||||
import _ from 'lodash';
|
||||
import { buildFeedPublicOpenapi, feedIntegrationRouter } from './integration';
|
||||
import { fetchDataByCursor } from '../../../utils/prisma';
|
||||
|
||||
export const feedRouter = router({
|
||||
channels: workspaceProcedure
|
||||
@ -113,33 +114,42 @@ export const feedRouter = router({
|
||||
|
||||
return channel;
|
||||
}),
|
||||
events: workspaceProcedure
|
||||
fetchEventsByCursor: workspaceProcedure
|
||||
.meta(
|
||||
buildFeedOpenapi({
|
||||
method: 'GET',
|
||||
path: '/{channelId}/events',
|
||||
path: '/{channelId}/fetchEventsByCursor',
|
||||
description: 'Fetch workspace feed channel events',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
z.object({
|
||||
channelId: z.string(),
|
||||
limit: z.number().min(1).max(100).default(50),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
items: z.array(FeedEventModelSchema),
|
||||
nextCursor: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.output(z.array(FeedEventModelSchema))
|
||||
.query(async ({ input }) => {
|
||||
const { channelId } = input;
|
||||
const { channelId, cursor, limit } = input;
|
||||
|
||||
const events = await prisma.feedEvent.findMany({
|
||||
const { items, nextCursor } = await fetchDataByCursor(prisma.feedEvent, {
|
||||
where: {
|
||||
channelId: channelId,
|
||||
},
|
||||
take: 50,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
channelId,
|
||||
},
|
||||
limit,
|
||||
cursor,
|
||||
});
|
||||
|
||||
return events;
|
||||
return {
|
||||
items,
|
||||
nextCursor,
|
||||
};
|
||||
}),
|
||||
createChannel: workspaceOwnerProcedure
|
||||
.meta(
|
||||
|
Loading…
Reference in New Issue
Block a user