2
0
Files
bot/apps/builder/components/results/ResultsTable/ResultsTable.tsx
Baptiste Arnaud ee338f62dc build: add pnpm
2022-08-10 18:49:06 +02:00

266 lines
7.6 KiB
TypeScript

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 {
useReactTable,
getCoreRowModel,
ColumnOrderState,
ColumnDef,
} 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'
import { CellValueType, TableData } from 'services/typebots/results'
type ResultsTableProps = {
resultHeader: ResultHeaderCell[]
data: TableData[]
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<ColumnDef<TableData>[]>(
() => [
{
id: 'select',
enableResizing: false,
maxSize: 40,
header: ({ table }) => (
<IndeterminateCheckbox
{...{
checked: table.getIsAllRowsSelected(),
indeterminate: table.getIsSomeRowsSelected(),
onChange: table.getToggleAllRowsSelectedHandler(),
}}
/>
),
cell: ({ row }) => (
<div className="px-1">
<IndeterminateCheckbox
{...{
checked: row.getIsSelected(),
indeterminate: row.getIsSomeSelected(),
onChange: row.getToggleSelectedHandler(),
}}
/>
</div>
),
},
...resultHeader.map<ColumnDef<TableData>>((header) => ({
id: header.id,
accessorKey: header.label,
size: header.isLong ? 400 : 200,
header: () => (
<HStack overflow="hidden" data-testid={`${header.label} header`}>
<HeaderIcon header={header} />
<Text>{header.label}</Text>
</HStack>
),
cell: (info) => {
const value = info.getValue() as CellValueType | undefined
if (!value) return
return value.element || value.plainText || ''
},
})),
{
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 = useReactTable({
data,
columns,
state: {
rowSelection,
columnVisibility: columnsVisibility,
columnOrder: columnsOrder,
columnSizing: columnsWidth,
},
getRowId: (row) => row.id.plainText,
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 />
)