feat(results): ✨ Brand new Results table
- Resizable columns - Hide / Show columns - Reorganize columns - Expand result
This commit is contained in:
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 />
|
||||
)
|
Reference in New Issue
Block a user