🪥 Consult submissions
This commit is contained in:
@ -200,3 +200,11 @@ export const UploadIcon = (props: IconProps) => (
|
||||
<polyline points="16 16 12 12 8 16"></polyline>
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const DownloadIcon = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</Icon>
|
||||
)
|
||||
|
@ -0,0 +1,44 @@
|
||||
import { Checkbox, Flex, Skeleton } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
|
||||
type LoadingRowsProps = {
|
||||
totalColumns: number
|
||||
}
|
||||
|
||||
export const LoadingRows = ({ totalColumns }: LoadingRowsProps) => {
|
||||
return (
|
||||
<>
|
||||
{Array.from(Array(3)).map((row, idx) => (
|
||||
<Flex as="tr" key={idx}>
|
||||
<Flex
|
||||
key={idx}
|
||||
py={2}
|
||||
px={4}
|
||||
border="1px"
|
||||
as="td"
|
||||
borderColor="gray.200"
|
||||
flex="0"
|
||||
>
|
||||
<Checkbox isDisabled />
|
||||
</Flex>
|
||||
{Array.from(Array(totalColumns)).map((cell, idx) => {
|
||||
return (
|
||||
<Flex
|
||||
key={idx}
|
||||
py={2}
|
||||
px={4}
|
||||
border="1px"
|
||||
as="td"
|
||||
borderColor="gray.200"
|
||||
flex="1"
|
||||
align="center"
|
||||
>
|
||||
<Skeleton height="5px" w="full" />
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,73 +1,110 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable react/jsx-key */
|
||||
import { Box, Flex } from '@chakra-ui/react'
|
||||
import { Box, Checkbox, Flex } from '@chakra-ui/react'
|
||||
import { Answer, Result } from 'bot-engine'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import React from 'react'
|
||||
import { useTable } from 'react-table'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Hooks, useRowSelect, useTable } from 'react-table'
|
||||
import { parseSubmissionsColumns } from 'services/publicTypebot'
|
||||
import { parseDateToReadable } from 'services/results'
|
||||
import { LoadingRows } from './LoadingRows'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
type SubmissionsTableProps = {}
|
||||
type SubmissionsTableProps = {
|
||||
results?: (Result & { answers: Answer[] })[]
|
||||
onNewSelection: (selection: string[]) => void
|
||||
}
|
||||
|
||||
export const SubmissionsTable = ({}: SubmissionsTableProps) => {
|
||||
export const SubmissionsTable = ({
|
||||
results,
|
||||
onNewSelection,
|
||||
}: SubmissionsTableProps) => {
|
||||
const { publishedTypebot } = useTypebot()
|
||||
const columns: any = React.useMemo(
|
||||
() => parseSubmissionsColumns(publishedTypebot),
|
||||
[publishedTypebot]
|
||||
)
|
||||
const data = React.useMemo(() => [], [])
|
||||
const { getTableProps, headerGroups, rows, prepareRow, getTableBodyProps } =
|
||||
useTable({ columns, data })
|
||||
const data = React.useMemo(
|
||||
() =>
|
||||
(results ?? []).map((result) => ({
|
||||
createdAt: parseDateToReadable(result.createdAt),
|
||||
...result.answers.reduce(
|
||||
(o, answer) => ({ ...o, [answer.blockId]: answer.content }),
|
||||
{}
|
||||
),
|
||||
})),
|
||||
[results]
|
||||
)
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
getTableBodyProps,
|
||||
selectedFlatRows,
|
||||
} = useTable({ columns, data }, useRowSelect, checkboxColumnHook) as any
|
||||
|
||||
useEffect(() => {
|
||||
onNewSelection(selectedFlatRows.map((row: any) => row.id))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedFlatRows])
|
||||
|
||||
return (
|
||||
<Flex overflowX="scroll" maxW="full" className="table-wrapper" rounded="md">
|
||||
<Box as="table" rounded="md" {...getTableProps()}>
|
||||
<Box as="table" rounded="md" {...getTableProps()} w="full">
|
||||
<Box as="thead">
|
||||
{headerGroups.map((headerGroup) => {
|
||||
{headerGroups.map((headerGroup: any) => {
|
||||
return (
|
||||
<Box as="tr" {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map((column) => {
|
||||
<Flex as="tr" {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map((column: any, idx: number) => {
|
||||
return (
|
||||
<Box
|
||||
<Flex
|
||||
py={2}
|
||||
px={4}
|
||||
border="1px"
|
||||
borderColor="gray.200"
|
||||
as="th"
|
||||
minW="200px"
|
||||
color="gray.500"
|
||||
fontWeight="normal"
|
||||
textAlign="left"
|
||||
minW={idx > 0 ? '200px' : 'unset'}
|
||||
flex={idx > 0 ? '1' : '0'}
|
||||
{...column.getHeaderProps()}
|
||||
>
|
||||
{column.render('Header')}
|
||||
</Box>
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
|
||||
<Box as="tbody" {...getTableBodyProps()}>
|
||||
{rows.map((row) => {
|
||||
{results === undefined && (
|
||||
<LoadingRows totalColumns={columns.length} />
|
||||
)}
|
||||
{rows.map((row: any) => {
|
||||
prepareRow(row)
|
||||
return (
|
||||
<Box as="tr" {...row.getRowProps()}>
|
||||
{row.cells.map((cell) => {
|
||||
<Flex as="tr" {...row.getRowProps()}>
|
||||
{row.cells.map((cell: any, idx: number) => {
|
||||
return (
|
||||
<Box
|
||||
<Flex
|
||||
py={2}
|
||||
px={4}
|
||||
border="1px"
|
||||
as="td"
|
||||
minW="200px"
|
||||
borderColor="gray.200"
|
||||
minW={idx > 0 ? '200px' : 'unset'}
|
||||
{...cell.getCellProps()}
|
||||
flex={idx > 0 ? '1' : '0'}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
</Box>
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
@ -75,3 +112,34 @@ export const SubmissionsTable = ({}: SubmissionsTableProps) => {
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
const checkboxColumnHook = (hooks: Hooks<any>) => {
|
||||
hooks.visibleColumns.push((columns) => [
|
||||
{
|
||||
id: 'selection',
|
||||
Header: ({ getToggleAllRowsSelectedProps }: any) => (
|
||||
<IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} />
|
||||
),
|
||||
Cell: ({ row }: any) => (
|
||||
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
|
||||
),
|
||||
},
|
||||
...columns,
|
||||
])
|
||||
}
|
||||
|
||||
const IndeterminateCheckbox = React.forwardRef(
|
||||
({ indeterminate, checked, ...rest }: any, ref) => {
|
||||
const defaultRef = React.useRef()
|
||||
const resolvedRef: any = ref || defaultRef
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
ref={resolvedRef}
|
||||
{...rest}
|
||||
isIndeterminate={indeterminate}
|
||||
isChecked={checked}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -1,8 +1,17 @@
|
||||
import { Button, Flex, HStack, Stack } from '@chakra-ui/react'
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
HStack,
|
||||
Stack,
|
||||
Tag,
|
||||
useToast,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useResultsCount } from 'services/results'
|
||||
import { AnalyticsContent } from './AnalyticsContent'
|
||||
import { SubmissionsContent } from './SubmissionContent'
|
||||
|
||||
@ -13,6 +22,15 @@ export const ResultsContent = () => {
|
||||
() => router.pathname.endsWith('analytics'),
|
||||
[router.pathname]
|
||||
)
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
})
|
||||
|
||||
const { totalResults } = useResultsCount({
|
||||
typebotId: typebot?.id,
|
||||
onError: (err) => toast({ title: err.name, description: err.message }),
|
||||
})
|
||||
return (
|
||||
<Flex h="full" w="full" justifyContent="center" align="flex-start">
|
||||
<Stack maxW="1200px" w="full" pt="4" spacing={6}>
|
||||
@ -24,7 +42,12 @@ export const ResultsContent = () => {
|
||||
size="sm"
|
||||
href={`/typebots/${typebot?.id}/results`}
|
||||
>
|
||||
Submissions
|
||||
<Text>Submissions</Text>
|
||||
{totalResults && (
|
||||
<Tag size="sm" colorScheme="blue" ml="1">
|
||||
{totalResults}
|
||||
</Tag>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
as={NextChakraLink}
|
||||
@ -36,7 +59,15 @@ export const ResultsContent = () => {
|
||||
Analytics
|
||||
</Button>
|
||||
</HStack>
|
||||
{isAnalytics ? <AnalyticsContent /> : <SubmissionsContent />}
|
||||
{typebot &&
|
||||
(isAnalytics ? (
|
||||
<AnalyticsContent />
|
||||
) : (
|
||||
<SubmissionsContent
|
||||
typebotId={typebot.id}
|
||||
totalResults={totalResults ?? 0}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Flex>
|
||||
)
|
||||
|
@ -1,10 +1,75 @@
|
||||
import {
|
||||
Button,
|
||||
HStack,
|
||||
Stack,
|
||||
Tag,
|
||||
useToast,
|
||||
Text,
|
||||
Fade,
|
||||
Flex,
|
||||
} from '@chakra-ui/react'
|
||||
import { DownloadIcon, TrashIcon } from 'assets/icons'
|
||||
import { SubmissionsTable } from 'components/results/SubmissionsTable'
|
||||
import React from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useResults } from 'services/results'
|
||||
|
||||
type Props = { typebotId: string; totalResults: number }
|
||||
export const SubmissionsContent = ({ typebotId, totalResults }: Props) => {
|
||||
const [lastResultId, setLastResultId] = useState<string>()
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
})
|
||||
|
||||
const { results } = useResults({
|
||||
lastResultId,
|
||||
typebotId,
|
||||
onError: (err) => toast({ title: err.name, description: err.message }),
|
||||
})
|
||||
|
||||
const handleNewSelection = (newSelection: string[]) => {
|
||||
if (newSelection.length === selectedIds.length) return
|
||||
setSelectedIds(newSelection)
|
||||
}
|
||||
|
||||
const totalSelected = useMemo(
|
||||
() =>
|
||||
selectedIds.length === results?.length
|
||||
? totalResults
|
||||
: selectedIds.length,
|
||||
[results?.length, selectedIds.length, totalResults]
|
||||
)
|
||||
|
||||
export const SubmissionsContent = () => {
|
||||
return (
|
||||
<>
|
||||
<SubmissionsTable />
|
||||
</>
|
||||
<Stack>
|
||||
<Flex w="full" justifyContent="flex-end">
|
||||
<HStack>
|
||||
<HStack as={Button} colorScheme="blue">
|
||||
<DownloadIcon />
|
||||
<Text>Export</Text>
|
||||
<Fade in={totalSelected > 0} unmountOnExit>
|
||||
<Tag colorScheme="blue" variant="subtle" size="sm">
|
||||
{totalSelected}
|
||||
</Tag>
|
||||
</Fade>
|
||||
</HStack>
|
||||
<Fade in={totalSelected > 0} unmountOnExit>
|
||||
<HStack as={Button} colorScheme="red">
|
||||
<TrashIcon />
|
||||
<Text>Delete</Text>
|
||||
{totalSelected > 0 && (
|
||||
<Tag colorScheme="red" variant="subtle" size="sm">
|
||||
{totalSelected}
|
||||
</Tag>
|
||||
)}
|
||||
</HStack>
|
||||
</Fade>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
<SubmissionsTable results={results} onNewSelection={handleNewSelection} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -38,6 +38,7 @@
|
||||
"next-auth": "beta",
|
||||
"nodemailer": "^6.7.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"qs": "^6.10.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-frame-component": "^5.2.1",
|
||||
@ -59,6 +60,7 @@
|
||||
"@types/micro-cors": "^0.1.2",
|
||||
"@types/node": "^16.11.9",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/react": "^17.0.37",
|
||||
"@types/react-table": "^7.7.9",
|
||||
"@types/testing-library__cypress": "^5.0.9",
|
||||
|
@ -9,10 +9,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (!session?.user)
|
||||
return res.status(401).json({ message: 'Not authenticated' })
|
||||
|
||||
const id = req.query.id.toString()
|
||||
const typebotId = req.query.typebotId.toString()
|
||||
if (req.method === 'GET') {
|
||||
const typebot = await prisma.typebot.findUnique({
|
||||
where: { id },
|
||||
where: { id: typebotId },
|
||||
include: {
|
||||
publishedTypebot: true,
|
||||
},
|
||||
@ -23,14 +23,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
}
|
||||
if (req.method === 'DELETE') {
|
||||
const typebots = await prisma.typebot.delete({
|
||||
where: { id },
|
||||
where: { id: typebotId },
|
||||
})
|
||||
return res.send({ typebots })
|
||||
}
|
||||
if (req.method === 'PUT') {
|
||||
const data = JSON.parse(req.body)
|
||||
const typebots = await prisma.typebot.update({
|
||||
where: { id },
|
||||
where: { id: typebotId },
|
||||
data,
|
||||
})
|
||||
return res.send({ typebots })
|
||||
@ -38,7 +38,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'PATCH') {
|
||||
const data = JSON.parse(req.body)
|
||||
const typebots = await prisma.typebot.update({
|
||||
where: { id },
|
||||
where: { id: typebotId },
|
||||
data,
|
||||
})
|
||||
return res.send({ typebots })
|
39
apps/builder/pages/api/typebots/[typebotId]/results.ts
Normal file
39
apps/builder/pages/api/typebots/[typebotId]/results.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { User } from 'db'
|
||||
import prisma from 'libs/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { getSession } from 'next-auth/react'
|
||||
import { methodNotAllowed } from 'utils'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (!session?.user)
|
||||
return res.status(401).send({ message: 'Not authenticated' })
|
||||
|
||||
const user = session.user as User
|
||||
if (req.method === 'GET') {
|
||||
const typebotId = req.query.typebotId.toString()
|
||||
const lastResultId = req.query.lastResultId?.toString()
|
||||
const results = await prisma.result.findMany({
|
||||
take: 50,
|
||||
skip: lastResultId ? 1 : 0,
|
||||
cursor: lastResultId
|
||||
? {
|
||||
id: lastResultId,
|
||||
}
|
||||
: undefined,
|
||||
where: {
|
||||
typebotId,
|
||||
typebot: { ownerId: user.id },
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: { answers: true },
|
||||
})
|
||||
return res.status(200).send({ results })
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default handler
|
27
apps/builder/pages/api/typebots/[typebotId]/results/count.ts
Normal file
27
apps/builder/pages/api/typebots/[typebotId]/results/count.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { User } from 'db'
|
||||
import prisma from 'libs/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { getSession } from 'next-auth/react'
|
||||
import { methodNotAllowed } from 'utils'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (!session?.user)
|
||||
return res.status(401).send({ message: 'Not authenticated' })
|
||||
|
||||
const user = session.user as User
|
||||
if (req.method === 'GET') {
|
||||
const typebotId = req.query.typebotId.toString()
|
||||
const totalResults = await prisma.result.count({
|
||||
where: {
|
||||
typebotId,
|
||||
typebot: { ownerId: user.id },
|
||||
},
|
||||
})
|
||||
return res.status(200).send({ totalResults })
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default handler
|
@ -1,6 +1,7 @@
|
||||
import { DashboardFolder } from '.prisma/client'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher, sendRequest } from './utils'
|
||||
import { stringify } from 'qs'
|
||||
|
||||
export const useFolders = ({
|
||||
parentId,
|
||||
@ -9,9 +10,7 @@ export const useFolders = ({
|
||||
parentId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const params = new URLSearchParams(
|
||||
parentId ? { parentId: parentId.toString() } : undefined
|
||||
)
|
||||
const params = stringify({ parentId })
|
||||
const { data, error, mutate } = useSWR<{ folders: DashboardFolder[] }, Error>(
|
||||
`/api/folders?${params}`,
|
||||
fetcher
|
||||
|
@ -8,6 +8,9 @@ import {
|
||||
} from 'bot-engine'
|
||||
import { sendRequest } from './utils'
|
||||
import shortId from 'short-uuid'
|
||||
import { HStack, Text } from '@chakra-ui/react'
|
||||
import { CalendarIcon } from 'assets/icons'
|
||||
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
||||
|
||||
export const parseTypebotToPublicTypebot = (
|
||||
typebot: Typebot
|
||||
@ -44,12 +47,32 @@ export const updatePublishedTypebot = async (
|
||||
export const parseSubmissionsColumns = (
|
||||
typebot?: PublicTypebot
|
||||
): {
|
||||
Header: string
|
||||
Header: JSX.Element
|
||||
accessor: string
|
||||
}[] =>
|
||||
(typebot?.blocks ?? [])
|
||||
.filter(blockContainsInput)
|
||||
.map((block) => ({ Header: block.title, accessor: block.id }))
|
||||
}[] => [
|
||||
{
|
||||
Header: (
|
||||
<HStack>
|
||||
<CalendarIcon />
|
||||
<Text>Submitted at</Text>
|
||||
</HStack>
|
||||
),
|
||||
accessor: 'createdAt',
|
||||
},
|
||||
...(typebot?.blocks ?? []).filter(blockContainsInput).map((block) => ({
|
||||
Header: (
|
||||
<HStack>
|
||||
<StepIcon
|
||||
type={
|
||||
block.steps.find((step) => step.target)?.type ?? StepType.TEXT_INPUT
|
||||
}
|
||||
/>
|
||||
<Text>{block.title}</Text>
|
||||
</HStack>
|
||||
),
|
||||
accessor: block.id,
|
||||
})),
|
||||
]
|
||||
|
||||
const blockContainsInput = (block: Block) => block.steps.some(stepIsInput)
|
||||
|
60
apps/builder/services/results.ts
Normal file
60
apps/builder/services/results.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Result } from 'bot-engine'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher } from './utils'
|
||||
import { stringify } from 'qs'
|
||||
import { Answer } from 'db'
|
||||
|
||||
export const useResults = ({
|
||||
lastResultId,
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
lastResultId?: string
|
||||
typebotId: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const params = stringify({
|
||||
lastResultId,
|
||||
})
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ results: (Result & { answers: Answer[] })[] },
|
||||
Error
|
||||
>(`/api/typebots/${typebotId}/results?${params}`, fetcher)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
results: data?.results,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const useResultsCount = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<{ totalResults: number }, Error>(
|
||||
typebotId ? `/api/typebots/${typebotId}/results/count` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
totalResults: data?.totalResults,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const parseDateToReadable = (dateStr: string): string => {
|
||||
const date = new Date(dateStr)
|
||||
return (
|
||||
date.toDateString().split(' ').slice(1, 3).join(' ') +
|
||||
', ' +
|
||||
date.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
)
|
||||
}
|
@ -11,6 +11,7 @@ import { Typebot } from 'bot-engine'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher, sendRequest, toKebabCase } from './utils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { stringify } from 'qs'
|
||||
|
||||
export const useTypebots = ({
|
||||
folderId,
|
||||
@ -19,9 +20,7 @@ export const useTypebots = ({
|
||||
folderId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const params = new URLSearchParams(
|
||||
folderId ? { folderId: folderId.toString() } : undefined
|
||||
)
|
||||
const params = stringify({ folderId })
|
||||
const { data, error, mutate } = useSWR<{ typebots: Typebot[] }, Error>(
|
||||
`/api/typebots?${params}`,
|
||||
fetcher
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Answer, PublicTypebot, TypebotViewer } from 'bot-engine'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { upsertAnswer } from 'services/answer'
|
||||
import { SEO } from '../components/Seo'
|
||||
import { createResult, updateResult } from '../services/result'
|
||||
import { ErrorPage } from './ErrorPage'
|
||||
@ -38,9 +39,9 @@ export const TypebotPage = ({ typebot, isIE, url }: TypebotPageProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAnswersUpdate = async (answers: Answer[]) => {
|
||||
const handleNewAnswer = async (answer: Answer) => {
|
||||
if (!resultId) return setError(new Error('Result was not created'))
|
||||
const { error } = await updateResult(resultId, { answers })
|
||||
const { error } = await upsertAnswer({ ...answer, resultId })
|
||||
if (error) setError(error)
|
||||
}
|
||||
|
||||
@ -62,7 +63,7 @@ export const TypebotPage = ({ typebot, isIE, url }: TypebotPageProps) => {
|
||||
{resultId && (
|
||||
<TypebotViewer
|
||||
typebot={typebot}
|
||||
onAnswersUpdate={handleAnswersUpdate}
|
||||
onNewAnswer={handleNewAnswer}
|
||||
onCompleted={handleCompleted}
|
||||
/>
|
||||
)}
|
||||
|
25
apps/viewer/pages/api/answers.ts
Normal file
25
apps/viewer/pages/api/answers.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Answer } from 'db'
|
||||
import prisma from 'libs/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed } from 'utils'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'PUT') {
|
||||
const answer = JSON.parse(req.body) as Answer
|
||||
const result = await prisma.answer.upsert({
|
||||
where: {
|
||||
resultId_blockId_stepId: {
|
||||
resultId: answer.resultId,
|
||||
blockId: answer.blockId,
|
||||
stepId: answer.stepId,
|
||||
},
|
||||
},
|
||||
create: answer,
|
||||
update: answer,
|
||||
})
|
||||
return res.send(result)
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default handler
|
10
apps/viewer/services/answer.ts
Normal file
10
apps/viewer/services/answer.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Answer } from 'db'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const upsertAnswer = async (answer: Answer) => {
|
||||
return sendRequest<Answer>({
|
||||
url: `/api/answers`,
|
||||
method: 'PUT',
|
||||
body: answer,
|
||||
})
|
||||
}
|
@ -16,7 +16,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv-cli": "^4.1.1",
|
||||
"turbo": "^1.0.23"
|
||||
"turbo": "^1.0.24-canary.2"
|
||||
},
|
||||
"turbo": {
|
||||
"baseBranch": "origin/main",
|
||||
|
@ -11,18 +11,18 @@ import { deepEqual } from 'fast-equals'
|
||||
type Props = {
|
||||
typebot: PublicTypebot
|
||||
onNewBlockVisible: (blockId: string) => void
|
||||
onAnswersUpdate: (answers: Answer[]) => void
|
||||
onNewAnswer: (answer: Answer) => void
|
||||
onCompleted: () => void
|
||||
}
|
||||
export const ConversationContainer = ({
|
||||
typebot,
|
||||
onNewBlockVisible,
|
||||
onAnswersUpdate,
|
||||
onNewAnswer,
|
||||
onCompleted,
|
||||
}: Props) => {
|
||||
const { document: frameDocument } = useFrame()
|
||||
const [displayedBlocks, setDisplayedBlocks] = useState<Block[]>([])
|
||||
const [localAnswers, setLocalAnswers] = useState<Answer[]>([])
|
||||
const [localAnswer, setLocalAnswer] = useState<Answer | undefined>()
|
||||
const { answers } = useAnswers()
|
||||
const bottomAnchor = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
@ -44,9 +44,10 @@ export const ConversationContainer = ({
|
||||
}, [typebot.theme, frameDocument])
|
||||
|
||||
useEffect(() => {
|
||||
if (deepEqual(localAnswers, answers)) return
|
||||
setLocalAnswers(answers)
|
||||
onAnswersUpdate(answers)
|
||||
const answer = [...answers].pop()
|
||||
if (!answer || deepEqual(localAnswer, answer)) return
|
||||
setLocalAnswer(answer)
|
||||
onNewAnswer(answer)
|
||||
}, [answers])
|
||||
|
||||
return (
|
||||
|
@ -10,13 +10,13 @@ import { AnswersContext } from '../contexts/AnswersContext'
|
||||
export type TypebotViewerProps = {
|
||||
typebot: PublicTypebot
|
||||
onNewBlockVisible?: (blockId: string) => void
|
||||
onAnswersUpdate?: (answers: Answer[]) => void
|
||||
onNewAnswer?: (answer: Answer) => void
|
||||
onCompleted?: () => void
|
||||
}
|
||||
export const TypebotViewer = ({
|
||||
typebot,
|
||||
onNewBlockVisible,
|
||||
onAnswersUpdate,
|
||||
onNewAnswer,
|
||||
onCompleted,
|
||||
}: TypebotViewerProps) => {
|
||||
const containerBgColor = useMemo(
|
||||
@ -29,8 +29,8 @@ export const TypebotViewer = ({
|
||||
const handleNewBlockVisible = (blockId: string) => {
|
||||
if (onNewBlockVisible) onNewBlockVisible(blockId)
|
||||
}
|
||||
const handleAnswersUpdate = (answers: Answer[]) => {
|
||||
if (onAnswersUpdate) onAnswersUpdate(answers)
|
||||
const handleNewAnswer = (answer: Answer) => {
|
||||
if (onNewAnswer) onNewAnswer(answer)
|
||||
}
|
||||
const handleCompleted = () => {
|
||||
if (onCompleted) onCompleted()
|
||||
@ -60,7 +60,7 @@ export const TypebotViewer = ({
|
||||
<ConversationContainer
|
||||
typebot={typebot}
|
||||
onNewBlockVisible={handleNewBlockVisible}
|
||||
onAnswersUpdate={handleAnswersUpdate}
|
||||
onNewAnswer={handleNewAnswer}
|
||||
onCompleted={handleCompleted}
|
||||
/>
|
||||
</div>
|
||||
|
3
packages/bot-engine/src/models/answer.ts
Normal file
3
packages/bot-engine/src/models/answer.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { Answer as AnswerFromPrisma } from 'db'
|
||||
|
||||
export type Answer = Omit<AnswerFromPrisma, 'resultId'>
|
@ -1,3 +1,4 @@
|
||||
export * from './typebot'
|
||||
export * from './publicTypebot'
|
||||
export * from './result'
|
||||
export * from './answer'
|
||||
|
@ -1,11 +1,3 @@
|
||||
import { Result as ResultFromPrisma } from 'db'
|
||||
|
||||
export type Result = Omit<ResultFromPrisma, 'answers'> & {
|
||||
answers: Answer[]
|
||||
}
|
||||
|
||||
export type Answer = {
|
||||
blockId: string
|
||||
stepId: string
|
||||
content: string
|
||||
}
|
||||
export type Result = Omit<ResultFromPrisma, 'createdAt'> & { createdAt: string }
|
||||
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `answers` on the `Result` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Result" DROP COLUMN "answers";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Answer" (
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"resultId" TEXT NOT NULL,
|
||||
"stepId" TEXT NOT NULL,
|
||||
"blockId" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Answer_resultId_blockId_stepId_key" ON "Answer"("resultId", "blockId", "stepId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Answer" ADD CONSTRAINT "Answer_resultId_fkey" FOREIGN KEY ("resultId") REFERENCES "Result"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -115,6 +115,17 @@ model Result {
|
||||
updatedAt DateTime @default(now())
|
||||
typebotId String
|
||||
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
||||
answers Json[]
|
||||
answers Answer[]
|
||||
isCompleted Boolean?
|
||||
}
|
||||
|
||||
model Answer {
|
||||
createdAt DateTime @default(now())
|
||||
resultId String
|
||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||
stepId String
|
||||
blockId String
|
||||
content String
|
||||
|
||||
@@unique([resultId, blockId, stepId])
|
||||
}
|
||||
|
135
yarn.lock
135
yarn.lock
@ -1482,6 +1482,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
|
||||
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
|
||||
|
||||
"@types/qs@^6.9.7":
|
||||
version "6.9.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
||||
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
|
||||
|
||||
"@types/react-scroll@^1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-scroll/-/react-scroll-1.8.3.tgz#80951ed1934ab49d4926aad95c26607b8b1a9713"
|
||||
@ -6046,7 +6051,7 @@ puppeteer@^2.1.1:
|
||||
rimraf "^2.6.1"
|
||||
ws "^6.1.0"
|
||||
|
||||
qs@^6.6.0:
|
||||
qs@^6.10.2, qs@^6.6.0:
|
||||
version "6.10.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.2.tgz#c1431bea37fc5b24c5bdbafa20f16bdf2a4b9ffe"
|
||||
integrity sha512-mSIdjzqznWgfd4pMii7sHtaYF8rx8861hBO80SraY5GT0XQibWZWJSid0avzHGkDIZLImux2S5mXO0Hfct2QCw==
|
||||
@ -7320,83 +7325,83 @@ tunnel-agent@^0.6.0:
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
turbo-darwin-64@1.0.23:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.0.23.tgz#0164b1a2ee6782bf2233bb1887b01ac78486378e"
|
||||
integrity sha512-z6ArzNKpQIw/YhFf20lUGl5VaGTZ94MSppXuuYkCyxMQwK3qye3r2ENQbUKkxfv16fAmfr7z1bJt6YwfOjwPiw==
|
||||
turbo-darwin-64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.0.24-canary.2.tgz#5d64c95d14fb06f45b2c0c44d1657b1884e009d7"
|
||||
integrity sha512-MTY6Eea5QQc+ZqC2EqZWtIRaIygenWY7i0f+PVyCyfaXp5TqZiIUljd4s1vfp4tygVg66EVkI2Pu34FsXNVB3g==
|
||||
|
||||
turbo-darwin-arm64@1.0.23:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.0.23.tgz#7d6de986595d6ded9d853744c26780b50c7b196c"
|
||||
integrity sha512-U02Ip7kO7eDN3WRliIrPtqYFqcpsvpNsE5fX1T0uFjXfSJruiDBrrNpGmZEh8hatS5wQyG7VaxW+izJqd7TCsw==
|
||||
turbo-darwin-arm64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.0.24-canary.2.tgz#2169cebc1be5d8fbcf8334a50d411432c7480c10"
|
||||
integrity sha512-FaaAJ25smSeKjnkyUYxJhth0L59VmGjM9MrHXg+KhjU9jPFzyXTr9Bnb0nV6TAFRcL+IzZ3TXHykRCqfY4FjYA==
|
||||
|
||||
turbo-freebsd-64@1.0.23:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.0.23.tgz#f78d2056414496334cb8c6bc28ac7f3d5d0ab30c"
|
||||
integrity sha512-5XT8pp0uJ1vRqNJydacn6ouY4COUeufOdLuiPOWAOsvMW5FOJNAOdAJ7lISBBqhCpYF8CEZ9kCAI2f9nOzzY0Q==
|
||||
turbo-freebsd-64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-64/-/turbo-freebsd-64-1.0.24-canary.2.tgz#91528c2a75f621d7e7861e94b73c9a70443206ef"
|
||||
integrity sha512-alNrWyWvpIdKoQARB85PJchwr6VqKbWdayq2zlzGnSnTmH0EQb0bSDL9zNsdiKu4ISGE2E7YUN616lfvqx3wHA==
|
||||
|
||||
turbo-freebsd-arm64@1.0.23:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.0.23.tgz#d7d4eca48a21da17403c804e5bd52a3daffbdc1c"
|
||||
integrity sha512-70BGJTVX3jYvzDISp8dWM/rwiK4CDlNVQJnnlUMKrueFtpgHZhsRKjIeNgIKxVSTyp4xT0Vl4xhc9zFhMT17aw==
|
||||
turbo-freebsd-arm64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.0.24-canary.2.tgz#8cea1bf6335e284beea3136eb7cc811da8501c72"
|
||||
integrity sha512-vfS+L5NqRLN2w4R0QgmBFl9Z9pXnIYwoS3IWcfx2SRqTUdcqfrM3Mq3jHUdfQE2DufMYcGt1u28aakYb5LEpdA==
|
||||
|
||||
turbo-linux-32@1.0.23:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.0.23.tgz#c11de3c98944d43b9e25582c03ddd8f38d6cd85c"
|
||||
integrity sha512-ee3N8f7h1qg5noKII5Tq2gyG6PVFl/apllX0TPbZiUab6553QPfVt+tB+5W6BZmKGUdHHCvW42xuig0krrrRGw==
|
||||
turbo-linux-32@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-32/-/turbo-linux-32-1.0.24-canary.2.tgz#41d68a780b805111a05a2e4b692b3f10c944bb51"
|
||||
integrity sha512-kjhHH3IQv83MrxpW5ScWlC08XzoWgoI2IbrZsZeZYXVdzDgIk5Uf8PfiUe0fJXpdUL8bIFq3nQ+yX8U/FuChAw==
|
||||
|
||||
turbo-linux-64@1.0.23:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.0.23.tgz#7700a5e5ad96e741405957e7cd33a7422c49bfbd"
|
||||
integrity sha512-j2+1P2+rdOXb/yzz78SCmxXLdFSSlCzZS0Psg1vetYozuoT0k+xu7gr6iqEHdBWw39jNFuT72hVqrNZ4TClHcA==
|
||||
turbo-linux-64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.0.24-canary.2.tgz#18331dfc7b1d0f89a5f82cc17383571186779e85"
|
||||
integrity sha512-v8+u+gV01UsCl7psvptL+mr863lrVr3L1j0CFKXhjyZQJlEgXoRbkxU4VUWaXwf9ABaadsPsmv9B2oQnudypeA==
|
||||
|
||||
turbo-linux-arm64@1.0.23:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.0.23.tgz#79b49e14ef3038f99036134abcf53c17645b5eb9"
|
||||
integrity sha512-VNW8yyBG63MGUzZ/Y2DS883P8rzz/hQuiCklw2lTBQ4O9dp2FxYrAVnmCxbnsIDZG/wEupdvaHVgqH7j2DJgwg==
|
||||
turbo-linux-arm64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.0.24-canary.2.tgz#8873aa5b812f668934d16b69d59f5030b3d9fc8e"
|
||||
integrity sha512-v3/XDIQ9Cyz1dfPXnuJyH2V4xIDGPw9SaQS3FPiGH6TvUULkFYphg+cLttk7DPo3573GDxUQug3c5TvlIZFO8Q==
|
||||
|
||||
turbo-linux-arm@1.0.23:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.0.23.tgz#7140b73572948a0285395feb3abe8a9067f74d68"
|
||||
integrity sha512-+aD+v03bpkmuXEfk14w2fLH5cAyfyGlAs5gBwQmwyF3XllJ40OL/cw9xDnBREpZsnm9o1GTQpr0wmepjw9d4iQ==
|
||||
turbo-linux-arm@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-arm/-/turbo-linux-arm-1.0.24-canary.2.tgz#22126953b881f952a1b5f480a3053c514cf314aa"
|
||||
integrity sha512-pkfSuuOjN8DDJDOAdx0jqwTL4jeEsZbjmgNnhgrkb1HwVjQCjmqHbh15Wa2E51G12/Ctr6h2aKit2kbLXPfGWw==
|
||||
|
||||
turbo-linux-mips64le@1.0.23:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.0.23.tgz#fa4e989a8e149e3684f4732ed1b2a386fc829c8a"
|
||||
integrity sha512-CJNd7F9z8EhqcdJnd1rSiU/+hSNG3E+fudQ5pr55y0nhSShFMMdZxdLvL5zzhWpBl1g8YldphjPqNLChgGatKw==
|
||||
turbo-linux-mips64le@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-mips64le/-/turbo-linux-mips64le-1.0.24-canary.2.tgz#11254292c40ac8acef8452a379bb74bad39bf858"
|
||||
integrity sha512-35WJMoIxfIG8ruoMJVdOCxuQlJjNeLbxbqigDizJh2zkjlJITzEK+WwmF9KrqzMaN7Ks4LPq+xd/YvBEULx4Zw==
|
||||
|
||||
turbo-linux-ppc64le@1.0.23:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.0.23.tgz#3c1e0f5a79b8c34d790c7828ec443ed73ed95706"
|
||||
integrity sha512-RjzDqXQl+nqV+2A6MRk1sALBQtpCB/h8qDWhJIL/VUgToTMdllAThiYI+lALLmGWBumXs4XDbk+83FGJlJ1bDg==
|
||||
turbo-linux-ppc64le@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.0.24-canary.2.tgz#abc93ede90deca66f20c23fedc0f2b032e43a48a"
|
||||
integrity sha512-ZCipAVam+ZKyiQ5KYrL8pPgQ06JJ09H6mqTxEAP0OP2kN6Se+TuycFjAsedE0Gn/G+6MBmStjR99ha+I2fRnDA==
|
||||
|
||||
turbo-windows-32@1.0.23:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.0.23.tgz#17d58f43406cd68062131924f94c2df8ca159f9e"
|
||||
integrity sha512-qTfldTd5OO/s8unGFnITe4IX8ua2SNj/VVQbvVcObDJK6xrfBY4tDS78rIo9//xTSRyK+28AoPxRZsVqqF2hlA==
|
||||
turbo-windows-32@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-32/-/turbo-windows-32-1.0.24-canary.2.tgz#c24ae090c84c4ad83833deaeb0dba68186fe8e7d"
|
||||
integrity sha512-b9FBMoxhunGQNN/DwT9KK7g4FwDG3/cc4DEV5TdD809I66LmyVk1ThvpnUdZIRZRymcWq1c6kjT+0LfJFEBxGw==
|
||||
|
||||
turbo-windows-64@1.0.23:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.0.23.tgz#0e53fde0395fa88006be7fcd36c1853995cfa319"
|
||||
integrity sha512-yzhF6GEYiyrXoO/l9T6amB16OITsFXdeMTc0Vf2BZFOG1fn9Muc4CxQ+CcdsLlai+TqQTKlpEf0+l83vI+R2Jw==
|
||||
turbo-windows-64@1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.0.24-canary.2.tgz#f783fcb3661634cc3c1f578fdc79441ed3de5f8e"
|
||||
integrity sha512-HqXW3k+5h4nP8eCg5i/0NhUlR4+3vxTSyI3UvoACVz/IkiUIwVKF9ZhlAfFpkRCYU+1KKOMs7iDWlYiTyW4Inw==
|
||||
|
||||
turbo@^1.0.23:
|
||||
version "1.0.23"
|
||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.0.23.tgz#9a8f54f116daee67e0c131c4c936f075d32281a2"
|
||||
integrity sha512-/VHsARQyl8wsfCJtuJicjYq7Do/P5yjJde3bjG9TAZHj/PzA9LuRU7WOEPfUrT+felOQ3vwUMZtC8xrYFmndLA==
|
||||
turbo@^1.0.24-canary.2:
|
||||
version "1.0.24-canary.2"
|
||||
resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.0.24-canary.2.tgz#7b7c134d4e6dce0cf6e9e7b09393dde9767c397e"
|
||||
integrity sha512-Ve/KYeSxPBGKB6qmHc7BBZhZ7AaFH6WeUjd97IB38zfEtrcLlQ0xziY8EX7qiACarbrV9TKkYG5QctI5OIbD9A==
|
||||
optionalDependencies:
|
||||
turbo-darwin-64 "1.0.23"
|
||||
turbo-darwin-arm64 "1.0.23"
|
||||
turbo-freebsd-64 "1.0.23"
|
||||
turbo-freebsd-arm64 "1.0.23"
|
||||
turbo-linux-32 "1.0.23"
|
||||
turbo-linux-64 "1.0.23"
|
||||
turbo-linux-arm "1.0.23"
|
||||
turbo-linux-arm64 "1.0.23"
|
||||
turbo-linux-mips64le "1.0.23"
|
||||
turbo-linux-ppc64le "1.0.23"
|
||||
turbo-windows-32 "1.0.23"
|
||||
turbo-windows-64 "1.0.23"
|
||||
turbo-darwin-64 "1.0.24-canary.2"
|
||||
turbo-darwin-arm64 "1.0.24-canary.2"
|
||||
turbo-freebsd-64 "1.0.24-canary.2"
|
||||
turbo-freebsd-arm64 "1.0.24-canary.2"
|
||||
turbo-linux-32 "1.0.24-canary.2"
|
||||
turbo-linux-64 "1.0.24-canary.2"
|
||||
turbo-linux-arm "1.0.24-canary.2"
|
||||
turbo-linux-arm64 "1.0.24-canary.2"
|
||||
turbo-linux-mips64le "1.0.24-canary.2"
|
||||
turbo-linux-ppc64le "1.0.24-canary.2"
|
||||
turbo-windows-32 "1.0.24-canary.2"
|
||||
turbo-windows-64 "1.0.24-canary.2"
|
||||
|
||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||
version "0.14.5"
|
||||
|
Reference in New Issue
Block a user