⚗️ Add export results
This commit is contained in:
3
apps/builder/.gitignore
vendored
3
apps/builder/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
cypress/videos
|
cypress/videos
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
|
cypress/downloads
|
||||||
81
apps/builder/components/results/ResultsActionButtons.tsx
Normal file
81
apps/builder/components/results/ResultsActionButtons.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
HStack,
|
||||||
|
Button,
|
||||||
|
Fade,
|
||||||
|
Tag,
|
||||||
|
Text,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { DownloadIcon, TrashIcon } from 'assets/icons'
|
||||||
|
import { ConfirmModal } from 'components/modals/ConfirmModal'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type ResultsActionButtonsProps = {
|
||||||
|
totalSelected: number
|
||||||
|
isDeleteLoading: boolean
|
||||||
|
isExportLoading: boolean
|
||||||
|
onDeleteClick: () => Promise<void>
|
||||||
|
onExportClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResultsActionButtons = ({
|
||||||
|
totalSelected,
|
||||||
|
isDeleteLoading,
|
||||||
|
isExportLoading,
|
||||||
|
onDeleteClick,
|
||||||
|
onExportClick,
|
||||||
|
}: ResultsActionButtonsProps) => {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
|
return (
|
||||||
|
<HStack>
|
||||||
|
<Fade in={totalSelected > 0} unmountOnExit>
|
||||||
|
<HStack
|
||||||
|
as={Button}
|
||||||
|
colorScheme="blue"
|
||||||
|
onClick={onExportClick}
|
||||||
|
isLoading={isExportLoading}
|
||||||
|
>
|
||||||
|
<DownloadIcon />
|
||||||
|
<Text>Export</Text>
|
||||||
|
|
||||||
|
<Tag colorScheme="blue" variant="subtle" size="sm">
|
||||||
|
{totalSelected}
|
||||||
|
</Tag>
|
||||||
|
</HStack>
|
||||||
|
</Fade>
|
||||||
|
|
||||||
|
<Fade in={totalSelected > 0} unmountOnExit>
|
||||||
|
<HStack
|
||||||
|
as={Button}
|
||||||
|
colorScheme="red"
|
||||||
|
onClick={onOpen}
|
||||||
|
isLoading={isDeleteLoading}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
<Text>Delete</Text>
|
||||||
|
{totalSelected > 0 && (
|
||||||
|
<Tag colorScheme="red" variant="subtle" size="sm">
|
||||||
|
{totalSelected}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onConfirm={onDeleteClick}
|
||||||
|
onClose={onClose}
|
||||||
|
message={
|
||||||
|
<Text>
|
||||||
|
You are about to delete{' '}
|
||||||
|
<strong>
|
||||||
|
{totalSelected} submission
|
||||||
|
{totalSelected > 1 ? 's' : ''}
|
||||||
|
</strong>
|
||||||
|
. Are you sure you wish to continue?
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
confirmButtonLabel={'Delete'}
|
||||||
|
/>
|
||||||
|
</Fade>
|
||||||
|
</HStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,46 +1,33 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable react/jsx-key */
|
/* eslint-disable react/jsx-key */
|
||||||
import { Box, Checkbox, 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 { useTypebot } from 'contexts/TypebotContext'
|
||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useMemo, useRef } from 'react'
|
||||||
import { Hooks, useFlexLayout, useRowSelect, useTable } from 'react-table'
|
import { Hooks, useFlexLayout, useRowSelect, useTable } from 'react-table'
|
||||||
import { parseSubmissionsColumns } from 'services/publicTypebot'
|
import { parseSubmissionsColumns } from 'services/publicTypebot'
|
||||||
import { parseDateToReadable } from 'services/results'
|
|
||||||
import { LoadingRows } from './LoadingRows'
|
import { LoadingRows } from './LoadingRows'
|
||||||
|
|
||||||
const defaultCellWidth = 180
|
const defaultCellWidth = 180
|
||||||
|
|
||||||
type SubmissionsTableProps = {
|
type SubmissionsTableProps = {
|
||||||
results?: (Result & { answers: Answer[] })[]
|
data?: any
|
||||||
hasMore?: boolean
|
hasMore?: boolean
|
||||||
onNewSelection: (selection: string[]) => void
|
onNewSelection: (indices: number[]) => void
|
||||||
onScrollToBottom: () => void
|
onScrollToBottom: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubmissionsTable = ({
|
export const SubmissionsTable = ({
|
||||||
results,
|
data,
|
||||||
hasMore,
|
hasMore,
|
||||||
onNewSelection,
|
onNewSelection,
|
||||||
onScrollToBottom,
|
onScrollToBottom,
|
||||||
}: SubmissionsTableProps) => {
|
}: SubmissionsTableProps) => {
|
||||||
const { publishedTypebot } = useTypebot()
|
const { publishedTypebot } = useTypebot()
|
||||||
const columns: any = React.useMemo(
|
const columns: any = useMemo(
|
||||||
() => parseSubmissionsColumns(publishedTypebot),
|
() => parseSubmissionsColumns(publishedTypebot),
|
||||||
[publishedTypebot]
|
[publishedTypebot]
|
||||||
)
|
)
|
||||||
const data = React.useMemo(
|
|
||||||
() =>
|
|
||||||
(results ?? []).map((result) => ({
|
|
||||||
createdAt: parseDateToReadable(result.createdAt),
|
|
||||||
...result.answers.reduce(
|
|
||||||
(o, answer) => ({ ...o, [answer.blockId]: answer.content }),
|
|
||||||
{}
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[results?.length]
|
|
||||||
)
|
|
||||||
const bottomElement = useRef<HTMLDivElement | null>(null)
|
const bottomElement = useRef<HTMLDivElement | null>(null)
|
||||||
const tableWrapper = useRef<HTMLDivElement | null>(null)
|
const tableWrapper = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
@@ -59,8 +46,7 @@ export const SubmissionsTable = ({
|
|||||||
) as any
|
) as any
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!results) return
|
onNewSelection(selectedFlatRows.map((row: any) => row.index))
|
||||||
onNewSelection(selectedFlatRows.map((row: any) => results[row.index].id))
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selectedFlatRows])
|
}, [selectedFlatRows])
|
||||||
|
|
||||||
@@ -130,8 +116,7 @@ export const SubmissionsTable = ({
|
|||||||
as="tr"
|
as="tr"
|
||||||
{...row.getRowProps()}
|
{...row.getRowProps()}
|
||||||
ref={(ref) => {
|
ref={(ref) => {
|
||||||
if (results && idx === results.length - 10)
|
if (idx === data.length - 10) bottomElement.current = ref
|
||||||
bottomElement.current = ref
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{row.cells.map((cell: any, idx: number) => {
|
{row.cells.map((cell: any, idx: number) => {
|
||||||
@@ -155,9 +140,7 @@ export const SubmissionsTable = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{(results === undefined || hasMore === true) && (
|
{hasMore === true && <LoadingRows totalColumns={columns.length} />}
|
||||||
<LoadingRows totalColumns={columns.length} />
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export const TypebotHeader = () => {
|
|||||||
h={`${headerHeight}px`}
|
h={`${headerHeight}px`}
|
||||||
zIndex={2}
|
zIndex={2}
|
||||||
bgColor="white"
|
bgColor="white"
|
||||||
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<HStack>
|
<HStack>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { parse } from 'papaparse'
|
||||||
|
|
||||||
describe('ResultsPage', () => {
|
describe('ResultsPage', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.intercept({ url: '/api/typebots/typebot2/results*', method: 'GET' }).as(
|
cy.intercept({ url: '/api/typebots/typebot2/results*', method: 'GET' }).as(
|
||||||
@@ -44,4 +47,48 @@ describe('ResultsPage', () => {
|
|||||||
cy.findByText('content50').should('exist')
|
cy.findByText('content50').should('exist')
|
||||||
cy.findByText('content0').should('exist')
|
cy.findByText('content0').should('exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.only('should correctly export selection in CSV', () => {
|
||||||
|
const downloadsFolder = Cypress.config('downloadsFolder')
|
||||||
|
cy.signIn('test2@gmail.com')
|
||||||
|
cy.visit('/typebots/typebot2/results')
|
||||||
|
cy.wait('@getResults')
|
||||||
|
cy.findByRole('button', { name: 'Export' }).should('not.exist')
|
||||||
|
cy.findByText('content199').should('exist')
|
||||||
|
cy.findAllByRole('checkbox').eq(2).check({ force: true })
|
||||||
|
cy.findAllByRole('checkbox').eq(3).check({ force: true })
|
||||||
|
cy.findByRole('button', { name: 'Export 2' }).click({ force: true })
|
||||||
|
const filename = path.join(
|
||||||
|
downloadsFolder,
|
||||||
|
`typebot-export_${new Date()
|
||||||
|
.toLocaleDateString()
|
||||||
|
.replaceAll('/', '-')}.csv`
|
||||||
|
)
|
||||||
|
cy.readFile(filename, { timeout: 15000 })
|
||||||
|
.then(parse)
|
||||||
|
.then(validateExportSelection as any)
|
||||||
|
cy.findAllByRole('checkbox').first().check({ force: true })
|
||||||
|
cy.findByRole('button', { name: 'Export 200' }).click({ force: true })
|
||||||
|
const filenameAll = path.join(
|
||||||
|
downloadsFolder,
|
||||||
|
`typebot-export_${new Date()
|
||||||
|
.toLocaleDateString()
|
||||||
|
.replaceAll('/', '-')}_all.csv`
|
||||||
|
)
|
||||||
|
cy.readFile(filenameAll, { timeout: 15000 })
|
||||||
|
.then(parse)
|
||||||
|
.then(validateExportAll as any)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const validateExportSelection = (list: { data: unknown[][] }) => {
|
||||||
|
expect(list.data, 'number of records').to.have.length(3)
|
||||||
|
expect(list.data[1][1], 'first record').to.equal('content198')
|
||||||
|
expect(list.data[2][1], 'second record').to.equal('content197')
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateExportAll = (list: { data: unknown[][] }) => {
|
||||||
|
expect(list.data, 'number of records').to.have.length(201)
|
||||||
|
expect(list.data[1][1], 'first record').to.equal('content199')
|
||||||
|
expect(list.data[200][1], 'second record').to.equal('content0')
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"exclude": [],
|
"exclude": [],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["cypress", "@testing-library/cypress", "cypress-file-upload"],
|
"types": ["cypress", "@testing-library/cypress", "cypress-file-upload"],
|
||||||
"lib": ["es2015", "dom"],
|
"lib": ["es2015", "dom", "ES2021.String"],
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"isolatedModules": false,
|
"isolatedModules": false,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
import {
|
import { Stack, useToast, Flex } from '@chakra-ui/react'
|
||||||
Button,
|
import { ResultsActionButtons } from 'components/results/ResultsActionButtons'
|
||||||
HStack,
|
|
||||||
Stack,
|
|
||||||
Tag,
|
|
||||||
useToast,
|
|
||||||
Text,
|
|
||||||
Fade,
|
|
||||||
Flex,
|
|
||||||
useDisclosure,
|
|
||||||
} from '@chakra-ui/react'
|
|
||||||
import { DownloadIcon, TrashIcon } from 'assets/icons'
|
|
||||||
import { ConfirmModal } from 'components/modals/ConfirmModal'
|
|
||||||
import { SubmissionsTable } from 'components/results/SubmissionsTable'
|
import { SubmissionsTable } from 'components/results/SubmissionsTable'
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { deleteAllResults, deleteResults, useResults } from 'services/results'
|
import {
|
||||||
|
convertResultsToTableData,
|
||||||
|
deleteAllResults,
|
||||||
|
deleteResults,
|
||||||
|
getAllResults,
|
||||||
|
useResults,
|
||||||
|
} from 'services/results'
|
||||||
|
import { unparse } from 'papaparse'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
typebotId: string
|
typebotId: string
|
||||||
@@ -25,10 +21,9 @@ export const SubmissionsContent = ({
|
|||||||
totalResults,
|
totalResults,
|
||||||
onDeleteResults,
|
onDeleteResults,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
const [selectedIndices, setSelectedIndices] = useState<number[]>([])
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false)
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false)
|
||||||
|
const [isExportLoading, setIsExportLoading] = useState(false)
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
|
||||||
|
|
||||||
const toast = useToast({
|
const toast = useToast({
|
||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
@@ -42,13 +37,16 @@ export const SubmissionsContent = ({
|
|||||||
|
|
||||||
const results = useMemo(() => data?.flatMap((d) => d.results), [data])
|
const results = useMemo(() => data?.flatMap((d) => d.results), [data])
|
||||||
|
|
||||||
const handleNewSelection = (newSelection: string[]) => {
|
const handleNewSelection = (newSelectionIndices: number[]) => {
|
||||||
if (newSelection.length === selectedIds.length) return
|
if (newSelectionIndices.length === selectedIndices.length) return
|
||||||
setSelectedIds(newSelection)
|
setSelectedIndices(newSelectionIndices)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteSelection = async () => {
|
const handleDeleteSelection = async () => {
|
||||||
setIsDeleteLoading(true)
|
setIsDeleteLoading(true)
|
||||||
|
const selectedIds = (results ?? [])
|
||||||
|
.filter((_, idx) => selectedIndices.includes(idx))
|
||||||
|
.map((result) => result.id)
|
||||||
const { error } =
|
const { error } =
|
||||||
totalSelected === totalResults
|
totalSelected === totalResults
|
||||||
? await deleteAllResults(typebotId)
|
? await deleteAllResults(typebotId)
|
||||||
@@ -69,10 +67,10 @@ export const SubmissionsContent = ({
|
|||||||
|
|
||||||
const totalSelected = useMemo(
|
const totalSelected = useMemo(
|
||||||
() =>
|
() =>
|
||||||
selectedIds.length === results?.length
|
selectedIndices.length === results?.length
|
||||||
? totalResults
|
? totalResults
|
||||||
: selectedIds.length,
|
: selectedIndices.length,
|
||||||
[results?.length, selectedIds.length, totalResults]
|
[results?.length, selectedIndices.length, totalResults]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleScrolledToBottom = useCallback(
|
const handleScrolledToBottom = useCallback(
|
||||||
@@ -80,59 +78,51 @@ export const SubmissionsContent = ({
|
|||||||
[setSize]
|
[setSize]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleExportSelection = async () => {
|
||||||
|
setIsExportLoading(true)
|
||||||
|
const isSelectAll = totalSelected === totalResults
|
||||||
|
const dataToUnparse = isSelectAll
|
||||||
|
? await getAllTableData()
|
||||||
|
: tableData.filter((_, idx) => selectedIndices.includes(idx))
|
||||||
|
const csvData = new Blob([unparse(dataToUnparse)], {
|
||||||
|
type: 'text/csv;charset=utf-8;',
|
||||||
|
})
|
||||||
|
const fileName =
|
||||||
|
`typebot-export_${new Date().toLocaleDateString().replaceAll('/', '-')}` +
|
||||||
|
(isSelectAll ? `_all` : ``)
|
||||||
|
const tempLink = document.createElement('a')
|
||||||
|
tempLink.href = window.URL.createObjectURL(csvData)
|
||||||
|
tempLink.setAttribute('download', `${fileName}.csv`)
|
||||||
|
tempLink.click()
|
||||||
|
setIsExportLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllTableData = async () => {
|
||||||
|
const { data, error } = await getAllResults(typebotId)
|
||||||
|
if (error) toast({ description: error.message, title: error.name })
|
||||||
|
return convertResultsToTableData(data?.results)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableData: { [key: string]: string }[] = useMemo(
|
||||||
|
() => convertResultsToTableData(results),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[results?.length]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack maxW="1200px" w="full">
|
<Stack maxW="1200px" w="full">
|
||||||
<Flex w="full" justifyContent="flex-end">
|
<Flex w="full" justifyContent="flex-end">
|
||||||
<HStack>
|
<ResultsActionButtons
|
||||||
<HStack as={Button} colorScheme="blue">
|
isDeleteLoading={isDeleteLoading}
|
||||||
<DownloadIcon />
|
isExportLoading={isExportLoading}
|
||||||
<Text>Export</Text>
|
totalSelected={totalSelected}
|
||||||
<Fade
|
onDeleteClick={handleDeleteSelection}
|
||||||
in={totalSelected > 0 && (results ?? []).length > 0}
|
onExportClick={handleExportSelection}
|
||||||
unmountOnExit
|
/>
|
||||||
>
|
|
||||||
<Tag colorScheme="blue" variant="subtle" size="sm">
|
|
||||||
{totalSelected}
|
|
||||||
</Tag>
|
|
||||||
</Fade>
|
|
||||||
</HStack>
|
|
||||||
<Fade in={totalSelected > 0} unmountOnExit>
|
|
||||||
<HStack
|
|
||||||
as={Button}
|
|
||||||
colorScheme="red"
|
|
||||||
onClick={onOpen}
|
|
||||||
isLoading={isDeleteLoading}
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
|
||||||
<Text>Delete</Text>
|
|
||||||
{totalSelected > 0 && (
|
|
||||||
<Tag colorScheme="red" variant="subtle" size="sm">
|
|
||||||
{totalSelected}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onConfirm={handleDeleteSelection}
|
|
||||||
onClose={onClose}
|
|
||||||
message={
|
|
||||||
<Text>
|
|
||||||
You are about to delete{' '}
|
|
||||||
<strong>
|
|
||||||
{totalSelected} submission
|
|
||||||
{totalSelected > 1 ? 's' : ''}
|
|
||||||
</strong>
|
|
||||||
. Are you sure you wish to continue?
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
confirmButtonLabel={'Delete'}
|
|
||||||
/>
|
|
||||||
</Fade>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<SubmissionsTable
|
<SubmissionsTable
|
||||||
results={results}
|
data={tableData}
|
||||||
onNewSelection={handleNewSelection}
|
onNewSelection={handleNewSelection}
|
||||||
onScrollToBottom={handleScrolledToBottom}
|
onScrollToBottom={handleScrolledToBottom}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"next-auth": "beta",
|
"next-auth": "beta",
|
||||||
"nodemailer": "^6.7.2",
|
"nodemailer": "^6.7.2",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
|
"papaparse": "^5.3.1",
|
||||||
"qs": "^6.10.2",
|
"qs": "^6.10.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
"@types/micro-cors": "^0.1.2",
|
"@types/micro-cors": "^0.1.2",
|
||||||
"@types/node": "^16.11.9",
|
"@types/node": "^16.11.9",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
|
"@types/papaparse": "^5.3.1",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@types/react": "^17.0.37",
|
"@types/react": "^17.0.37",
|
||||||
"@types/react-table": "^7.7.9",
|
"@types/react-table": "^7.7.9",
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const typebotId = req.query.typebotId.toString()
|
const typebotId = req.query.typebotId.toString()
|
||||||
const lastResultId = req.query.lastResultId?.toString()
|
const lastResultId = req.query.lastResultId?.toString()
|
||||||
|
const take = parseInt(req.query.limit?.toString())
|
||||||
const results = await prisma.result.findMany({
|
const results = await prisma.result.findMany({
|
||||||
take: 50,
|
take: isNaN(take) ? undefined : take,
|
||||||
skip: lastResultId ? 1 : 0,
|
skip: lastResultId ? 1 : 0,
|
||||||
cursor: lastResultId
|
cursor: lastResultId
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -12,14 +12,13 @@ const getKey = (
|
|||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
if (previousPageData && previousPageData.results.length === 0) return null
|
if (previousPageData && previousPageData.results.length === 0) return null
|
||||||
if (pageIndex === 0) return `/api/typebots/${typebotId}/results`
|
if (pageIndex === 0) return `/api/typebots/${typebotId}/results?limit=50`
|
||||||
console.log(previousPageData.results)
|
|
||||||
return `/api/typebots/${typebotId}/results?lastResultId=${
|
return `/api/typebots/${typebotId}/results?lastResultId=${
|
||||||
previousPageData.results[previousPageData.results.length - 1].id
|
previousPageData.results[previousPageData.results.length - 1].id
|
||||||
}`
|
}&limit=50`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResultWithAnswers = Result & { answers: Answer[] }
|
export type ResultWithAnswers = Result & { answers: Answer[] }
|
||||||
export const useResults = ({
|
export const useResults = ({
|
||||||
typebotId,
|
typebotId,
|
||||||
onError,
|
onError,
|
||||||
@@ -72,6 +71,12 @@ export const deleteAllResults = async (typebotId: string) =>
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const getAllResults = async (typebotId: string) =>
|
||||||
|
sendRequest<{ results: ResultWithAnswers[] }>({
|
||||||
|
url: `/api/typebots/${typebotId}/results`,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
export const parseDateToReadable = (dateStr: string): string => {
|
export const parseDateToReadable = (dateStr: string): string => {
|
||||||
const date = new Date(dateStr)
|
const date = new Date(dateStr)
|
||||||
return (
|
return (
|
||||||
@@ -83,3 +88,12 @@ export const parseDateToReadable = (dateStr: string): string => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const convertResultsToTableData = (results?: ResultWithAnswers[]) =>
|
||||||
|
(results ?? []).map((result) => ({
|
||||||
|
createdAt: parseDateToReadable(result.createdAt),
|
||||||
|
...result.answers.reduce(
|
||||||
|
(o, answer) => ({ ...o, [answer.blockId]: answer.content }),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@@ -1472,6 +1472,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.2.0.tgz#86c593682d4199212a0509cc3c4d562bbbd6e45f"
|
resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.2.0.tgz#86c593682d4199212a0509cc3c4d562bbbd6e45f"
|
||||||
integrity sha512-1cYJrqq9GezNFPsWTZpFut/d4CjpZqA0vhqDUPFWYKF1oIyBz5qnoYMzR+0C/T96t3ebLAC1SSnwrVOm5/j74A==
|
integrity sha512-1cYJrqq9GezNFPsWTZpFut/d4CjpZqA0vhqDUPFWYKF1oIyBz5qnoYMzR+0C/T96t3ebLAC1SSnwrVOm5/j74A==
|
||||||
|
|
||||||
|
"@types/papaparse@^5.3.1":
|
||||||
|
version "5.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.3.1.tgz#fb5c613a64473c33b08fb9bc2a5ddbf25e54784e"
|
||||||
|
integrity sha512-1lbngk9wty2kCyQB42LjqSa12SEop3t9wcEC7/xYr3ujTSTmv7HWKjKYXly0GkMfQ42PRb2lFPFEibDOiMXS0g==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/parse-json@^4.0.0":
|
"@types/parse-json@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
@@ -5432,6 +5439,11 @@ pako@~1.0.5:
|
|||||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||||
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
||||||
|
|
||||||
|
papaparse@^5.3.1:
|
||||||
|
version "5.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.1.tgz#770b7a9124d821d4b2132132b7bd7dce7194b5b1"
|
||||||
|
integrity sha512-Dbt2yjLJrCwH2sRqKFFJaN5XgIASO9YOFeFP8rIBRG2Ain8mqk5r1M6DkfvqEVozVcz3r3HaUGw253hA1nLIcA==
|
||||||
|
|
||||||
parent-module@^1.0.0:
|
parent-module@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||||
|
|||||||
Reference in New Issue
Block a user