feat: add feed archive page
This commit is contained in:
parent
3270164710
commit
87b4000c47
136
src/client/components/feed/FeedArchivePageButton.tsx
Normal file
136
src/client/components/feed/FeedArchivePageButton.tsx
Normal file
@ -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<FeedArchivePageButtonProps> =
|
||||
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 (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button size="icon" variant="outline">
|
||||
<LuArchive />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="flex h-[50vh] w-96 flex-col overflow-hidden"
|
||||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-lg font-bold">{t('Archived Events')}</h1>
|
||||
|
||||
<AlertConfirm onConfirm={handleClear}>
|
||||
<Button size="sm">{t('Clear')}</Button>
|
||||
</AlertConfirm>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2" />
|
||||
|
||||
<div className="flex-1">
|
||||
<DynamicVirtualList
|
||||
allData={fullEvents}
|
||||
estimateSize={100}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
onFetchNextPage={fetchNextPage}
|
||||
getItemKey={(index) => get(fullEvents, [index, 'id'])}
|
||||
renderItem={(item) => (
|
||||
<FeedEventItem
|
||||
className="animate-fade-in mb-2"
|
||||
event={item}
|
||||
actions={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
className="absolute right-0 top-0 h-6 w-6 overflow-hidden"
|
||||
onClick={() => handleUnArchive(item)}
|
||||
>
|
||||
<LuArchiveRestore size={12} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
renderEmpty={() => (
|
||||
<div className="w-full overflow-hidden p-4">
|
||||
<div className="text-muted text-center">
|
||||
{isInitialLoading
|
||||
? t('Loading...')
|
||||
: t('No archived events')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
FeedArchivePageButton.displayName = 'FeedArchivePageButton';
|
@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
@ -53,14 +35,7 @@ export const FeedEventItem: React.FC<{
|
||||
</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>
|
||||
{actions}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
@ -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 (
|
||||
<CommonWrapper
|
||||
header={
|
||||
@ -93,6 +114,8 @@ function PageComponent() {
|
||||
</DialogWrapper>
|
||||
)}
|
||||
|
||||
<FeedArchivePageButton channelId={channelId} />
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
@ -129,7 +152,20 @@ function PageComponent() {
|
||||
onFetchNextPage={fetchNextPage}
|
||||
getItemKey={(index) => get(fullEvents, [index, 'id'])}
|
||||
renderItem={(item) => (
|
||||
<FeedEventItem className="animate-fade-in mb-2" event={item} />
|
||||
<FeedEventItem
|
||||
className="animate-fade-in mb-2"
|
||||
event={item}
|
||||
actions={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
className="absolute right-0 top-0 h-6 w-6 overflow-hidden"
|
||||
onClick={() => handleArchive(item)}
|
||||
>
|
||||
<LuArchive size={12} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
renderEmpty={() => (
|
||||
<div className="w-full overflow-hidden p-4">
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user