feat: survey basic fe framework and add new form
This commit is contained in:
parent
27642625ac
commit
010fd00be3
@ -11,7 +11,7 @@ export const TipIcon: React.FC<TipIconProps> = React.memo((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger type="button">
|
||||||
<LuHelpCircle className={className} />
|
<LuHelpCircle className={className} />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
255
src/client/components/survey/SurveyEditForm.tsx
Normal file
255
src/client/components/survey/SurveyEditForm.tsx
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useEvent, useEventWithLoading } from '@/hooks/useEvent';
|
||||||
|
import { useCurrentWorkspaceId } from '@/store/user';
|
||||||
|
import { defaultErrorHandler, trpc } from '@/api/trpc';
|
||||||
|
import { Card, CardContent, CardFooter } from '@/components/ui/card';
|
||||||
|
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||||
|
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { useForm, useFieldArray } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { generateRandomString } from '@/utils/common';
|
||||||
|
import { LuArrowDown, LuArrowUp, LuMinus, LuPlus } from 'react-icons/lu';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { TipIcon } from '../TipIcon';
|
||||||
|
import { cn } from '@/utils/style';
|
||||||
|
import { Switch } from '../ui/switch';
|
||||||
|
|
||||||
|
const addFormSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
payload: z.object({
|
||||||
|
items: z.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
type: z.enum(['text', 'select', 'email']),
|
||||||
|
options: z.array(z.string()).optional(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SurveyEditFormValues = z.infer<typeof addFormSchema>;
|
||||||
|
|
||||||
|
function generateDefaultItem() {
|
||||||
|
return {
|
||||||
|
label: 'New Field',
|
||||||
|
name: 'field-' + generateRandomString(4),
|
||||||
|
type: 'text' as const,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SurveyEditFormProps {
|
||||||
|
defaultValues?: SurveyEditFormValues;
|
||||||
|
onSubmit: (values: SurveyEditFormValues) => Promise<void>;
|
||||||
|
}
|
||||||
|
export const SurveyEditForm: React.FC<SurveyEditFormProps> = React.memo(
|
||||||
|
(props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [advancedMode, setAdvancedMode] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<SurveyEditFormValues>({
|
||||||
|
resolver: zodResolver(addFormSchema),
|
||||||
|
defaultValues: props.defaultValues ?? {
|
||||||
|
name: 'New Survey',
|
||||||
|
payload: {
|
||||||
|
items: [generateDefaultItem()],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [handleSubmit, isLoading] = useEventWithLoading(
|
||||||
|
async (values: SurveyEditFormValues) => {
|
||||||
|
await props.onSubmit(values);
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { fields, append, swap, insert, remove } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: 'payload.items',
|
||||||
|
});
|
||||||
|
|
||||||
|
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('Survey Name')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t('Survey Name to Display')}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="border-muted mt-2 rounded-lg border p-4">
|
||||||
|
<h2 className="mb-2 font-bold leading-6">{t('Form Info')}</h2>
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={advancedMode}
|
||||||
|
onCheckedChange={(checked) => setAdvancedMode(checked)}
|
||||||
|
/>
|
||||||
|
<div className="text-sm">{t('Advanced Mode')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{fields.map((field, i) => (
|
||||||
|
<div key={field.id} className="mb-2 flex gap-1">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Display Label')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...form.register(`payload.items.${i}.label`)} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
{advancedMode && (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t('Name')}
|
||||||
|
<TipIcon
|
||||||
|
className="ml-1"
|
||||||
|
content={t('Use for storage')}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...form.register(`payload.items.${i}.name`)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('Type')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
defaultValue="text"
|
||||||
|
onValueChange={(val) =>
|
||||||
|
form.setValue(`payload.items.${i}.type`, val as any)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
className="w-[100px]"
|
||||||
|
{...form.register(`payload.items.${i}.type`)}
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="text">{t('Text')}</SelectItem>
|
||||||
|
<SelectItem value="email">{t('Email')}</SelectItem>
|
||||||
|
{/* <SelectItem value="select">
|
||||||
|
{t('Select')}
|
||||||
|
</SelectItem> */}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
{/* actions */}
|
||||||
|
<div className="grid min-w-10 grid-flow-col grid-cols-2 grid-rows-2 gap-0.5 self-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="dashed"
|
||||||
|
size="icon"
|
||||||
|
className="h-5 w-5 text-xs"
|
||||||
|
Icon={LuMinus}
|
||||||
|
onClick={() => {
|
||||||
|
remove(i);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="dashed"
|
||||||
|
size="icon"
|
||||||
|
className="h-5 w-5 text-xs"
|
||||||
|
Icon={LuPlus}
|
||||||
|
onClick={() => {
|
||||||
|
insert(i + 1, generateDefaultItem());
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="dashed"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
'h-5 w-5 text-xs',
|
||||||
|
i === 0 && 'opacity-0'
|
||||||
|
)}
|
||||||
|
Icon={LuArrowUp}
|
||||||
|
onClick={() => {
|
||||||
|
swap(i, i - 1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="dashed"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
'h-5 w-5 text-xs',
|
||||||
|
i === fields.length - 1 && 'opacity-0'
|
||||||
|
)}
|
||||||
|
Icon={LuArrowDown}
|
||||||
|
onClick={() => {
|
||||||
|
swap(i, i + 1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="mt-2 flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="dashed"
|
||||||
|
type="button"
|
||||||
|
Icon={LuPlus}
|
||||||
|
size="icon"
|
||||||
|
onClick={() => append(generateDefaultItem())}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<CardFooter>
|
||||||
|
<Button type="submit" loading={isLoading}>
|
||||||
|
{t('Create')}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
SurveyEditForm.displayName = 'SurveyEditForm';
|
@ -4,6 +4,7 @@ import {
|
|||||||
LuFilePieChart,
|
LuFilePieChart,
|
||||||
LuMonitorDot,
|
LuMonitorDot,
|
||||||
LuServer,
|
LuServer,
|
||||||
|
LuTableProperties,
|
||||||
LuWifi,
|
LuWifi,
|
||||||
} from 'react-icons/lu';
|
} from 'react-icons/lu';
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
@ -97,6 +98,12 @@ export const DesktopLayout: React.FC<LayoutProps> = React.memo((props) => {
|
|||||||
icon: LuFilePieChart,
|
icon: LuFilePieChart,
|
||||||
to: '/page',
|
to: '/page',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t('Survey'),
|
||||||
|
label: String(serviceCount?.survey ?? ''),
|
||||||
|
icon: LuTableProperties,
|
||||||
|
to: '/survey',
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as WebsiteImport } from './routes/website'
|
import { Route as WebsiteImport } from './routes/website'
|
||||||
import { Route as TelemetryImport } from './routes/telemetry'
|
import { Route as TelemetryImport } from './routes/telemetry'
|
||||||
|
import { Route as SurveyImport } from './routes/survey'
|
||||||
import { Route as SettingsImport } from './routes/settings'
|
import { Route as SettingsImport } from './routes/settings'
|
||||||
import { Route as ServerImport } from './routes/server'
|
import { Route as ServerImport } from './routes/server'
|
||||||
import { Route as RegisterImport } from './routes/register'
|
import { Route as RegisterImport } from './routes/register'
|
||||||
@ -25,6 +26,8 @@ import { Route as WebsiteOverviewImport } from './routes/website/overview'
|
|||||||
import { Route as WebsiteAddImport } from './routes/website/add'
|
import { Route as WebsiteAddImport } from './routes/website/add'
|
||||||
import { Route as TelemetryAddImport } from './routes/telemetry/add'
|
import { Route as TelemetryAddImport } from './routes/telemetry/add'
|
||||||
import { Route as TelemetryTelemetryIdImport } from './routes/telemetry/$telemetryId'
|
import { Route as TelemetryTelemetryIdImport } from './routes/telemetry/$telemetryId'
|
||||||
|
import { Route as SurveyAddImport } from './routes/survey/add'
|
||||||
|
import { Route as SurveySurveyIdImport } from './routes/survey/$surveyId'
|
||||||
import { Route as StatusSlugImport } from './routes/status/$slug'
|
import { Route as StatusSlugImport } from './routes/status/$slug'
|
||||||
import { Route as SettingsUsageImport } from './routes/settings/usage'
|
import { Route as SettingsUsageImport } from './routes/settings/usage'
|
||||||
import { Route as SettingsProfileImport } from './routes/settings/profile'
|
import { Route as SettingsProfileImport } from './routes/settings/profile'
|
||||||
@ -50,6 +53,11 @@ const TelemetryRoute = TelemetryImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const SurveyRoute = SurveyImport.update({
|
||||||
|
path: '/survey',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const SettingsRoute = SettingsImport.update({
|
const SettingsRoute = SettingsImport.update({
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@ -110,6 +118,16 @@ const TelemetryTelemetryIdRoute = TelemetryTelemetryIdImport.update({
|
|||||||
getParentRoute: () => TelemetryRoute,
|
getParentRoute: () => TelemetryRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const SurveyAddRoute = SurveyAddImport.update({
|
||||||
|
path: '/add',
|
||||||
|
getParentRoute: () => SurveyRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const SurveySurveyIdRoute = SurveySurveyIdImport.update({
|
||||||
|
path: '/$surveyId',
|
||||||
|
getParentRoute: () => SurveyRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const StatusSlugRoute = StatusSlugImport.update({
|
const StatusSlugRoute = StatusSlugImport.update({
|
||||||
path: '/status/$slug',
|
path: '/status/$slug',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@ -206,6 +224,10 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof SettingsImport
|
preLoaderRoute: typeof SettingsImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/survey': {
|
||||||
|
preLoaderRoute: typeof SurveyImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/telemetry': {
|
'/telemetry': {
|
||||||
preLoaderRoute: typeof TelemetryImport
|
preLoaderRoute: typeof TelemetryImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
@ -246,6 +268,14 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof StatusSlugImport
|
preLoaderRoute: typeof StatusSlugImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/survey/$surveyId': {
|
||||||
|
preLoaderRoute: typeof SurveySurveyIdImport
|
||||||
|
parentRoute: typeof SurveyImport
|
||||||
|
}
|
||||||
|
'/survey/add': {
|
||||||
|
preLoaderRoute: typeof SurveyAddImport
|
||||||
|
parentRoute: typeof SurveyImport
|
||||||
|
}
|
||||||
'/telemetry/$telemetryId': {
|
'/telemetry/$telemetryId': {
|
||||||
preLoaderRoute: typeof TelemetryTelemetryIdImport
|
preLoaderRoute: typeof TelemetryTelemetryIdImport
|
||||||
parentRoute: typeof TelemetryImport
|
parentRoute: typeof TelemetryImport
|
||||||
@ -301,6 +331,7 @@ export const routeTree = rootRoute.addChildren([
|
|||||||
SettingsProfileRoute,
|
SettingsProfileRoute,
|
||||||
SettingsUsageRoute,
|
SettingsUsageRoute,
|
||||||
]),
|
]),
|
||||||
|
SurveyRoute.addChildren([SurveySurveyIdRoute, SurveyAddRoute]),
|
||||||
TelemetryRoute.addChildren([TelemetryTelemetryIdRoute, TelemetryAddRoute]),
|
TelemetryRoute.addChildren([TelemetryTelemetryIdRoute, TelemetryAddRoute]),
|
||||||
WebsiteRoute.addChildren([
|
WebsiteRoute.addChildren([
|
||||||
WebsiteAddRoute,
|
WebsiteAddRoute,
|
||||||
|
89
src/client/routes/survey.tsx
Normal file
89
src/client/routes/survey.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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 { LayoutV2 } from '@/pages/LayoutV2';
|
||||||
|
import { useCurrentWorkspaceId } from '@/store/user';
|
||||||
|
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||||
|
import { Trans, useTranslation } from '@i18next-toolkit/react';
|
||||||
|
import {
|
||||||
|
createFileRoute,
|
||||||
|
useNavigate,
|
||||||
|
useRouterState,
|
||||||
|
} from '@tanstack/react-router';
|
||||||
|
import { LuPlus } from 'react-icons/lu';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/survey')({
|
||||||
|
beforeLoad: routeAuthBeforeLoad,
|
||||||
|
component: PageComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function PageComponent() {
|
||||||
|
const workspaceId = useCurrentWorkspaceId();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { data = [] } = trpc.survey.all.useQuery({
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
const { data: allResultCount = {} } = trpc.survey.allResultCount.useQuery({
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const pathname = useRouterState({
|
||||||
|
select: (state) => state.location.pathname,
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = data.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
title: item.name,
|
||||||
|
number: allResultCount[item.id] ?? 0,
|
||||||
|
href: `/survey/${item.id}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
useDataReady(
|
||||||
|
() => data.length > 0,
|
||||||
|
() => {
|
||||||
|
if (pathname === Route.fullPath) {
|
||||||
|
navigate({
|
||||||
|
to: '/survey/$surveyId',
|
||||||
|
params: {
|
||||||
|
surveyId: data[0].id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickAdd = useEvent(() => {
|
||||||
|
navigate({
|
||||||
|
to: '/survey/add',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayoutV2
|
||||||
|
list={
|
||||||
|
<CommonWrapper
|
||||||
|
header={
|
||||||
|
<CommonHeader
|
||||||
|
title={t('Survey')}
|
||||||
|
actions={
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
Icon={LuPlus}
|
||||||
|
onClick={handleClickAdd}
|
||||||
|
>
|
||||||
|
{t('Add')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CommonList hasSearch={true} items={items} />
|
||||||
|
</CommonWrapper>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
54
src/client/routes/survey/$surveyId.tsx
Normal file
54
src/client/routes/survey/$surveyId.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc';
|
||||||
|
import { CommonHeader } from '@/components/CommonHeader';
|
||||||
|
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||||
|
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
|
||||||
|
import { useGlobalRangeDate } from '@/hooks/useGlobalRangeDate';
|
||||||
|
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';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/survey/$surveyId')({
|
||||||
|
beforeLoad: routeAuthBeforeLoad,
|
||||||
|
component: PageComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function PageComponent() {
|
||||||
|
const { surveyId } = Route.useParams<{ surveyId: string }>();
|
||||||
|
const workspaceId = useCurrentWorkspaceId();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { data: info } = trpc.survey.get.useQuery({
|
||||||
|
workspaceId,
|
||||||
|
surveyId,
|
||||||
|
});
|
||||||
|
const { data: count } = trpc.survey.count.useQuery({
|
||||||
|
workspaceId,
|
||||||
|
surveyId,
|
||||||
|
});
|
||||||
|
const deleteMutation = trpc.survey.delete.useMutation({
|
||||||
|
onSuccess: defaultSuccessHandler,
|
||||||
|
onError: defaultErrorHandler,
|
||||||
|
});
|
||||||
|
const trpcUtils = trpc.useUtils();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleDelete = useEvent(async () => {
|
||||||
|
await deleteMutation.mutateAsync({ workspaceId, surveyId });
|
||||||
|
trpcUtils.survey.all.refetch();
|
||||||
|
navigate({
|
||||||
|
to: '/survey',
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommonWrapper header={<CommonHeader title={info?.name ?? ''} />}>
|
||||||
|
<ScrollArea className="h-full overflow-hidden p-4">
|
||||||
|
<ScrollBar orientation="horizontal" />
|
||||||
|
|
||||||
|
{/* */}
|
||||||
|
</ScrollArea>
|
||||||
|
</CommonWrapper>
|
||||||
|
);
|
||||||
|
}
|
53
src/client/routes/survey/add.tsx
Normal file
53
src/client/routes/survey/add.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
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 {
|
||||||
|
SurveyEditForm,
|
||||||
|
SurveyEditFormValues,
|
||||||
|
} from '@/components/survey/SurveyEditForm';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/survey/add')({
|
||||||
|
beforeLoad: routeAuthBeforeLoad,
|
||||||
|
component: PageComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function PageComponent() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const workspaceId = useCurrentWorkspaceId();
|
||||||
|
const createMutation = trpc.survey.create.useMutation({
|
||||||
|
onError: defaultErrorHandler,
|
||||||
|
});
|
||||||
|
const utils = trpc.useUtils();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onSubmit = useEvent(async (values: SurveyEditFormValues) => {
|
||||||
|
const res = await createMutation.mutateAsync({
|
||||||
|
workspaceId,
|
||||||
|
name: values.name,
|
||||||
|
payload: values.payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
utils.survey.all.refetch();
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: '/survey/$surveyId',
|
||||||
|
params: {
|
||||||
|
surveyId: res.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommonWrapper
|
||||||
|
header={<h1 className="text-xl font-bold">{t('Add Survey')}</h1>}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
<SurveyEditForm onSubmit={onSubmit} />
|
||||||
|
</div>
|
||||||
|
</CommonWrapper>
|
||||||
|
);
|
||||||
|
}
|
@ -65,7 +65,7 @@ function TelemetryAddComponent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonWrapper
|
<CommonWrapper
|
||||||
header={<h1 className="text-xl font-bold">{t('Add Website')}</h1>}
|
header={<h1 className="text-xl font-bold">{t('Add Telemetry')}</h1>}
|
||||||
>
|
>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
|
@ -38,3 +38,14 @@ export function formatShortTime(val: number, formats = ['m', 's'], space = '') {
|
|||||||
|
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateRandomString(length: number): string {
|
||||||
|
const characters =
|
||||||
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
let result = '';
|
||||||
|
const charactersLength = characters.length;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ import { serverStatusRouter } from './serverStatus';
|
|||||||
import { auditLogRouter } from './auditLog';
|
import { auditLogRouter } from './auditLog';
|
||||||
import { billingRouter } from './billing';
|
import { billingRouter } from './billing';
|
||||||
import { telemetryRouter } from './telemetry';
|
import { telemetryRouter } from './telemetry';
|
||||||
|
import { surveyRouter } from './survey';
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
global: globalRouter,
|
global: globalRouter,
|
||||||
@ -18,6 +19,7 @@ export const appRouter = router({
|
|||||||
notification: notificationRouter,
|
notification: notificationRouter,
|
||||||
monitor: monitorRouter,
|
monitor: monitorRouter,
|
||||||
telemetry: telemetryRouter,
|
telemetry: telemetryRouter,
|
||||||
|
survey: surveyRouter,
|
||||||
serverStatus: serverStatusRouter,
|
serverStatus: serverStatusRouter,
|
||||||
auditLog: auditLogRouter,
|
auditLog: auditLogRouter,
|
||||||
billing: billingRouter,
|
billing: billingRouter,
|
||||||
|
@ -16,7 +16,7 @@ import { SurveyPayloadSchema } from '../../prisma/zod/schemas';
|
|||||||
import { buildCursorResponseSchema } from '../../utils/schema';
|
import { buildCursorResponseSchema } from '../../utils/schema';
|
||||||
import { fetchDataByCursor } from '../../utils/prisma';
|
import { fetchDataByCursor } from '../../utils/prisma';
|
||||||
|
|
||||||
export const telemetryRouter = router({
|
export const surveyRouter = router({
|
||||||
all: workspaceProcedure
|
all: workspaceProcedure
|
||||||
.meta(
|
.meta(
|
||||||
buildSurveyOpenapi({
|
buildSurveyOpenapi({
|
||||||
@ -89,6 +89,28 @@ export const telemetryRouter = router({
|
|||||||
|
|
||||||
return count;
|
return count;
|
||||||
}),
|
}),
|
||||||
|
allResultCount: workspaceProcedure
|
||||||
|
.meta(
|
||||||
|
buildSurveyOpenapi({
|
||||||
|
method: 'GET',
|
||||||
|
path: '/allResultCount',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(z.record(z.string(), z.number()))
|
||||||
|
.query(async () => {
|
||||||
|
const res = await prisma.surveyResult.groupBy({
|
||||||
|
by: ['surveyId'],
|
||||||
|
_count: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.reduce<Record<string, number>>((prev, item) => {
|
||||||
|
if (item.surveyId) {
|
||||||
|
prev[item.surveyId] = item._count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev;
|
||||||
|
}, {});
|
||||||
|
}),
|
||||||
submit: publicProcedure
|
submit: publicProcedure
|
||||||
.meta(
|
.meta(
|
||||||
buildSurveyOpenapi({
|
buildSurveyOpenapi({
|
||||||
|
@ -50,12 +50,13 @@ export const workspaceRouter = router({
|
|||||||
server: z.number(),
|
server: z.number(),
|
||||||
telemetry: z.number(),
|
telemetry: z.number(),
|
||||||
page: z.number(),
|
page: z.number(),
|
||||||
|
survey: z.number(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const { workspaceId } = input;
|
const { workspaceId } = input;
|
||||||
|
|
||||||
const [website, monitor, telemetry, page] = await Promise.all([
|
const [website, monitor, telemetry, page, survey] = await Promise.all([
|
||||||
prisma.website.count({
|
prisma.website.count({
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -76,6 +77,11 @@ export const workspaceRouter = router({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
prisma.survey.count({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const server = getServerCount(workspaceId);
|
const server = getServerCount(workspaceId);
|
||||||
@ -86,6 +92,7 @@ export const workspaceRouter = router({
|
|||||||
server,
|
server,
|
||||||
telemetry,
|
telemetry,
|
||||||
page,
|
page,
|
||||||
|
survey,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
updateDashboardOrder: workspaceOwnerProcedure
|
updateDashboardOrder: workspaceOwnerProcedure
|
||||||
|
Loading…
Reference in New Issue
Block a user