feat(results): ✨ Brand new Results table
- Resizable columns - Hide / Show columns - Reorganize columns - Expand result
This commit is contained in:
101
apps/builder/components/results/LogsModal.tsx
Normal file
101
apps/builder/components/results/LogsModal.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
Stack,
|
||||
Spinner,
|
||||
ModalFooter,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
HStack,
|
||||
AccordionIcon,
|
||||
AccordionPanel,
|
||||
Text,
|
||||
Tag,
|
||||
} from '@chakra-ui/react'
|
||||
import { Log } from 'db'
|
||||
import { useLogs } from 'services/typebots/logs'
|
||||
import { isDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
typebotId: string
|
||||
resultId: string | null
|
||||
onClose: () => void
|
||||
}
|
||||
export const LogsModal = ({ typebotId, resultId, onClose }: Props) => {
|
||||
const { isLoading, logs } = useLogs(typebotId, resultId ?? undefined)
|
||||
return (
|
||||
<Modal isOpen={isDefined(resultId)} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Logs</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody as={Stack}>
|
||||
{logs?.map((log, idx) => (
|
||||
<LogCard key={idx} log={log} />
|
||||
))}
|
||||
{isLoading && <Spinner />}
|
||||
{!isLoading && (logs ?? []).length === 0 && (
|
||||
<Text>No logs found.</Text>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const LogCard = ({ log }: { log: Log }) => {
|
||||
if (log.details)
|
||||
return (
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem style={{ borderBottomWidth: 0, borderWidth: 0 }}>
|
||||
<AccordionButton
|
||||
as={HStack}
|
||||
p="4"
|
||||
cursor="pointer"
|
||||
justifyContent="space-between"
|
||||
borderRadius="md"
|
||||
>
|
||||
<HStack>
|
||||
<StatusTag status={log.status} />
|
||||
<Text>{log.description}</Text>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel
|
||||
as="pre"
|
||||
overflow="scroll"
|
||||
borderWidth="1px"
|
||||
borderRadius="md"
|
||||
>
|
||||
{log.details}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)
|
||||
return (
|
||||
<HStack p="4">
|
||||
<StatusTag status={log.status} />
|
||||
<Text>{log.description}</Text>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const StatusTag = ({ status }: { status: string }) => {
|
||||
switch (status) {
|
||||
case 'error':
|
||||
return <Tag colorScheme={'red'}>Fail</Tag>
|
||||
case 'warning':
|
||||
return <Tag colorScheme={'orange'}>Warn</Tag>
|
||||
case 'info':
|
||||
return <Tag colorScheme={'blue'}>Info</Tag>
|
||||
default:
|
||||
return <Tag colorScheme={'green'}>Ok</Tag>
|
||||
}
|
||||
}
|
55
apps/builder/components/results/ResultModal.tsx
Normal file
55
apps/builder/components/results/ResultModal.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
Stack,
|
||||
Heading,
|
||||
Text,
|
||||
HStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { useResults } from 'contexts/ResultsProvider'
|
||||
import React from 'react'
|
||||
import { isDefined } from 'utils'
|
||||
import { HeaderIcon } from './ResultsTable/ResultsTable'
|
||||
|
||||
type Props = {
|
||||
resultIdx: number | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const ResultModal = ({ resultIdx, onClose }: Props) => {
|
||||
const { tableData, resultHeader } = useResults()
|
||||
const result = isDefined(resultIdx) ? tableData[resultIdx] : undefined
|
||||
|
||||
const getHeaderValue = (
|
||||
val: string | { plainText: string; element?: JSX.Element | undefined }
|
||||
) => (typeof val === 'string' ? val : val.element ?? val.plainText)
|
||||
|
||||
return (
|
||||
<Modal isOpen={isDefined(result)} onClose={onClose} size="2xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalCloseButton />
|
||||
<ModalBody as={Stack} p="10" spacing="10">
|
||||
{resultHeader.map((header) =>
|
||||
result && result[header.label] ? (
|
||||
<Stack key={header.id} spacing="4">
|
||||
<HStack>
|
||||
<HeaderIcon header={header} />
|
||||
<Heading fontSize="md">{header.label}</Heading>
|
||||
</HStack>
|
||||
<Text whiteSpace="pre-wrap" textAlign="justify">
|
||||
{getHeaderValue(result[header.label])}
|
||||
</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
81
apps/builder/components/results/ResultsContent.tsx
Normal file
81
apps/builder/components/results/ResultsContent.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { Stack } from '@chakra-ui/react'
|
||||
import { SubmissionsTable } from 'components/results/ResultsTable'
|
||||
import React, { useState } from 'react'
|
||||
import { UnlockPlanInfo } from 'components/shared/Info'
|
||||
import { LogsModal } from './LogsModal'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { Plan } from 'db'
|
||||
import { useResults } from 'contexts/ResultsProvider'
|
||||
import { ResultModal } from './ResultModal'
|
||||
|
||||
export const ResultsContent = () => {
|
||||
const {
|
||||
flatResults: results,
|
||||
fetchMore,
|
||||
hasMore,
|
||||
resultHeader,
|
||||
totalHiddenResults,
|
||||
tableData,
|
||||
} = useResults()
|
||||
const { typebot, publishedTypebot } = useTypebot()
|
||||
const [inspectingLogsResultId, setInspectingLogsResultId] = useState<
|
||||
string | null
|
||||
>(null)
|
||||
const [expandedResultIndex, setExpandedResultIndex] = useState<number | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const handleLogsModalClose = () => setInspectingLogsResultId(null)
|
||||
|
||||
const handleResultModalClose = () => setExpandedResultIndex(null)
|
||||
|
||||
const handleLogOpenIndex = (index: number) => () => {
|
||||
if (!results) return
|
||||
setInspectingLogsResultId(results[index].id)
|
||||
}
|
||||
|
||||
const handleResultExpandIndex = (index: number) => () =>
|
||||
setExpandedResultIndex(index)
|
||||
|
||||
return (
|
||||
<Stack
|
||||
pb="28"
|
||||
px={['4', '0']}
|
||||
spacing="4"
|
||||
maxW="1600px"
|
||||
overflow="scroll"
|
||||
w="full"
|
||||
>
|
||||
{totalHiddenResults && (
|
||||
<UnlockPlanInfo
|
||||
buttonLabel={`Unlock ${totalHiddenResults} results`}
|
||||
contentLabel="You are seeing complete submissions only."
|
||||
plan={Plan.PRO}
|
||||
/>
|
||||
)}
|
||||
{publishedTypebot && (
|
||||
<LogsModal
|
||||
typebotId={publishedTypebot?.typebotId}
|
||||
resultId={inspectingLogsResultId}
|
||||
onClose={handleLogsModalClose}
|
||||
/>
|
||||
)}
|
||||
<ResultModal
|
||||
resultIdx={expandedResultIndex}
|
||||
onClose={handleResultModalClose}
|
||||
/>
|
||||
|
||||
{typebot && (
|
||||
<SubmissionsTable
|
||||
preferences={typebot.resultsTablePreferences}
|
||||
resultHeader={resultHeader}
|
||||
data={tableData}
|
||||
onScrollToBottom={fetchMore}
|
||||
hasMore={hasMore}
|
||||
onLogOpenIndex={handleLogOpenIndex}
|
||||
onResultExpandIndex={handleResultExpandIndex}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,218 @@
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
Button,
|
||||
PopoverContent,
|
||||
PopoverBody,
|
||||
Stack,
|
||||
IconButton,
|
||||
Flex,
|
||||
HStack,
|
||||
Text,
|
||||
Portal,
|
||||
} from '@chakra-ui/react'
|
||||
import { ToolIcon, EyeIcon, EyeOffIcon, GripIcon } from 'assets/icons'
|
||||
import { ResultHeaderCell } from 'models'
|
||||
import React, { forwardRef, useState } from 'react'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { HeaderIcon } from './ResultsTable'
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
DragStartEvent,
|
||||
DragOverlay,
|
||||
} from '@dnd-kit/core'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
useSortable,
|
||||
arrayMove,
|
||||
} from '@dnd-kit/sortable'
|
||||
|
||||
type Props = {
|
||||
resultHeader: ResultHeaderCell[]
|
||||
columnVisibility: { [key: string]: boolean }
|
||||
columnOrder: string[]
|
||||
onColumnOrderChange: (columnOrder: string[]) => void
|
||||
setColumnVisibility: (columnVisibility: { [key: string]: boolean }) => void
|
||||
}
|
||||
|
||||
export const ColumnSettingsButton = ({
|
||||
resultHeader,
|
||||
columnVisibility,
|
||||
setColumnVisibility,
|
||||
columnOrder,
|
||||
onColumnOrderChange,
|
||||
}: Props) => {
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
)
|
||||
const [draggingColumnId, setDraggingColumnId] = useState<string | null>(null)
|
||||
|
||||
const onEyeClick = (id: string) => () => {
|
||||
columnVisibility[id] === false
|
||||
? setColumnVisibility({ ...columnVisibility, [id]: true })
|
||||
: setColumnVisibility({ ...columnVisibility, [id]: false })
|
||||
}
|
||||
const visibleHeaders = resultHeader
|
||||
.filter(
|
||||
(header) =>
|
||||
isNotDefined(columnVisibility[header.id]) || columnVisibility[header.id]
|
||||
)
|
||||
.sort((a, b) => columnOrder.indexOf(a.id) - columnOrder.indexOf(b.id))
|
||||
const hiddenHeaders = resultHeader.filter(
|
||||
(header) => columnVisibility[header.id] === false
|
||||
)
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
const { active } = event
|
||||
setDraggingColumnId(active.id as string)
|
||||
}
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
|
||||
if (active.id !== over?.id) {
|
||||
onColumnOrderChange
|
||||
const oldIndex = columnOrder.indexOf(active.id as string)
|
||||
const newIndex = columnOrder.indexOf(over?.id as string)
|
||||
const newColumnOrder = arrayMove(columnOrder, oldIndex, newIndex)
|
||||
onColumnOrderChange(newColumnOrder)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover isLazy placement="bottom-end">
|
||||
<PopoverTrigger>
|
||||
<Button leftIcon={<ToolIcon />}>Columns</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="400px">
|
||||
<PopoverBody
|
||||
as={Stack}
|
||||
spacing="4"
|
||||
p="4"
|
||||
maxH="450px"
|
||||
overflowY="scroll"
|
||||
>
|
||||
<Stack>
|
||||
<Text fontWeight="semibold" fontSize="sm">
|
||||
Shown in table:
|
||||
</Text>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={columnOrder}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{visibleHeaders.map((header) => (
|
||||
<SortableColumns
|
||||
key={header.id}
|
||||
header={header}
|
||||
onEyeClick={onEyeClick}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
<Portal>
|
||||
<DragOverlay dropAnimation={{ duration: 0 }}>
|
||||
{draggingColumnId ? <SortableColumnOverlay /> : null}
|
||||
</DragOverlay>
|
||||
</Portal>
|
||||
</DndContext>
|
||||
</Stack>
|
||||
{hiddenHeaders.length > 0 && (
|
||||
<Stack>
|
||||
<Text fontWeight="semibold" fontSize="sm">
|
||||
Hidden in table:
|
||||
</Text>
|
||||
{hiddenHeaders.map((header) => (
|
||||
<Flex key={header.id} justify="space-between">
|
||||
<HStack>
|
||||
<HeaderIcon header={header} />
|
||||
<Text>{header.label}</Text>
|
||||
</HStack>
|
||||
<IconButton
|
||||
icon={<EyeOffIcon />}
|
||||
size="sm"
|
||||
aria-label={'Hide column'}
|
||||
onClick={onEyeClick(header.id)}
|
||||
/>
|
||||
</Flex>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const SortableColumns = ({
|
||||
header,
|
||||
onEyeClick,
|
||||
}: {
|
||||
header: ResultHeaderCell
|
||||
onEyeClick: (key: string) => () => void
|
||||
}) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: header.id })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
justify="space-between"
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
opacity={isDragging ? 0.5 : 1}
|
||||
{...attributes}
|
||||
>
|
||||
<HStack>
|
||||
<IconButton
|
||||
size="sm"
|
||||
cursor="grab"
|
||||
icon={<GripIcon transform="rotate(90deg)" />}
|
||||
aria-label={'Drag'}
|
||||
variant="ghost"
|
||||
{...listeners}
|
||||
/>
|
||||
<HeaderIcon header={header} />
|
||||
<Text>{header.label}</Text>
|
||||
</HStack>
|
||||
<IconButton
|
||||
icon={<EyeIcon />}
|
||||
size="sm"
|
||||
aria-label={'Hide column'}
|
||||
onClick={onEyeClick(header.id)}
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
const SortableColumnOverlay = forwardRef(
|
||||
(_, ref: React.LegacyRef<HTMLDivElement>) => {
|
||||
return <HStack ref={ref}></HStack>
|
||||
}
|
||||
)
|
60
apps/builder/components/results/ResultsTable/HeaderRow.tsx
Normal file
60
apps/builder/components/results/ResultsTable/HeaderRow.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { Box, BoxProps, chakra } from '@chakra-ui/react'
|
||||
import { HeaderGroup } from '@tanstack/react-table'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
headerGroup: HeaderGroup<any>
|
||||
}
|
||||
|
||||
export const HeaderRow = ({ headerGroup }: Props) => {
|
||||
return (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<chakra.th
|
||||
key={header.id}
|
||||
px="4"
|
||||
py="2"
|
||||
pos="relative"
|
||||
border="1px"
|
||||
borderColor="gray.200"
|
||||
fontWeight="normal"
|
||||
whiteSpace="nowrap"
|
||||
wordBreak="normal"
|
||||
colSpan={header.colSpan}
|
||||
style={{
|
||||
minWidth: header.getSize(),
|
||||
maxWidth: header.getSize(),
|
||||
}}
|
||||
>
|
||||
{header.isPlaceholder ? null : header.renderHeader()}
|
||||
{header.column.getCanResize() && (
|
||||
<ResizeHandle
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
/>
|
||||
)}
|
||||
</chakra.th>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
const ResizeHandle = (props: BoxProps) => {
|
||||
return (
|
||||
<Box
|
||||
pos="absolute"
|
||||
right="-5px"
|
||||
w="10px"
|
||||
h="full"
|
||||
top="0"
|
||||
cursor="col-resize"
|
||||
zIndex={2}
|
||||
userSelect="none"
|
||||
data-testid="resize-handle"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
@ -8,20 +8,20 @@ type LoadingRowsProps = {
|
||||
export const LoadingRows = ({ totalColumns }: LoadingRowsProps) => {
|
||||
return (
|
||||
<>
|
||||
{Array.from(Array(3)).map((row, idx) => (
|
||||
{Array.from(Array(3)).map((_, idx) => (
|
||||
<tr key={idx}>
|
||||
<chakra.td
|
||||
px="4"
|
||||
px="2"
|
||||
py="2"
|
||||
border="1px"
|
||||
borderColor="gray.200"
|
||||
width="50px"
|
||||
width="40px"
|
||||
>
|
||||
<Flex>
|
||||
<Flex ml="1">
|
||||
<Checkbox isDisabled />
|
||||
</Flex>
|
||||
</chakra.td>
|
||||
{Array.from(Array(totalColumns)).map((cell, idx) => {
|
||||
{Array.from(Array(totalColumns)).map((_, idx) => {
|
||||
return (
|
||||
<chakra.td
|
||||
key={idx}
|
@ -0,0 +1,175 @@
|
||||
import {
|
||||
HStack,
|
||||
Button,
|
||||
Fade,
|
||||
Tag,
|
||||
Text,
|
||||
useDisclosure,
|
||||
StackProps,
|
||||
} from '@chakra-ui/react'
|
||||
import { DownloadIcon, TrashIcon } from 'assets/icons'
|
||||
import { ConfirmModal } from 'components/modals/ConfirmModal'
|
||||
import { useToast } from 'components/shared/hooks/useToast'
|
||||
import { useResults } from 'contexts/ResultsProvider'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { unparse } from 'papaparse'
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
convertResultsToTableData,
|
||||
getAllResults,
|
||||
deleteResults as deleteFetchResults,
|
||||
} from 'services/typebots'
|
||||
|
||||
type ResultsActionButtonsProps = {
|
||||
selectedResultsId: string[]
|
||||
onClearSelection: () => void
|
||||
}
|
||||
|
||||
export const ResultsActionButtons = ({
|
||||
selectedResultsId,
|
||||
onClearSelection,
|
||||
...props
|
||||
}: ResultsActionButtonsProps & StackProps) => {
|
||||
const { typebot } = useTypebot()
|
||||
const { showToast } = useToast()
|
||||
const {
|
||||
resultsList: data,
|
||||
flatResults: results,
|
||||
resultHeader,
|
||||
mutate,
|
||||
totalResults,
|
||||
totalHiddenResults,
|
||||
tableData,
|
||||
onDeleteResults,
|
||||
} = useResults()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false)
|
||||
const [isExportLoading, setIsExportLoading] = useState(false)
|
||||
|
||||
const workspaceId = typebot?.workspaceId
|
||||
const typebotId = typebot?.id
|
||||
|
||||
const getAllTableData = async () => {
|
||||
if (!workspaceId || !typebotId) return []
|
||||
const results = await getAllResults(workspaceId, typebotId)
|
||||
return convertResultsToTableData(results, resultHeader)
|
||||
}
|
||||
|
||||
const totalSelected =
|
||||
selectedResultsId.length > 0 && selectedResultsId.length === results?.length
|
||||
? totalResults - (totalHiddenResults ?? 0)
|
||||
: selectedResultsId.length
|
||||
|
||||
const deleteResults = async () => {
|
||||
if (!workspaceId || !typebotId) return
|
||||
setIsDeleteLoading(true)
|
||||
const { error } = await deleteFetchResults(
|
||||
workspaceId,
|
||||
typebotId,
|
||||
totalSelected === totalResults ? [] : selectedResultsId
|
||||
)
|
||||
if (error) showToast({ description: error.message, title: error.name })
|
||||
else {
|
||||
mutate(
|
||||
totalSelected === totalResults
|
||||
? []
|
||||
: data?.map((d) => ({
|
||||
results: d.results.filter(
|
||||
(r) => !selectedResultsId.includes(r.id)
|
||||
),
|
||||
}))
|
||||
)
|
||||
}
|
||||
onDeleteResults(selectedResultsId.length)
|
||||
onClearSelection()
|
||||
setIsDeleteLoading(false)
|
||||
}
|
||||
|
||||
const exportResultsToCSV = async () => {
|
||||
setIsExportLoading(true)
|
||||
const isSelectAll =
|
||||
totalSelected === 0 ||
|
||||
totalSelected === totalResults - (totalHiddenResults ?? 0)
|
||||
|
||||
const dataToUnparse = isSelectAll
|
||||
? await getAllTableData()
|
||||
: tableData.filter((data) => selectedResultsId.includes(data.id))
|
||||
const csvData = new Blob(
|
||||
[
|
||||
unparse({
|
||||
data: dataToUnparse.map<{ [key: string]: string }>((data) => {
|
||||
const newObject: { [key: string]: string } = {}
|
||||
Object.keys(data).forEach((key) => {
|
||||
newObject[key] = (data[key] as { plainText: string })
|
||||
.plainText as string
|
||||
})
|
||||
return newObject
|
||||
}),
|
||||
fields: resultHeader.map((h) => h.label),
|
||||
}),
|
||||
],
|
||||
{
|
||||
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)
|
||||
}
|
||||
return (
|
||||
<HStack {...props}>
|
||||
<HStack
|
||||
as={Button}
|
||||
onClick={exportResultsToCSV}
|
||||
isLoading={isExportLoading}
|
||||
>
|
||||
<DownloadIcon />
|
||||
<Text>Export {totalSelected > 0 ? '' : 'all'}</Text>
|
||||
|
||||
{totalSelected && (
|
||||
<Tag variant="solid" size="sm">
|
||||
{totalSelected}
|
||||
</Tag>
|
||||
)}
|
||||
</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={deleteResults}
|
||||
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>
|
||||
)
|
||||
}
|
277
apps/builder/components/results/ResultsTable/ResultsTable.tsx
Normal file
277
apps/builder/components/results/ResultsTable/ResultsTable.tsx
Normal file
@ -0,0 +1,277 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
chakra,
|
||||
Checkbox,
|
||||
Flex,
|
||||
HStack,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { AlignLeftTextIcon, CalendarIcon, CodeIcon } from 'assets/icons'
|
||||
import { ResultHeaderCell, ResultsTablePreferences } from 'models'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { LoadingRows } from './LoadingRows'
|
||||
import {
|
||||
createTable,
|
||||
useTableInstance,
|
||||
getCoreRowModel,
|
||||
ColumnOrderState,
|
||||
} from '@tanstack/react-table'
|
||||
import { BlockIcon } from 'components/editor/BlocksSideBar/BlockIcon'
|
||||
import { ColumnSettingsButton } from './ColumnsSettingsButton'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { ResultsActionButtons } from './ResultsActionButtons'
|
||||
import Row from './Row'
|
||||
import { HeaderRow } from './HeaderRow'
|
||||
|
||||
type RowType = {
|
||||
id: string
|
||||
[key: string]:
|
||||
| {
|
||||
plainText: string
|
||||
element?: JSX.Element | undefined
|
||||
}
|
||||
| string
|
||||
}
|
||||
const table = createTable().setRowType<RowType>()
|
||||
|
||||
type ResultsTableProps = {
|
||||
resultHeader: ResultHeaderCell[]
|
||||
data: RowType[]
|
||||
hasMore?: boolean
|
||||
preferences?: ResultsTablePreferences
|
||||
onScrollToBottom: () => void
|
||||
onLogOpenIndex: (index: number) => () => void
|
||||
onResultExpandIndex: (index: number) => () => void
|
||||
}
|
||||
|
||||
export const ResultsTable = ({
|
||||
resultHeader,
|
||||
data,
|
||||
hasMore,
|
||||
preferences,
|
||||
onScrollToBottom,
|
||||
onLogOpenIndex,
|
||||
onResultExpandIndex,
|
||||
}: ResultsTableProps) => {
|
||||
const { updateTypebot } = useTypebot()
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({})
|
||||
const [columnsVisibility, setColumnsVisibility] = useState<
|
||||
Record<string, boolean>
|
||||
>(preferences?.columnsVisibility || {})
|
||||
const [columnsWidth, setColumnsWidth] = useState<Record<string, number>>(
|
||||
preferences?.columnsWidth || {}
|
||||
)
|
||||
const [debouncedColumnsWidth] = useDebounce(columnsWidth, 500)
|
||||
const [columnsOrder, setColumnsOrder] = useState<ColumnOrderState>([
|
||||
'select',
|
||||
...(preferences?.columnsOrder
|
||||
? resultHeader
|
||||
.map((h) => h.id)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
preferences?.columnsOrder.indexOf(a) -
|
||||
preferences?.columnsOrder.indexOf(b)
|
||||
)
|
||||
: resultHeader.map((h) => h.id)),
|
||||
'logs',
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
updateTypebot({
|
||||
resultsTablePreferences: {
|
||||
columnsVisibility,
|
||||
columnsOrder,
|
||||
columnsWidth: debouncedColumnsWidth,
|
||||
},
|
||||
})
|
||||
}, [columnsOrder, columnsVisibility, debouncedColumnsWidth, updateTypebot])
|
||||
|
||||
const bottomElement = useRef<HTMLDivElement | null>(null)
|
||||
const tableWrapper = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const columns = React.useMemo(
|
||||
() => [
|
||||
table.createDisplayColumn({
|
||||
id: 'select',
|
||||
enableResizing: false,
|
||||
maxSize: 40,
|
||||
header: ({ instance }) => (
|
||||
<IndeterminateCheckbox
|
||||
{...{
|
||||
checked: instance.getIsAllRowsSelected(),
|
||||
indeterminate: instance.getIsSomeRowsSelected(),
|
||||
onChange: instance.getToggleAllRowsSelectedHandler(),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="px-1">
|
||||
<IndeterminateCheckbox
|
||||
{...{
|
||||
checked: row.getIsSelected(),
|
||||
indeterminate: row.getIsSomeSelected(),
|
||||
onChange: row.getToggleSelectedHandler(),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
...resultHeader.map((header) =>
|
||||
table.createDataColumn(header.label, {
|
||||
id: header.id,
|
||||
size: header.isLong ? 400 : 200,
|
||||
cell: (info) => {
|
||||
const value = info.getValue()
|
||||
if (!value) return
|
||||
if (typeof value === 'string') return ''
|
||||
return value.element || value.plainText || ''
|
||||
},
|
||||
header: () => (
|
||||
<HStack overflow="hidden" data-testid={`${header.label} header`}>
|
||||
<HeaderIcon header={header} />
|
||||
<Text>{header.label}</Text>
|
||||
</HStack>
|
||||
),
|
||||
})
|
||||
),
|
||||
table.createDisplayColumn({
|
||||
id: 'logs',
|
||||
enableResizing: false,
|
||||
maxSize: 110,
|
||||
header: () => (
|
||||
<HStack>
|
||||
<AlignLeftTextIcon />
|
||||
<Text>Logs</Text>
|
||||
</HStack>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Button size="sm" onClick={onLogOpenIndex(row.index)}>
|
||||
See logs
|
||||
</Button>
|
||||
),
|
||||
}),
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[resultHeader]
|
||||
)
|
||||
|
||||
const instance = useTableInstance(table, {
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
columnVisibility: columnsVisibility,
|
||||
columnOrder: columnsOrder,
|
||||
columnSizing: columnsWidth,
|
||||
},
|
||||
getRowId: (row) => row.id,
|
||||
columnResizeMode: 'onChange',
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onColumnVisibilityChange: setColumnsVisibility,
|
||||
onColumnSizingChange: setColumnsWidth,
|
||||
onColumnOrderChange: setColumnsOrder,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!bottomElement.current) return
|
||||
const options: IntersectionObserverInit = {
|
||||
root: tableWrapper.current,
|
||||
threshold: 0,
|
||||
}
|
||||
const observer = new IntersectionObserver(handleObserver, options)
|
||||
if (bottomElement.current) observer.observe(bottomElement.current)
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bottomElement.current])
|
||||
|
||||
const handleObserver = (entities: IntersectionObserverEntry[]) => {
|
||||
const target = entities[0]
|
||||
if (target.isIntersecting) onScrollToBottom()
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
maxW="1600px"
|
||||
px="4"
|
||||
overflow="scroll"
|
||||
spacing={6}
|
||||
ref={tableWrapper}
|
||||
>
|
||||
<Flex w="full" justifyContent="flex-end">
|
||||
<ResultsActionButtons
|
||||
selectedResultsId={Object.keys(rowSelection)}
|
||||
onClearSelection={() => setRowSelection({})}
|
||||
mr="2"
|
||||
/>
|
||||
<ColumnSettingsButton
|
||||
resultHeader={resultHeader}
|
||||
columnVisibility={columnsVisibility}
|
||||
setColumnVisibility={setColumnsVisibility}
|
||||
columnOrder={columnsOrder}
|
||||
onColumnOrderChange={instance.setColumnOrder}
|
||||
/>
|
||||
</Flex>
|
||||
<Box className="table-wrapper" overflow="scroll" rounded="md">
|
||||
<chakra.table rounded="md">
|
||||
<thead>
|
||||
{instance.getHeaderGroups().map((headerGroup) => (
|
||||
<HeaderRow key={headerGroup.id} headerGroup={headerGroup} />
|
||||
))}
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{instance.getRowModel().rows.map((row, rowIndex) => (
|
||||
<Row
|
||||
row={row}
|
||||
key={row.id}
|
||||
bottomElement={
|
||||
rowIndex === data.length - 10 ? bottomElement : undefined
|
||||
}
|
||||
isSelected={row.getIsSelected()}
|
||||
onExpandButtonClick={onResultExpandIndex(rowIndex)}
|
||||
/>
|
||||
))}
|
||||
{hasMore === true && (
|
||||
<LoadingRows totalColumns={columns.length - 1} />
|
||||
)}
|
||||
</tbody>
|
||||
</chakra.table>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const IndeterminateCheckbox = React.forwardRef(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
({ indeterminate, checked, ...rest }: any, ref) => {
|
||||
const defaultRef = React.useRef()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const resolvedRef: any = ref || defaultRef
|
||||
|
||||
return (
|
||||
<Flex justify="center" data-testid="checkbox">
|
||||
<Checkbox
|
||||
ref={resolvedRef}
|
||||
{...rest}
|
||||
isIndeterminate={indeterminate}
|
||||
isChecked={checked}
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export const HeaderIcon = ({ header }: { header: ResultHeaderCell }) =>
|
||||
header.blockType ? (
|
||||
<BlockIcon type={header.blockType} />
|
||||
) : header.variableId ? (
|
||||
<CodeIcon />
|
||||
) : (
|
||||
<CalendarIcon />
|
||||
)
|
73
apps/builder/components/results/ResultsTable/Row.tsx
Normal file
73
apps/builder/components/results/ResultsTable/Row.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React, { memo, useState } from 'react'
|
||||
import { Row as RowProps } from '@tanstack/react-table'
|
||||
import { Button, chakra, Fade } from '@chakra-ui/react'
|
||||
import { ExpandIcon } from 'assets/icons'
|
||||
|
||||
type Props = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
row: RowProps<any>
|
||||
isSelected: boolean
|
||||
bottomElement?: React.MutableRefObject<HTMLDivElement | null>
|
||||
onExpandButtonClick: () => void
|
||||
}
|
||||
|
||||
const Row = ({ row, bottomElement, onExpandButtonClick }: Props) => {
|
||||
const [isExpandButtonVisible, setIsExpandButtonVisible] = useState(false)
|
||||
|
||||
const showExpandButton = () => setIsExpandButtonVisible(true)
|
||||
const hideExpandButton = () => setIsExpandButtonVisible(false)
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
data-rowid={row.id}
|
||||
ref={(ref) => {
|
||||
if (bottomElement && bottomElement.current?.dataset.rowid !== row.id)
|
||||
bottomElement.current = ref
|
||||
}}
|
||||
onMouseEnter={showExpandButton}
|
||||
onClick={showExpandButton}
|
||||
onMouseLeave={hideExpandButton}
|
||||
>
|
||||
{row.getVisibleCells().map((cell, cellIndex) => (
|
||||
<chakra.td
|
||||
key={cell.id}
|
||||
px="4"
|
||||
py="2"
|
||||
border="1px"
|
||||
borderColor="gray.200"
|
||||
whiteSpace="nowrap"
|
||||
wordBreak="normal"
|
||||
overflow="hidden"
|
||||
pos="relative"
|
||||
>
|
||||
{cell.renderCell()}
|
||||
<chakra.span
|
||||
pos="absolute"
|
||||
top="0"
|
||||
right={2}
|
||||
h="full"
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<Fade unmountOnExit in={isExpandButtonVisible && cellIndex === 1}>
|
||||
<Button
|
||||
leftIcon={<ExpandIcon />}
|
||||
shadow="lg"
|
||||
size="xs"
|
||||
onClick={onExpandButtonClick}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
</Fade>
|
||||
</chakra.span>
|
||||
</chakra.td>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(
|
||||
Row,
|
||||
(prev, next) =>
|
||||
prev.row.id === next.row.id && prev.isSelected === next.isSelected
|
||||
)
|
1
apps/builder/components/results/ResultsTable/index.tsx
Normal file
1
apps/builder/components/results/ResultsTable/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { ResultsTable as SubmissionsTable } from './ResultsTable'
|
@ -1,188 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable react/jsx-key */
|
||||
import { Button, chakra, Checkbox, Flex, HStack, Text } from '@chakra-ui/react'
|
||||
import { AlignLeftTextIcon } from 'assets/icons'
|
||||
import { ResultHeaderCell } from 'models'
|
||||
import React, { useEffect, useMemo, useRef } from 'react'
|
||||
import { Hooks, useRowSelect, useTable } from 'react-table'
|
||||
import { parseSubmissionsColumns } from 'services/typebots'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { LoadingRows } from './LoadingRows'
|
||||
|
||||
type SubmissionsTableProps = {
|
||||
resultHeader: ResultHeaderCell[]
|
||||
data?: any
|
||||
hasMore?: boolean
|
||||
onNewSelection: (indices: number[]) => void
|
||||
onScrollToBottom: () => void
|
||||
onLogOpenIndex: (index: number) => () => void
|
||||
}
|
||||
|
||||
export const SubmissionsTable = ({
|
||||
resultHeader,
|
||||
data,
|
||||
hasMore,
|
||||
onNewSelection,
|
||||
onScrollToBottom,
|
||||
onLogOpenIndex,
|
||||
}: SubmissionsTableProps) => {
|
||||
const columns: any = useMemo(
|
||||
() => parseSubmissionsColumns(resultHeader),
|
||||
[resultHeader]
|
||||
)
|
||||
const bottomElement = useRef<HTMLDivElement | null>(null)
|
||||
const tableWrapper = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
getTableBodyProps,
|
||||
selectedFlatRows,
|
||||
} = useTable({ columns, data }, useRowSelect, checkboxColumnHook) as any
|
||||
|
||||
useEffect(() => {
|
||||
onNewSelection(selectedFlatRows.map((row: any) => row.index))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedFlatRows])
|
||||
|
||||
useEffect(() => {
|
||||
if (!bottomElement.current) return
|
||||
const options: IntersectionObserverInit = {
|
||||
root: tableWrapper.current,
|
||||
threshold: 0,
|
||||
}
|
||||
const observer = new IntersectionObserver(handleObserver, options)
|
||||
if (bottomElement.current) observer.observe(bottomElement.current)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bottomElement.current])
|
||||
|
||||
const handleObserver = (entities: any[]) => {
|
||||
const target = entities[0]
|
||||
if (target.isIntersecting) onScrollToBottom()
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
maxW="full"
|
||||
overflow="scroll"
|
||||
ref={tableWrapper}
|
||||
className="table-wrapper"
|
||||
rounded="md"
|
||||
>
|
||||
<chakra.table rounded="md" {...getTableProps()}>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup: any) => {
|
||||
return (
|
||||
<tr {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map((column: any) => {
|
||||
return (
|
||||
<chakra.th
|
||||
px="4"
|
||||
py="2"
|
||||
border="1px"
|
||||
borderColor="gray.200"
|
||||
fontWeight="normal"
|
||||
whiteSpace="nowrap"
|
||||
{...column.getHeaderProps()}
|
||||
>
|
||||
{column.render('Header')}
|
||||
</chakra.th>
|
||||
)
|
||||
})}
|
||||
<chakra.th
|
||||
px="4"
|
||||
py="2"
|
||||
border="1px"
|
||||
borderColor="gray.200"
|
||||
fontWeight="normal"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
<HStack>
|
||||
<AlignLeftTextIcon />
|
||||
<Text>Logs</Text>
|
||||
</HStack>
|
||||
</chakra.th>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</thead>
|
||||
|
||||
<tbody {...getTableBodyProps()}>
|
||||
{rows.map((row: any, idx: number) => {
|
||||
prepareRow(row)
|
||||
return (
|
||||
<tr
|
||||
{...row.getRowProps()}
|
||||
ref={(ref) => {
|
||||
if (idx === data.length - 10) bottomElement.current = ref
|
||||
}}
|
||||
>
|
||||
{row.cells.map((cell: any) => {
|
||||
return (
|
||||
<chakra.td
|
||||
px="4"
|
||||
py="2"
|
||||
border="1px"
|
||||
borderColor="gray.200"
|
||||
whiteSpace={
|
||||
cell?.value?.length > 100 ? 'normal' : 'nowrap'
|
||||
}
|
||||
{...cell.getCellProps()}
|
||||
>
|
||||
{isNotDefined(cell.value)
|
||||
? cell.render('Cell')
|
||||
: cell.value.element ?? cell.value.plainText}
|
||||
</chakra.td>
|
||||
)
|
||||
})}
|
||||
<chakra.td px="4" py="2" border="1px" borderColor="gray.200">
|
||||
<Button size="sm" onClick={onLogOpenIndex(idx)}>
|
||||
See logs
|
||||
</Button>
|
||||
</chakra.td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
{hasMore === true && (
|
||||
<LoadingRows totalColumns={columns.length + 1} />
|
||||
)}
|
||||
</tbody>
|
||||
</chakra.table>
|
||||
</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 (
|
||||
<Flex justify="center" data-testid="checkbox">
|
||||
<Checkbox
|
||||
ref={resolvedRef}
|
||||
{...rest}
|
||||
isIndeterminate={indeterminate}
|
||||
isChecked={checked}
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
)
|
@ -1 +0,0 @@
|
||||
export { SubmissionsTable } from './SubmissionsTable'
|
Reference in New Issue
Block a user