feat: add feed page
This commit is contained in:
parent
f459c6beea
commit
96a5a33ad6
@ -1167,6 +1167,45 @@ export class FeedService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.channelId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedChannelInfo(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/info']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/info']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/feed/{channelId}/info',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
channelId: data.channelId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.channelId
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedUpdateChannelInfo(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/update']['post']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/update']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/workspace/{workspaceId}/feed/{channelId}/update',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
channelId: data.channelId
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
@ -1185,4 +1224,60 @@ export class FeedService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedCreateChannel(data: $OpenApiTs['/workspace/{workspaceId}/feed/createChannel']['post']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/createChannel']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/workspace/{workspaceId}/feed/createChannel',
|
||||
path: {
|
||||
workspaceId: data.workspaceId
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.channelId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedDeleteChannel(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}']['delete']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/workspace/{workspaceId}/feed/{channelId}',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
channelId: data.channelId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.channelId
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedSendEvent(data: $OpenApiTs['/feed/{channelId}/send']['post']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/send']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/feed/{channelId}/send',
|
||||
path: {
|
||||
channelId: data.channelId
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -1397,6 +1397,49 @@ export type $OpenApiTs = {
|
||||
};
|
||||
};
|
||||
};
|
||||
'/workspace/{workspaceId}/feed/{channelId}/info': {
|
||||
get: {
|
||||
req: {
|
||||
channelId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
res: {
|
||||
/**
|
||||
* Successful response
|
||||
*/
|
||||
200: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
'/workspace/{workspaceId}/feed/{channelId}/update': {
|
||||
post: {
|
||||
req: {
|
||||
channelId: string;
|
||||
requestBody: {
|
||||
name: string;
|
||||
};
|
||||
workspaceId: string;
|
||||
};
|
||||
res: {
|
||||
/**
|
||||
* Successful response
|
||||
*/
|
||||
200: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
'/workspace/{workspaceId}/feed/{channelId}/events': {
|
||||
get: {
|
||||
req: {
|
||||
@ -1423,4 +1466,80 @@ export type $OpenApiTs = {
|
||||
};
|
||||
};
|
||||
};
|
||||
'/workspace/{workspaceId}/feed/createChannel': {
|
||||
post: {
|
||||
req: {
|
||||
requestBody: {
|
||||
name: string;
|
||||
};
|
||||
workspaceId: string;
|
||||
};
|
||||
res: {
|
||||
/**
|
||||
* Successful response
|
||||
*/
|
||||
200: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
'/workspace/{workspaceId}/feed/{channelId}': {
|
||||
delete: {
|
||||
req: {
|
||||
channelId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
res: {
|
||||
/**
|
||||
* Successful response
|
||||
*/
|
||||
200: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
'/feed/{channelId}/send': {
|
||||
post: {
|
||||
req: {
|
||||
channelId: string;
|
||||
requestBody: {
|
||||
eventName: string;
|
||||
eventContent: string;
|
||||
tags: Array<(string)>;
|
||||
source: string;
|
||||
senderId?: string | null;
|
||||
senderName?: string | null;
|
||||
important: boolean;
|
||||
};
|
||||
};
|
||||
res: {
|
||||
/**
|
||||
* Successful response
|
||||
*/
|
||||
200: {
|
||||
id: string;
|
||||
channelId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
eventName: string;
|
||||
eventContent: string;
|
||||
tags: Array<(string)>;
|
||||
source: string;
|
||||
senderId?: string | null;
|
||||
senderName?: string | null;
|
||||
important: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
@ -4,6 +4,7 @@ import { Button } from './ui/button';
|
||||
import { LuCopy, LuCopyCheck } from 'react-icons/lu';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { ScrollBar } from './ui/scroll-area';
|
||||
|
||||
export const CodeBlock: React.FC<{
|
||||
code: string;
|
||||
@ -20,7 +21,7 @@ export const CodeBlock: React.FC<{
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="group relative overflow-auto">
|
||||
<div className="group relative w-full overflow-auto">
|
||||
<pre className="rounded-sm border border-zinc-800 bg-zinc-900 p-3 pr-12 text-sm">
|
||||
<code>{props.code}</code>
|
||||
</pre>
|
||||
|
36
src/client/components/feed/FeedApiGuide.tsx
Normal file
36
src/client/components/feed/FeedApiGuide.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import React from 'react';
|
||||
import { CodeBlock } from '../CodeBlock';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
|
||||
export const FeedApiGuide: React.FC<{ channelId: string }> = React.memo(
|
||||
(props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const code = `fetch('${window.location.origin}/open/feed/${props.channelId}/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
eventName: 'test name',
|
||||
eventContent: 'test content',
|
||||
tags: ['test'],
|
||||
source: 'custom',
|
||||
important: false,
|
||||
})
|
||||
})`;
|
||||
|
||||
return (
|
||||
<Card className="w-full overflow-hidden">
|
||||
<CardHeader>
|
||||
<div>{t('You can send any message into this channel with:')}</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full overflow-hidden">
|
||||
<CodeBlock code={code} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
);
|
||||
FeedApiGuide.displayName = 'FeedApiGuide';
|
81
src/client/components/feed/FeedChannelEditForm.tsx
Normal file
81
src/client/components/feed/FeedChannelEditForm.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useEventWithLoading } from '@/hooks/useEvent';
|
||||
import { Card, CardContent, CardFooter } from '@/components/ui/card';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import React from 'react';
|
||||
|
||||
const addFormSchema = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export type FeedChannelEditFormValues = z.infer<typeof addFormSchema>;
|
||||
|
||||
interface FeedChannelEditFormProps {
|
||||
defaultValues?: FeedChannelEditFormValues;
|
||||
onSubmit: (values: FeedChannelEditFormValues) => Promise<void>;
|
||||
}
|
||||
export const FeedChannelEditForm: React.FC<FeedChannelEditFormProps> =
|
||||
React.memo((props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<FeedChannelEditFormValues>({
|
||||
resolver: zodResolver(addFormSchema),
|
||||
defaultValues: props.defaultValues ?? {
|
||||
name: 'New Channel',
|
||||
},
|
||||
});
|
||||
|
||||
const [handleSubmit, isLoading] = useEventWithLoading(
|
||||
async (values: FeedChannelEditFormValues) => {
|
||||
await props.onSubmit(values);
|
||||
form.reset();
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-8">
|
||||
<Card>
|
||||
<CardContent className="pt-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Channel Name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('Channel Name to Display')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
<Button type="submit" loading={isLoading}>
|
||||
{props.defaultValues ? t('Update') : t('Create')}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
FeedChannelEditForm.displayName = 'FeedChannelEditForm';
|
23
src/client/components/feed/FeedEventItem.tsx
Normal file
23
src/client/components/feed/FeedEventItem.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { AppRouterOutput } from '@/api/trpc';
|
||||
import React from 'react';
|
||||
import { Badge } from '../ui/badge';
|
||||
|
||||
type FeedEventItemType = AppRouterOutput['feed']['events'][number];
|
||||
|
||||
export const FeedEventItem: React.FC<{ event: FeedEventItemType }> = React.memo(
|
||||
({ event }) => {
|
||||
return (
|
||||
<div className="border-muted rounded-lg border px-4 py-2">
|
||||
<div className="mb-2">{event.eventName}</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge>{event.source}</Badge>
|
||||
|
||||
{event.tags.map((tag) => (
|
||||
<Badge variant="secondary">{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
FeedEventItem.displayName = 'FeedEventItem';
|
@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
LuActivitySquare,
|
||||
LuAreaChart,
|
||||
LuFilePieChart,
|
||||
LuMonitorDot,
|
||||
@ -103,6 +104,12 @@ export const DesktopLayout: React.FC<LayoutProps> = React.memo((props) => {
|
||||
icon: RiSurveyLine,
|
||||
to: '/survey',
|
||||
},
|
||||
{
|
||||
title: t('Feed'),
|
||||
label: '',
|
||||
icon: LuActivitySquare,
|
||||
to: '/feed',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Separator />
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
LuActivitySquare,
|
||||
LuAreaChart,
|
||||
LuFilePieChart,
|
||||
LuMonitorDot,
|
||||
@ -79,6 +80,12 @@ export const MobileLayout: React.FC<LayoutProps> = React.memo((props) => {
|
||||
to="/survey"
|
||||
extraModal={true}
|
||||
/>
|
||||
<MobileNavItem
|
||||
title={t('Feed')}
|
||||
icon={LuActivitySquare}
|
||||
to="/feed"
|
||||
extraModal={true}
|
||||
/>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
@ -20,6 +20,7 @@ import { Route as RegisterImport } from './routes/register'
|
||||
import { Route as PageImport } from './routes/page'
|
||||
import { Route as MonitorImport } from './routes/monitor'
|
||||
import { Route as LoginImport } from './routes/login'
|
||||
import { Route as FeedImport } from './routes/feed'
|
||||
import { Route as DashboardImport } from './routes/dashboard'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as WebsiteOverviewImport } from './routes/website/overview'
|
||||
@ -35,12 +36,15 @@ import { Route as SettingsAuditLogImport } from './routes/settings/auditLog'
|
||||
import { Route as PageAddImport } from './routes/page/add'
|
||||
import { Route as PageSlugImport } from './routes/page/$slug'
|
||||
import { Route as MonitorAddImport } from './routes/monitor/add'
|
||||
import { Route as FeedAddImport } from './routes/feed/add'
|
||||
import { Route as WebsiteWebsiteIdIndexImport } from './routes/website/$websiteId/index'
|
||||
import { Route as SurveySurveyIdIndexImport } from './routes/survey/$surveyId/index'
|
||||
import { Route as MonitorMonitorIdIndexImport } from './routes/monitor/$monitorId/index'
|
||||
import { Route as FeedChannelIdIndexImport } from './routes/feed/$channelId/index'
|
||||
import { Route as WebsiteWebsiteIdConfigImport } from './routes/website/$websiteId/config'
|
||||
import { Route as SurveySurveyIdEditImport } from './routes/survey/$surveyId/edit'
|
||||
import { Route as MonitorMonitorIdEditImport } from './routes/monitor/$monitorId/edit'
|
||||
import { Route as FeedChannelIdEditImport } from './routes/feed/$channelId/edit'
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
@ -89,6 +93,11 @@ const LoginRoute = LoginImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const FeedRoute = FeedImport.update({
|
||||
path: '/feed',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const DashboardRoute = DashboardImport.update({
|
||||
path: '/dashboard',
|
||||
getParentRoute: () => rootRoute,
|
||||
@ -164,6 +173,11 @@ const MonitorAddRoute = MonitorAddImport.update({
|
||||
getParentRoute: () => MonitorRoute,
|
||||
} as any)
|
||||
|
||||
const FeedAddRoute = FeedAddImport.update({
|
||||
path: '/add',
|
||||
getParentRoute: () => FeedRoute,
|
||||
} as any)
|
||||
|
||||
const WebsiteWebsiteIdIndexRoute = WebsiteWebsiteIdIndexImport.update({
|
||||
path: '/$websiteId/',
|
||||
getParentRoute: () => WebsiteRoute,
|
||||
@ -179,6 +193,11 @@ const MonitorMonitorIdIndexRoute = MonitorMonitorIdIndexImport.update({
|
||||
getParentRoute: () => MonitorRoute,
|
||||
} as any)
|
||||
|
||||
const FeedChannelIdIndexRoute = FeedChannelIdIndexImport.update({
|
||||
path: '/$channelId/',
|
||||
getParentRoute: () => FeedRoute,
|
||||
} as any)
|
||||
|
||||
const WebsiteWebsiteIdConfigRoute = WebsiteWebsiteIdConfigImport.update({
|
||||
path: '/$websiteId/config',
|
||||
getParentRoute: () => WebsiteRoute,
|
||||
@ -194,6 +213,11 @@ const MonitorMonitorIdEditRoute = MonitorMonitorIdEditImport.update({
|
||||
getParentRoute: () => MonitorRoute,
|
||||
} as any)
|
||||
|
||||
const FeedChannelIdEditRoute = FeedChannelIdEditImport.update({
|
||||
path: '/$channelId/edit',
|
||||
getParentRoute: () => FeedRoute,
|
||||
} as any)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
@ -206,6 +230,10 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof DashboardImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/feed': {
|
||||
preLoaderRoute: typeof FeedImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/login': {
|
||||
preLoaderRoute: typeof LoginImport
|
||||
parentRoute: typeof rootRoute
|
||||
@ -242,6 +270,10 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof WebsiteImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/feed/add': {
|
||||
preLoaderRoute: typeof FeedAddImport
|
||||
parentRoute: typeof FeedImport
|
||||
}
|
||||
'/monitor/add': {
|
||||
preLoaderRoute: typeof MonitorAddImport
|
||||
parentRoute: typeof MonitorImport
|
||||
@ -294,6 +326,10 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof WebsiteOverviewImport
|
||||
parentRoute: typeof WebsiteImport
|
||||
}
|
||||
'/feed/$channelId/edit': {
|
||||
preLoaderRoute: typeof FeedChannelIdEditImport
|
||||
parentRoute: typeof FeedImport
|
||||
}
|
||||
'/monitor/$monitorId/edit': {
|
||||
preLoaderRoute: typeof MonitorMonitorIdEditImport
|
||||
parentRoute: typeof MonitorImport
|
||||
@ -306,6 +342,10 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof WebsiteWebsiteIdConfigImport
|
||||
parentRoute: typeof WebsiteImport
|
||||
}
|
||||
'/feed/$channelId/': {
|
||||
preLoaderRoute: typeof FeedChannelIdIndexImport
|
||||
parentRoute: typeof FeedImport
|
||||
}
|
||||
'/monitor/$monitorId/': {
|
||||
preLoaderRoute: typeof MonitorMonitorIdIndexImport
|
||||
parentRoute: typeof MonitorImport
|
||||
@ -326,6 +366,11 @@ declare module '@tanstack/react-router' {
|
||||
export const routeTree = rootRoute.addChildren([
|
||||
IndexRoute,
|
||||
DashboardRoute,
|
||||
FeedRoute.addChildren([
|
||||
FeedAddRoute,
|
||||
FeedChannelIdEditRoute,
|
||||
FeedChannelIdIndexRoute,
|
||||
]),
|
||||
LoginRoute,
|
||||
MonitorRoute.addChildren([
|
||||
MonitorAddRoute,
|
||||
|
88
src/client/routes/feed.tsx
Normal file
88
src/client/routes/feed.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { trpc } from '@/api/trpc';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { CommonList } from '@/components/CommonList';
|
||||
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useDataReady } from '@/hooks/useDataReady';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { Layout } from '@/components/layout';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { cn } from '@/utils/style';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import {
|
||||
createFileRoute,
|
||||
useNavigate,
|
||||
useRouterState,
|
||||
} from '@tanstack/react-router';
|
||||
import { LuPlus } from 'react-icons/lu';
|
||||
|
||||
export const Route = createFileRoute('/feed')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: PageComponent,
|
||||
});
|
||||
|
||||
function PageComponent() {
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const { t } = useTranslation();
|
||||
const { data: channels = [], isLoading } = trpc.feed.channels.useQuery({
|
||||
workspaceId,
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const pathname = useRouterState({
|
||||
select: (state) => state.location.pathname,
|
||||
});
|
||||
|
||||
const items = channels.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.name,
|
||||
number: item._count.events ?? 0,
|
||||
href: `/feed/${item.id}`,
|
||||
}));
|
||||
|
||||
useDataReady(
|
||||
() => channels.length > 0,
|
||||
() => {
|
||||
if (pathname === Route.fullPath) {
|
||||
navigate({
|
||||
to: '/feed/$channelId',
|
||||
params: {
|
||||
channelId: channels[0].id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleClickAdd = useEvent(() => {
|
||||
navigate({
|
||||
to: '/feed/add',
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Layout
|
||||
list={
|
||||
<CommonWrapper
|
||||
header={
|
||||
<CommonHeader
|
||||
title={t('Feed')}
|
||||
actions={
|
||||
<Button
|
||||
className={cn(pathname === '/feed/add' && '!bg-muted')}
|
||||
variant="outline"
|
||||
Icon={LuPlus}
|
||||
onClick={handleClickAdd}
|
||||
>
|
||||
{t('Add')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CommonList hasSearch={true} items={items} isLoading={isLoading} />
|
||||
</CommonWrapper>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
110
src/client/routes/feed/$channelId/edit.tsx
Normal file
110
src/client/routes/feed/$channelId/edit.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { defaultErrorHandler, trpc } from '@/api/trpc';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { Loading } from '@/components/Loading';
|
||||
import { ErrorTip } from '@/components/ErrorTip';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import {
|
||||
FeedChannelEditForm,
|
||||
FeedChannelEditFormValues,
|
||||
} from '@/components/feed/FeedChannelEditForm';
|
||||
|
||||
export const Route = createFileRoute('/feed/$channelId/edit')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: PageComponent,
|
||||
});
|
||||
|
||||
function PageComponent() {
|
||||
const { t } = useTranslation();
|
||||
const { channelId } = Route.useParams<{ channelId: string }>();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const navigate = useNavigate();
|
||||
const mutation = trpc.feed.updateChannelInfo.useMutation({
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
const { data: channel, isLoading } = trpc.feed.channelInfo.useQuery({
|
||||
workspaceId,
|
||||
channelId,
|
||||
});
|
||||
const trpcUtils = trpc.useUtils();
|
||||
|
||||
const handleSubmit = useEvent(async (values: FeedChannelEditFormValues) => {
|
||||
const res = await mutation.mutateAsync({
|
||||
...values,
|
||||
channelId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
trpcUtils.feed.channelInfo.setData(
|
||||
{
|
||||
channelId,
|
||||
workspaceId,
|
||||
},
|
||||
res
|
||||
);
|
||||
trpcUtils.feed.channels.setData(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
(prev) => {
|
||||
if (prev) {
|
||||
const index = prev.findIndex((item) => item.id === channelId);
|
||||
if (index >= 0) {
|
||||
prev[index] = {
|
||||
...prev[index],
|
||||
...res,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return prev;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
navigate({
|
||||
to: '/feed/$channelId',
|
||||
params: {
|
||||
channelId: res.id,
|
||||
},
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
navigate({
|
||||
to: '/feed',
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
return <ErrorTip />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CommonWrapper
|
||||
header={<CommonHeader title={channel.name} desc={t('Edit')} />}
|
||||
>
|
||||
<ScrollArea className="h-full overflow-hidden p-4">
|
||||
<Card>
|
||||
<CardContent className="pt-4">
|
||||
<FeedChannelEditForm
|
||||
defaultValues={channel}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ScrollArea>
|
||||
</CommonWrapper>
|
||||
);
|
||||
}
|
103
src/client/routes/feed/$channelId/index.tsx
Normal file
103
src/client/routes/feed/$channelId/index.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import {
|
||||
AppRouterOutput,
|
||||
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';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { AlertConfirm } from '@/components/AlertConfirm';
|
||||
import { LuPencil, LuTrash } from 'react-icons/lu';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Scrollbar } from '@radix-ui/react-scroll-area';
|
||||
import { Empty } from 'antd';
|
||||
import { FeedApiGuide } from '@/components/feed/FeedApiGuide';
|
||||
import { FeedEventItem } from '@/components/feed/FeedEventItem';
|
||||
|
||||
export const Route = createFileRoute('/feed/$channelId/')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: PageComponent,
|
||||
});
|
||||
|
||||
function PageComponent() {
|
||||
const { channelId } = Route.useParams<{ channelId: string }>();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const { t } = useTranslation();
|
||||
const { data: info } = trpc.feed.channelInfo.useQuery({
|
||||
workspaceId,
|
||||
channelId,
|
||||
});
|
||||
const { data: events } = trpc.feed.events.useQuery({
|
||||
workspaceId,
|
||||
channelId,
|
||||
});
|
||||
const deleteMutation = trpc.feed.deleteChannel.useMutation({
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
const trpcUtils = trpc.useUtils();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleDelete = useEvent(async () => {
|
||||
await deleteMutation.mutateAsync({ workspaceId, channelId });
|
||||
trpcUtils.feed.channels.refetch();
|
||||
navigate({
|
||||
to: '/feed',
|
||||
replace: true,
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<CommonWrapper
|
||||
header={
|
||||
<CommonHeader
|
||||
title={info?.name ?? ''}
|
||||
actions={
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
Icon={LuPencil}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: '/feed/$channelId/edit',
|
||||
params: {
|
||||
channelId,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<AlertConfirm
|
||||
title={t('Confirm to delete this channel?')}
|
||||
description={t('All feed will be remove')}
|
||||
content={t('It will permanently delete the relevant data')}
|
||||
onConfirm={handleDelete}
|
||||
>
|
||||
<Button variant="outline" size="icon" Icon={LuTrash} />
|
||||
</AlertConfirm>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{events && events.length === 0 ? (
|
||||
<div className="w-full overflow-hidden p-4">
|
||||
<FeedApiGuide channelId={channelId} />
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-full overflow-hidden p-4">
|
||||
{(events ?? []).map((event) => (
|
||||
<FeedEventItem key={event.id} event={event} />
|
||||
))}
|
||||
</ScrollArea>
|
||||
)}
|
||||
</CommonWrapper>
|
||||
);
|
||||
}
|
52
src/client/routes/feed/add.tsx
Normal file
52
src/client/routes/feed/add.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { defaultErrorHandler, trpc } from '@/api/trpc';
|
||||
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import {
|
||||
FeedChannelEditForm,
|
||||
FeedChannelEditFormValues,
|
||||
} from '@/components/feed/FeedChannelEditForm';
|
||||
|
||||
export const Route = createFileRoute('/feed/add')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: PageComponent,
|
||||
});
|
||||
|
||||
function PageComponent() {
|
||||
const { t } = useTranslation();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const createMutation = trpc.feed.createChannel.useMutation({
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
const utils = trpc.useUtils();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onSubmit = useEvent(async (values: FeedChannelEditFormValues) => {
|
||||
const res = await createMutation.mutateAsync({
|
||||
workspaceId,
|
||||
name: values.name,
|
||||
});
|
||||
|
||||
utils.feed.channels.refetch();
|
||||
|
||||
navigate({
|
||||
to: '/feed/$channelId',
|
||||
params: {
|
||||
channelId: res.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<CommonWrapper
|
||||
header={<h1 className="text-xl font-bold">{t('Add Channel')}</h1>}
|
||||
>
|
||||
<div className="p-4">
|
||||
<FeedChannelEditForm onSubmit={onSubmit} />
|
||||
</div>
|
||||
</CommonWrapper>
|
||||
);
|
||||
}
|
@ -9,7 +9,7 @@ import { Layout } from '@/components/layout';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { cn } from '@/utils/style';
|
||||
import { Trans, useTranslation } from '@i18next-toolkit/react';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import {
|
||||
createFileRoute,
|
||||
useNavigate,
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
import { OpenApiMetaInfo, router, workspaceProcedure } from '../trpc';
|
||||
import {
|
||||
OpenApiMetaInfo,
|
||||
publicProcedure,
|
||||
router,
|
||||
workspaceOwnerProcedure,
|
||||
workspaceProcedure,
|
||||
} from '../trpc';
|
||||
import { OPENAPI_TAG } from '../../utils/const';
|
||||
import { OpenApiMeta } from 'trpc-openapi';
|
||||
import { FeedChannelModelSchema, FeedEventModelSchema } from '../../prisma/zod';
|
||||
@ -43,6 +49,65 @@ export const feedRouter = router({
|
||||
|
||||
return channels;
|
||||
}),
|
||||
channelInfo: workspaceProcedure
|
||||
.meta(
|
||||
buildFeedOpenapi({
|
||||
method: 'GET',
|
||||
path: '/{channelId}/info',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
z.object({
|
||||
channelId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(FeedChannelModelSchema.nullable())
|
||||
.query(async ({ input }) => {
|
||||
const { channelId, workspaceId } = input;
|
||||
|
||||
const channel = prisma.feedChannel.findFirst({
|
||||
where: {
|
||||
workspaceId,
|
||||
id: channelId,
|
||||
},
|
||||
});
|
||||
|
||||
return channel;
|
||||
}),
|
||||
updateChannelInfo: workspaceProcedure
|
||||
.meta(
|
||||
buildFeedOpenapi({
|
||||
method: 'POST',
|
||||
path: '/{channelId}/update',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
channelId: z.string(),
|
||||
})
|
||||
.merge(
|
||||
FeedChannelModelSchema.pick({
|
||||
name: true,
|
||||
})
|
||||
)
|
||||
)
|
||||
.output(FeedChannelModelSchema.nullable())
|
||||
.mutation(async ({ input }) => {
|
||||
const { channelId, workspaceId, name } = input;
|
||||
|
||||
const channel = prisma.feedChannel.update({
|
||||
where: {
|
||||
workspaceId,
|
||||
id: channelId,
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
return channel;
|
||||
}),
|
||||
events: workspaceProcedure
|
||||
.meta(
|
||||
buildFeedOpenapi({
|
||||
@ -67,6 +132,93 @@ export const feedRouter = router({
|
||||
|
||||
return events;
|
||||
}),
|
||||
createChannel: workspaceOwnerProcedure
|
||||
.meta(
|
||||
buildFeedOpenapi({
|
||||
method: 'POST',
|
||||
path: '/createChannel',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
FeedChannelModelSchema.pick({
|
||||
name: true,
|
||||
})
|
||||
)
|
||||
.output(FeedChannelModelSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { name, workspaceId } = input;
|
||||
|
||||
const channel = await prisma.feedChannel.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
return channel;
|
||||
}),
|
||||
deleteChannel: workspaceOwnerProcedure
|
||||
.meta(
|
||||
buildFeedOpenapi({
|
||||
method: 'DELETE',
|
||||
path: '/{channelId}',
|
||||
})
|
||||
)
|
||||
.input(
|
||||
z.object({
|
||||
channelId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(FeedChannelModelSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { channelId, workspaceId } = input;
|
||||
|
||||
const channel = await prisma.feedChannel.delete({
|
||||
where: {
|
||||
workspaceId,
|
||||
id: channelId,
|
||||
},
|
||||
});
|
||||
|
||||
return channel;
|
||||
}),
|
||||
sendEvent: publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
tags: [OPENAPI_TAG.FEED],
|
||||
protect: false,
|
||||
method: 'POST',
|
||||
path: '/feed/{channelId}/send',
|
||||
},
|
||||
})
|
||||
.input(
|
||||
FeedEventModelSchema.pick({
|
||||
eventName: true,
|
||||
eventContent: true,
|
||||
tags: true,
|
||||
source: true,
|
||||
senderId: true,
|
||||
senderName: true,
|
||||
important: true,
|
||||
}).merge(
|
||||
z.object({
|
||||
channelId: z.string(),
|
||||
})
|
||||
)
|
||||
)
|
||||
.output(FeedEventModelSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { channelId, ...data } = input;
|
||||
|
||||
const event = await prisma.feedEvent.create({
|
||||
data: {
|
||||
...data,
|
||||
channelId: channelId,
|
||||
},
|
||||
});
|
||||
|
||||
return event;
|
||||
}),
|
||||
});
|
||||
|
||||
function buildFeedOpenapi(meta: OpenApiMetaInfo): OpenApiMeta {
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user