From 8ddf608c9ecd71376cf4125e74899e4b4e60a9cc Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Tue, 4 Jan 2022 09:15:33 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20Add=20delete=20results=20l?= =?UTF-8?q?ogic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SubmissionsTable/SubmissionsTable.tsx | 24 ++-- apps/builder/cypress/plugins/database.ts | 112 ++++++++++++++++-- apps/builder/cypress/tests/results.ts | 31 +++++ .../layouts/results/SubmissionContent.tsx | 50 +++++++- .../pages/api/typebots/[typebotId]/results.ts | 8 ++ apps/builder/services/results.ts | 15 ++- 6 files changed, 218 insertions(+), 22 deletions(-) create mode 100644 apps/builder/cypress/tests/results.ts diff --git a/apps/builder/components/results/SubmissionsTable/SubmissionsTable.tsx b/apps/builder/components/results/SubmissionsTable/SubmissionsTable.tsx index 05ec43ee0dd..d7c1956c743 100644 --- a/apps/builder/components/results/SubmissionsTable/SubmissionsTable.tsx +++ b/apps/builder/components/results/SubmissionsTable/SubmissionsTable.tsx @@ -4,11 +4,13 @@ import { Box, Checkbox, Flex } from '@chakra-ui/react' import { Answer, Result } from 'bot-engine' import { useTypebot } from 'contexts/TypebotContext' import React, { useEffect } from 'react' -import { Hooks, useRowSelect, useTable } from 'react-table' +import { Hooks, useFlexLayout, useRowSelect, useTable } from 'react-table' import { parseSubmissionsColumns } from 'services/publicTypebot' import { parseDateToReadable } from 'services/results' import { LoadingRows } from './LoadingRows' +const defaultCellWidth = 180 + type SubmissionsTableProps = { results?: (Result & { answers: Answer[] })[] onNewSelection: (selection: string[]) => void @@ -42,10 +44,16 @@ export const SubmissionsTable = ({ prepareRow, getTableBodyProps, selectedFlatRows, - } = useTable({ columns, data }, useRowSelect, checkboxColumnHook) as any + } = useTable( + { columns, data, defaultColumn: { width: defaultCellWidth } }, + useRowSelect, + checkboxColumnHook, + useFlexLayout + ) as any useEffect(() => { - onNewSelection(selectedFlatRows.map((row: any) => row.id)) + if (!results) return + onNewSelection(selectedFlatRows.map((row: any) => results[row.index].id)) // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedFlatRows]) @@ -67,9 +75,10 @@ export const SubmissionsTable = ({ color="gray.500" fontWeight="normal" textAlign="left" - minW={idx > 0 ? '200px' : 'unset'} - flex={idx > 0 ? '1' : '0'} {...column.getHeaderProps()} + style={{ + width: idx === 0 ? '50px' : `${defaultCellWidth}px`, + }} > {column.render('Header')} @@ -96,9 +105,10 @@ export const SubmissionsTable = ({ border="1px" as="td" borderColor="gray.200" - minW={idx > 0 ? '200px' : 'unset'} {...cell.getCellProps()} - flex={idx > 0 ? '1' : '0'} + style={{ + width: idx === 0 ? '50px' : `${defaultCellWidth}px`, + }} > {cell.render('Cell')} diff --git a/apps/builder/cypress/plugins/database.ts b/apps/builder/cypress/plugins/database.ts index 8a01d329924..c8162dfcfb3 100644 --- a/apps/builder/cypress/plugins/database.ts +++ b/apps/builder/cypress/plugins/database.ts @@ -1,4 +1,4 @@ -import { parseNewTypebot } from 'bot-engine' +import { parseNewTypebot, PublicTypebot, StepType, Typebot } from 'bot-engine' import { Plan, PrismaClient } from 'db' const prisma = new PrismaClient() @@ -9,7 +9,9 @@ export const seedDb = async () => { await teardownTestData() await createUsers() await createFolders() - return createTypebots() + await createTypebots() + await createResults() + return createAnswers() } const createUsers = () => @@ -31,8 +33,38 @@ const createFolders = () => data: [{ ownerId: 'test2', name: 'Folder #1', id: 'folder1' }], }) -const createTypebots = () => { - return prisma.typebot.createMany({ +const createTypebots = async () => { + const typebot2: Typebot = { + ...(parseNewTypebot({ + name: 'Typebot #2', + ownerId: 'test2', + folderId: null, + }) as Typebot), + id: 'typebot2', + startBlock: { + id: 'start-block', + steps: [ + { + id: 'start-step', + blockId: 'start-block', + type: StepType.START, + label: 'Start', + target: { blockId: 'block1' }, + }, + ], + graphCoordinates: { x: 0, y: 0 }, + title: 'Start', + }, + blocks: [ + { + title: 'Block #1', + id: 'block1', + steps: [{ id: 'step1', type: StepType.TEXT_INPUT, blockId: 'block1' }], + graphCoordinates: { x: 200, y: 200 }, + }, + ], + } + await prisma.typebot.createMany({ data: [ { ...parseNewTypebot({ @@ -42,14 +74,74 @@ const createTypebots = () => { }), id: 'typebot1', }, + typebot2, + ], + }) + return prisma.publicTypebot.createMany({ + data: [parseTypebotToPublicTypebot('publictypebot2', typebot2)], + }) +} + +const createResults = () => { + return prisma.result.createMany({ + data: [ { - ...parseNewTypebot({ - name: 'Typebot #2', - ownerId: 'test2', - folderId: null, - }), - id: 'typebot2', + typebotId: 'typebot1', + }, + { + typebotId: 'typebot1', + }, + { + id: 'result1', + typebotId: 'typebot2', + }, + { + id: 'result2', + typebotId: 'typebot2', + }, + { + id: 'result3', + typebotId: 'typebot2', + }, + ], + }) +} + +const createAnswers = () => { + return prisma.answer.createMany({ + data: [ + { + resultId: 'result1', + content: 'content 1', + stepId: 'step1', + blockId: 'block1', + }, + { + resultId: 'result2', + content: 'content 2', + stepId: 'step1', + blockId: 'block1', + }, + { + resultId: 'result3', + content: 'content 3', + stepId: 'step1', + blockId: 'block1', }, ], }) } + +const parseTypebotToPublicTypebot = ( + id: string, + typebot: Typebot +): PublicTypebot => ({ + id, + blocks: typebot.blocks, + name: typebot.name, + startBlock: typebot.startBlock, + typebotId: typebot.id, + theme: typebot.theme, + settings: typebot.settings, + publicId: typebot.publicId, +}) diff --git a/apps/builder/cypress/tests/results.ts b/apps/builder/cypress/tests/results.ts new file mode 100644 index 00000000000..e330ff9b156 --- /dev/null +++ b/apps/builder/cypress/tests/results.ts @@ -0,0 +1,31 @@ +describe('ResultsPage', () => { + before(() => { + cy.intercept({ url: '/api/typebots/typebot2/results?', method: 'GET' }).as( + 'getResults' + ) + }) + beforeEach(() => { + cy.task('seed') + cy.signOut() + }) + + it('results should be deletable', () => { + cy.signIn('test2@gmail.com') + cy.visit('/typebots/typebot2/results') + cy.wait('@getResults') + cy.findByText('content 2').should('exist') + cy.findByText('content 3').should('exist') + cy.findAllByRole('checkbox').eq(2).check({ force: true }) + cy.findAllByRole('checkbox').eq(3).check({ force: true }) + cy.findByRole('button', { name: 'Delete 2' }).click() + cy.findByRole('button', { name: 'Delete' }).click() + cy.findByText('content 2').should('not.exist') + cy.findByText('content 3').should('not.exist') + }) + + it.only('submissions table should have infinite scroll', () => { + cy.signIn('test2@gmail.com') + cy.visit('/typebots/typebot2/results') + cy.wait('@getResults') + }) +}) diff --git a/apps/builder/layouts/results/SubmissionContent.tsx b/apps/builder/layouts/results/SubmissionContent.tsx index 517105a81f1..5a2b2fd60cb 100644 --- a/apps/builder/layouts/results/SubmissionContent.tsx +++ b/apps/builder/layouts/results/SubmissionContent.tsx @@ -7,23 +7,28 @@ import { 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 React, { useMemo, useState } from 'react' -import { useResults } from 'services/results' +import { deleteResults, useResults } from 'services/results' type Props = { typebotId: string; totalResults: number } export const SubmissionsContent = ({ typebotId, totalResults }: Props) => { const [lastResultId, setLastResultId] = useState() const [selectedIds, setSelectedIds] = useState([]) + const [isDeleteLoading, setIsDeleteLoading] = useState(false) + + const { isOpen, onOpen, onClose } = useDisclosure() const toast = useToast({ position: 'top-right', status: 'error', }) - const { results } = useResults({ + const { results, mutate } = useResults({ lastResultId, typebotId, onError: (err) => toast({ title: err.name, description: err.message }), @@ -34,6 +39,19 @@ export const SubmissionsContent = ({ typebotId, totalResults }: Props) => { setSelectedIds(newSelection) } + const handleDeleteSelection = async () => { + setIsDeleteLoading(true) + const { error } = await deleteResults(typebotId, selectedIds) + if (error) toast({ description: error.message, title: error.name }) + else + mutate({ + results: (results ?? []).filter((result) => + selectedIds.includes(result.id) + ), + }) + setIsDeleteLoading(false) + } + const totalSelected = useMemo( () => selectedIds.length === results?.length @@ -49,14 +67,22 @@ export const SubmissionsContent = ({ typebotId, totalResults }: Props) => { Export - 0} unmountOnExit> + 0 && (results ?? []).length > 0} + unmountOnExit + > {totalSelected} 0} unmountOnExit> - + Delete {totalSelected > 0 && ( @@ -64,6 +90,22 @@ export const SubmissionsContent = ({ typebotId, totalResults }: Props) => { {totalSelected} )} + + You are about to delete{' '} + + {totalSelected} submission + {totalSelected > 0 ? 's' : ''} + + . Are you sure you wish to continue? + + } + confirmButtonLabel={'Delete'} + /> diff --git a/apps/builder/pages/api/typebots/[typebotId]/results.ts b/apps/builder/pages/api/typebots/[typebotId]/results.ts index add539786b9..375ccb798d5 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/results.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/results.ts @@ -34,6 +34,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { }) return res.status(200).send({ results }) } + if (req.method === 'DELETE') { + const typebotId = req.query.typebotId.toString() + const ids = req.query.ids as string[] + const results = await prisma.result.deleteMany({ + where: { id: { in: ids }, typebotId, typebot: { ownerId: user.id } }, + }) + return res.status(200).send({ results }) + } return methodNotAllowed(res) } diff --git a/apps/builder/services/results.ts b/apps/builder/services/results.ts index b5d8601be96..65e0c9d9d10 100644 --- a/apps/builder/services/results.ts +++ b/apps/builder/services/results.ts @@ -1,6 +1,6 @@ import { Result } from 'bot-engine' import useSWR from 'swr' -import { fetcher } from './utils' +import { fetcher, sendRequest } from './utils' import { stringify } from 'qs' import { Answer } from 'db' @@ -28,6 +28,19 @@ export const useResults = ({ } } +export const deleteResults = async (typebotId: string, ids: string[]) => { + const params = stringify( + { + ids, + }, + { indices: false } + ) + return sendRequest({ + url: `/api/typebots/${typebotId}/results?${params}`, + method: 'DELETE', + }) +} + export const parseDateToReadable = (dateStr: string): string => { const date = new Date(dateStr) return (