Compare commits

...

3 Commits

Author SHA1 Message Date
moonrailgun
cbd6821def chore: update survey edit form 2024-10-11 00:58:10 +08:00
moonrailgun
8a6a75f3f5 feat: survey add webhook url field which can send webhook when receive any survey 2024-10-11 00:26:34 +08:00
moonrailgun
fd63f2a22e feat: add survey webhook 2024-10-10 23:40:24 +08:00
7 changed files with 65 additions and 15 deletions

View File

@ -1,12 +1,7 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useEvent, useEventWithLoading } from '@/hooks/useEvent'; import { useEventWithLoading } from '@/hooks/useEvent';
import { useCurrentWorkspaceId } from '@/store/user';
import { defaultErrorHandler, trpc } from '@/api/trpc';
import { Card, CardContent, CardFooter } from '@/components/ui/card'; import { Card, CardContent, CardFooter } from '@/components/ui/card';
import { CommonWrapper } from '@/components/CommonWrapper';
import { routeAuthBeforeLoad } from '@/utils/route';
import { z } from 'zod'; import { z } from 'zod';
import { import {
Form, Form,
@ -18,7 +13,7 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form'; } from '@/components/ui/form';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { useForm, useFieldArray, useWatch } from 'react-hook-form'; import { useForm, useFieldArray } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { generateRandomString } from '@/utils/common'; import { generateRandomString } from '@/utils/common';
import { LuArrowDown, LuArrowUp, LuMinus, LuPlus } from 'react-icons/lu'; import { LuArrowDown, LuArrowUp, LuMinus, LuPlus } from 'react-icons/lu';
@ -49,6 +44,7 @@ const addFormSchema = z.object({
}), }),
feedChannelIds: z.array(z.string()), feedChannelIds: z.array(z.string()),
feedTemplate: z.string(), feedTemplate: z.string(),
webhookUrl: z.string().url().or(z.literal('')),
}); });
export type SurveyEditFormValues = z.infer<typeof addFormSchema>; export type SurveyEditFormValues = z.infer<typeof addFormSchema>;
@ -80,6 +76,7 @@ export const SurveyEditForm: React.FC<SurveyEditFormProps> = React.memo(
}, },
feedChannelIds: [], feedChannelIds: [],
feedTemplate: '', feedTemplate: '',
webhookUrl: '',
}, },
}); });
@ -87,7 +84,7 @@ export const SurveyEditForm: React.FC<SurveyEditFormProps> = React.memo(
const [handleSubmit, isLoading] = useEventWithLoading( const [handleSubmit, isLoading] = useEventWithLoading(
async (values: SurveyEditFormValues) => { async (values: SurveyEditFormValues) => {
await props.onSubmit(values); await props.onSubmit({ ...values });
form.reset(); form.reset();
} }
); );
@ -297,6 +294,23 @@ export const SurveyEditForm: React.FC<SurveyEditFormProps> = React.memo(
)} )}
/> />
)} )}
<FormField
control={form.control}
name="webhookUrl"
render={({ field }) => (
<FormItem>
<FormLabel optional={true}>{t('Webhook Url')}</FormLabel>
<FormControl className="w-full">
<Input {...field} />
</FormControl>
<FormDescription>
{t('Optional, webhook url to send survey payload')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent> </CardContent>
<CardFooter> <CardFooter>

View File

@ -2,7 +2,7 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId } from '@/store/user';
import { trpc } from '@/api/trpc'; import { defaultErrorHandler, trpc } from '@/api/trpc';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { CommonWrapper } from '@/components/CommonWrapper'; import { CommonWrapper } from '@/components/CommonWrapper';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
@ -25,7 +25,9 @@ function PageComponent() {
const { surveyId } = Route.useParams<{ surveyId: string }>(); const { surveyId } = Route.useParams<{ surveyId: string }>();
const workspaceId = useCurrentWorkspaceId(); const workspaceId = useCurrentWorkspaceId();
const navigate = useNavigate(); const navigate = useNavigate();
const mutation = trpc.survey.update.useMutation(); const mutation = trpc.survey.update.useMutation({
onError: defaultErrorHandler,
});
const { data: survey, isLoading } = trpc.survey.get.useQuery({ const { data: survey, isLoading } = trpc.survey.get.useQuery({
workspaceId, workspaceId,
surveyId, surveyId,

View File

@ -1,3 +1,8 @@
import { version } from '@tianji/shared';
import axios from 'axios';
axios.defaults.headers.common['User-Agent'] = `tianji/${version}`;
(BigInt.prototype as any).toJSON = function () { (BigInt.prototype as any).toJSON = function () {
const int = Number.parseInt(this.toString()); const int = Number.parseInt(this.toString());
return int ?? this.toString(); return int ?? this.toString();

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Survey" ADD COLUMN "webhookUrl" TEXT NOT NULL DEFAULT '';

View File

@ -495,6 +495,7 @@ model Survey {
payload Json @db.Json payload Json @db.Json
feedChannelIds String[] @default([]) // send survey result to feed channel feedChannelIds String[] @default([]) // send survey result to feed channel
feedTemplate String @default("") // send survey result to feed channel feedTemplate String @default("") // send survey result to feed channel
webhookUrl String @default("")
createdAt DateTime @default(now()) @db.Timestamptz(6) createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6)

View File

@ -18,6 +18,7 @@ export const SurveyModelSchema = z.object({
payload: imports.SurveyPayloadSchema, payload: imports.SurveyPayloadSchema,
feedChannelIds: z.string().array(), feedChannelIds: z.string().array(),
feedTemplate: z.string(), feedTemplate: z.string(),
webhookUrl: z.string(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
}) })

View File

@ -22,6 +22,7 @@ import { Prisma } from '@prisma/client';
import { createFeedEvent } from '../../model/feed/event.js'; import { createFeedEvent } from '../../model/feed/event.js';
import { formatString } from '../../utils/template.js'; import { formatString } from '../../utils/template.js';
import { logger } from '../../utils/logger.js'; import { logger } from '../../utils/logger.js';
import axios from 'axios';
export const surveyRouter = router({ export const surveyRouter = router({
all: workspaceProcedure all: workspaceProcedure
@ -154,7 +155,7 @@ export const surveyRouter = router({
const sessionId = hashUuid(workspaceId, surveyId, ip, userAgent!); const sessionId = hashUuid(workspaceId, surveyId, ip, userAgent!);
await prisma.surveyResult.create({ const result = await prisma.surveyResult.create({
data: { data: {
surveyId, surveyId,
sessionId, sessionId,
@ -191,11 +192,11 @@ export const surveyRouter = router({
survey.feedTemplate || survey.feedTemplate ||
'survey {{_surveyName}} receive a new record.'; 'survey {{_surveyName}} receive a new record.';
survey.feedChannelIds.forEach((channelId) => { survey.feedChannelIds.forEach(async (channelId) => {
try { try {
const surveyPayload = SurveyPayloadSchema.parse(survey.payload); const surveyPayload = SurveyPayloadSchema.parse(survey.payload);
createFeedEvent(workspaceId, { await createFeedEvent(workspaceId, {
channelId: channelId, channelId: channelId,
eventName: 'receive', eventName: 'receive',
eventContent: formatString(templateStr, { eventContent: formatString(templateStr, {
@ -216,6 +217,19 @@ export const surveyRouter = router({
} }
}); });
} }
if (survey && survey.webhookUrl) {
axios
.post(survey.webhookUrl, {
...result,
})
.then(() => {
logger.info(
`[surveySubmitWebhook] send webhooks to ${survey.webhookUrl} success!`
);
})
.catch((err) => logger.error('[surveySubmitWebhook]', err));
}
}); });
return 'success'; return 'success';
@ -233,12 +247,19 @@ export const surveyRouter = router({
payload: SurveyPayloadSchema, payload: SurveyPayloadSchema,
feedChannelIds: z.array(z.string()), feedChannelIds: z.array(z.string()),
feedTemplate: z.string(), feedTemplate: z.string(),
webhookUrl: z.string(),
}) })
) )
.output(SurveyModelSchema) .output(SurveyModelSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const { workspaceId, name, payload, feedChannelIds, feedTemplate } = const {
input; workspaceId,
name,
payload,
feedChannelIds,
feedTemplate,
webhookUrl,
} = input;
const res = await prisma.survey.create({ const res = await prisma.survey.create({
data: { data: {
@ -247,6 +268,7 @@ export const surveyRouter = router({
payload, payload,
feedChannelIds, feedChannelIds,
feedTemplate, feedTemplate,
webhookUrl,
}, },
}); });
@ -266,6 +288,7 @@ export const surveyRouter = router({
payload: SurveyPayloadSchema.optional(), payload: SurveyPayloadSchema.optional(),
feedChannelIds: z.array(z.string()).optional(), feedChannelIds: z.array(z.string()).optional(),
feedTemplate: z.string().optional(), feedTemplate: z.string().optional(),
webhookUrl: z.string().optional(),
}) })
) )
.output(SurveyModelSchema) .output(SurveyModelSchema)
@ -277,6 +300,7 @@ export const surveyRouter = router({
payload, payload,
feedChannelIds, feedChannelIds,
feedTemplate, feedTemplate,
webhookUrl,
} = input; } = input;
const res = await prisma.survey.update({ const res = await prisma.survey.update({
@ -289,6 +313,7 @@ export const surveyRouter = router({
payload, payload,
feedChannelIds, feedChannelIds,
feedTemplate, feedTemplate,
webhookUrl,
}, },
}); });