/* eslint-disable no-unused-vars */ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useMemo, useState, useEffect, useCallback } from 'react'; import { useTable, useFilters, useSortBy, useGlobalFilter, useAsyncDebounce, usePagination, useBlockLayout, useResizeColumns, useRowSelect, } from 'react-table'; import cx from 'classnames'; import { resolveReferences, resolveWidgetFieldValue, validateWidget } from '@/_helpers/utils'; import SelectSearch, { fuzzySearch } from 'react-select-search'; import { useExportData } from 'react-table-plugins'; import Papa from 'papaparse'; import { Pagination } from './Pagination'; import { CustomSelect } from './CustomSelect'; import { Tags } from './Tags'; import { Radio } from './Radio'; import { Toggle } from './Toggle'; import { Datepicker } from './Datepicker'; import { GlobalFilter } from './GlobalFilter'; var _ = require('lodash'); export function Table({ id, width, height, component, onComponentClick, currentState = { components: {} }, onEvent, paramUpdated, changeCanDrag, onComponentOptionChanged, onComponentOptionsChanged, darkMode, fireEvent, setExposedVariable, registerAction, }) { const color = component.definition.styles.textColor.value !== '#000' ? component.definition.styles.textColor.value : darkMode && '#fff'; const actions = component.definition.properties.actions || { value: [] }; const serverSidePaginationProperty = component.definition.properties.serverSidePagination; const serverSidePagination = serverSidePaginationProperty ? resolveWidgetFieldValue(serverSidePaginationProperty.value, currentState) : false; const serverSideSearchProperty = component.definition.properties.serverSideSearch; const serverSideSearch = serverSideSearchProperty ? resolveWidgetFieldValue(serverSideSearchProperty.value, currentState) : false; const displaySearchBoxProperty = component.definition.properties.displaySearchBox; const displaySearchBox = displaySearchBoxProperty ? resolveWidgetFieldValue(displaySearchBoxProperty.value, currentState) : true; const showDownloadButtonProperty = component.definition.properties.showDownloadButton?.value; const showDownloadButton = resolveWidgetFieldValue(showDownloadButtonProperty, currentState) ?? true; // default is true for backward compatibility const showFilterButtonProperty = component.definition.properties.showFilterButton?.value; const showFilterButton = resolveWidgetFieldValue(showFilterButtonProperty, currentState) ?? true; // default is true for backward compatibility const showBulkUpdateActionsProperty = component.definition.properties.showBulkUpdateActions?.value; const showBulkUpdateActions = resolveWidgetFieldValue(showBulkUpdateActionsProperty, currentState) ?? true; // default is true for backward compatibility const showBulkSelectorProperty = component.definition.properties.showBulkSelector?.value; const showBulkSelector = resolveWidgetFieldValue(showBulkSelectorProperty, currentState) ?? false; // default is false for backward compatibility const highlightSelectedRowProperty = component.definition.properties.highlightSelectedRow?.value; const highlightSelectedRow = resolveWidgetFieldValue(highlightSelectedRowProperty, currentState) ?? false; // default is false for backward compatibility const clientSidePaginationProperty = component.definition.properties.clientSidePagination?.value; const clientSidePagination = resolveWidgetFieldValue(clientSidePaginationProperty, currentState) ?? !serverSidePagination; // default is true for backward compatibility const tableTypeProperty = component.definition.styles.tableType; let tableType = tableTypeProperty ? tableTypeProperty.value : 'table-bordered'; tableType = tableType === '' ? 'table-bordered' : tableType; const cellSizeType = component.definition.styles.cellSize?.value; const borderRadius = component.definition.styles.borderRadius?.value; const widgetVisibility = component.definition.styles?.visibility?.value ?? true; const disabledState = component.definition.styles?.disabledState?.value ?? false; const parsedDisabledState = typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState; let parsedWidgetVisibility = widgetVisibility; try { parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []); } catch (err) { console.log(err); } const [loadingState, setLoadingState] = useState(false); useEffect(() => { const loadingStateProperty = component.definition.properties.loadingState; if (loadingStateProperty && currentState) { const newState = resolveReferences(loadingStateProperty.value, currentState, false); setLoadingState(newState); } }, [currentState]); const [componentState, setcomponentState] = useState(currentState.components[component.component] || {}); useEffect(() => { setcomponentState(currentState.components[component.name] || {}); }, [currentState.components[component.name]]); const [isFiltersVisible, setFiltersVisibility] = useState(false); const [filters, setFilters] = useState([]); function showFilters() { setFiltersVisibility(true); } function hideFilters() { setFiltersVisibility(false); } function filterColumnChanged(index, value) { const newFilters = filters; newFilters[index].id = value; setFilters(newFilters); setAllFilters(newFilters.filter((filter) => filter.id !== '')); } function filterOperationChanged(index, value) { const newFilters = filters; newFilters[index].value = { ...newFilters[index].value, operation: value, }; setFilters(newFilters); setAllFilters(newFilters.filter((filter) => filter.id !== '')); } function filterValueChanged(index, value) { const newFilters = filters; newFilters[index].value = { ...newFilters[index].value, value: value, }; setFilters(newFilters); setAllFilters(newFilters.filter((filter) => filter.id !== '')); } function addFilter() { setFilters([...filters, { id: '', value: { operation: 'contains', value: '' } }]); } function removeFilter(index) { let newFilters = filters; newFilters.splice(index, 1); setFilters(newFilters); setAllFilters(newFilters.filter((filter) => filter.id !== '')); } function clearFilters() { setFilters([]); setAllFilters([]); } const defaultColumn = React.useMemo( () => ({ minWidth: 60, width: 268, }), [] ); const columnSizes = component.definition.properties.columnSizes || {}; function handleCellValueChange(index, key, value, rowData) { const changeSet = componentState.changeSet; const dataUpdates = componentState.dataUpdates || []; let obj = changeSet ? changeSet[index] || {} : {}; obj = _.set(obj, key, value); let newChangeset = { ...changeSet, [index]: { ...obj, }, }; obj = _.set(rowData, key, value); let newDataUpdates = { ...dataUpdates, [index]: { ...obj }, }; return onComponentOptionsChanged(component, [ ['dataUpdates', newDataUpdates], ['changeSet', newChangeset], ]); } function getExportFileBlob({ columns, data }) { const headerNames = columns.map((col) => col.exportValue); const csvString = Papa.unparse({ fields: headerNames, data }); return new Blob([csvString], { type: 'text/csv' }); } function onPageIndexChanged(page) { onComponentOptionChanged(component, 'pageIndex', page).then(() => { onEvent('onPageChanged', { component, data: {} }); }); } function handleChangesSaved() { Object.keys(changeSet).forEach((key) => { tableData[key] = { ..._.merge(tableData[key], changeSet[key]), }; }); onComponentOptionChanged(component, 'changeSet', {}); onComponentOptionChanged(component, 'dataUpdates', []); } function handleChangesDiscarded() { onComponentOptionChanged(component, 'changeSet', {}); onComponentOptionChanged(component, 'dataUpdates', []); } function customFilter(rows, columnIds, filterValue) { try { if (filterValue.operation === 'equals') { return rows.filter((row) => row.values[columnIds[0]] === filterValue.value); } if (filterValue.operation === 'ne') { return rows.filter((row) => row.values[columnIds[0]] !== filterValue.value); } if (filterValue.operation === 'matches') { return rows.filter((row) => row.values[columnIds[0]].toString().toLowerCase().includes(filterValue.value.toLowerCase()) ); } if (filterValue.operation === 'nl') { return rows.filter((row) => !row.values[columnIds[0]].toString().toLowerCase().includes(filterValue.value.toLowerCase()) ); } if (filterValue.operation === 'gt') { return rows.filter((row) => row.values[columnIds[0]] > filterValue.value); } if (filterValue.operation === 'lt') { return rows.filter((row) => row.values[columnIds[0]] < filterValue.value); } if (filterValue.operation === 'gte') { return rows.filter((row) => row.values[columnIds[0]] >= filterValue.value); } if (filterValue.operation === 'lte') { return rows.filter((row) => row.values[columnIds[0]] <= filterValue.value); } let value = filterValue.value; if (typeof value === 'string') { value = value.toLowerCase(); } return rows.filter((row) => { let rowValue = row.values[columnIds[0]]; if (typeof rowValue === 'string') { rowValue = rowValue.toLowerCase(); } return rowValue.includes(value); }); } catch { return rows; } } const changeSet = componentState ? componentState.changeSet : {}; const computeFontColor = useCallback(() => { if (color !== undefined) { return color; } else { return darkMode ? '#ffffff' : '#000000'; } }, [color, darkMode]); const columnData = component.definition.properties.columns.value.map((column) => { const columnSize = columnSizes[column.id] || columnSizes[column.name]; const columnType = column.columnType; const columnOptions = {}; if ( columnType === 'dropdown' || columnType === 'multiselect' || columnType === 'badge' || columnType === 'badges' || columnType === 'radio' ) { const values = resolveReferences(column.values, currentState) || []; const labels = resolveReferences(column.labels, currentState, []) || []; if (Array.isArray(labels)) { columnOptions.selectOptions = labels.map((label, index) => { return { name: label, value: values[index] }; }); } } if (columnType === 'datepicker') { column.isTimeChecked = column.isTimeChecked ? column.isTimeChecked : false; column.dateFormat = column.dateFormat ? column.dateFormat : 'DD/MM/YYYY'; column.parseDateFormat = column.parseDateFormat ?? column.dateFormat; //backwards compatibility } const width = columnSize || defaultColumn.width; return { id: column.id, Header: column.name, accessor: column.key || column.name, filter: customFilter, width: width, columnOptions, columnType, isEditable: column.isEditable, Cell: function (cell) { const rowChangeSet = changeSet ? changeSet[cell.row.index] : null; const cellValue = rowChangeSet ? rowChangeSet[column.name] || cell.value : cell.value; switch (columnType) { case 'string': case undefined: case 'default': { const textColor = resolveReferences(column.textColor, currentState, '', { cellValue }); const cellStyles = { color: textColor ?? '', }; if (column.isEditable) { const validationData = validateWidget({ validationObject: { regex: { value: column.regex, }, minLength: { value: column.minLength, }, maxLength: { value: column.maxLength, }, customRule: { value: column.customRule, }, }, widgetValue: cellValue, currentState, customResolveObjects: { cellValue }, }); const { isValid, validationError } = validationData; const cellStyles = { color: textColor ?? '', }; return (
{ if (e.key === 'Enter') { if (e.target.defaultValue !== e.target.value) { handleCellValueChange( cell.row.index, column.key || column.name, e.target.value, cell.row.original ); } } }} onBlur={(e) => { if (e.target.defaultValue !== e.target.value) { handleCellValueChange( cell.row.index, column.key || column.name, e.target.value, cell.row.original ); } }} className={`form-control-plaintext form-control-plaintext-sm ${!isValid ? 'is-invalid' : ''}`} defaultValue={cellValue} />
{validationError}
); } return {cellValue}; } case 'text': { return ( ); } case 'dropdown': { const validationData = validateWidget({ validationObject: { regex: { value: column.regex, }, minLength: { value: column.minLength, }, maxLength: { value: column.maxLength, }, customRule: { value: column.customRule, }, }, widgetValue: cellValue, currentState, customResolveObjects: { cellValue }, }); const { isValid, validationError } = validationData; return (
{ handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original); }} filterOptions={fuzzySearch} placeholder="Select.." />
{validationError}
); } case 'multiselect': { return (
{ handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original); }} />
); } case 'badge': case 'badges': { return (
{ handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original); }} />
); } case 'tags': { return (
{ handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original); }} />
); } case 'radio': { return (
{ handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original); }} />
); } case 'toggle': { return (
{ handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original).then( () => { fireEvent('OnTableToggleCellChanged', { column: column, rowId: cell.row.id, row: cell.row.original, }); } ); }} />
); } case 'datepicker': { return (
{ handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original); }} />
); } } return cellValue || ''; }, }; }); let tableData = []; if (currentState) { tableData = resolveReferences(component.definition.properties.data.value, currentState, []); if (!Array.isArray(tableData)) tableData = []; console.log('resolved param', tableData); } tableData = tableData || []; const leftActions = () => actions.value.filter((action) => action.position === 'left'); const rightActions = () => actions.value.filter((action) => [undefined, 'right'].includes(action.position)); const leftActionsCellData = leftActions().length > 0 ? [ { id: 'leftActions', Header: 'Actions', accessor: 'edit', width: columnSizes.leftActions || defaultColumn.width, Cell: (cell) => { return leftActions().map((action) => ( )); }, }, ] : []; const rightActionsCellData = rightActions().length > 0 ? [ { id: 'rightActions', Header: 'Actions', accessor: 'edit', width: columnSizes.rightActions || defaultColumn.width, Cell: (cell) => { return rightActions().map((action) => ( )); }, }, ] : []; const IndeterminateCheckbox = React.forwardRef(({ indeterminate, ...rest }, ref) => { const defaultRef = React.useRef(); const resolvedRef = ref || defaultRef; React.useEffect(() => { resolvedRef.current.indeterminate = indeterminate; }, [resolvedRef, indeterminate]); return ( <> event.stopPropagation()} {...rest} /> ); }); const optionsData = columnData.map((column) => column.columnOptions?.selectOptions); const columns = useMemo( () => [...leftActionsCellData, ...columnData, ...rightActionsCellData], [ JSON.stringify(columnData), leftActionsCellData.length, rightActionsCellData.length, componentState.changeSet, JSON.stringify(optionsData), JSON.stringify(component.definition.properties.columns), showBulkSelector, ] // Hack: need to fix ); const data = useMemo( () => tableData, [tableData.length, componentState.changeSet, component.definition.properties.data.value] ); const computedStyles = { // width: `${width}px`, }; const { getTableProps, getTableBodyProps, headerGroups, page, canPreviousPage, canNextPage, pageOptions, gotoPage, pageCount, nextPage, previousPage, setPageSize, state, rows, prepareRow, setAllFilters, preGlobalFilteredRows, setGlobalFilter, state: { pageIndex, pageSize }, exportData, selectedFlatRows, } = useTable( { autoResetPage: false, columns, data, defaultColumn, initialState: { pageIndex: 0, pageSize: -1 }, pageCount: -1, manualPagination: false, getExportFileBlob, }, useFilters, useGlobalFilter, useSortBy, usePagination, useBlockLayout, useResizeColumns, useExportData, useRowSelect, (hooks) => { showBulkSelector && hooks.visibleColumns.push((columns) => [ { id: 'selection', Header: ({ getToggleAllPageRowsSelectedProps }) => (
), Cell: ({ row }) => (
), width: 1, columnType: 'selector', }, ...columns, ]); } ); const registerSetPageAction = () => { registerAction('setPage', (targetPageIndex) => { setPaginationInternalPageIndex(targetPageIndex); setExposedVariable('pageIndex', targetPageIndex); if (!serverSidePagination && clientSidePagination) gotoPage(targetPageIndex - 1); }); }; useEffect(registerSetPageAction, []); useEffect(registerSetPageAction, [serverSidePagination, clientSidePagination]); useEffect(() => { const selectedRowsOriginalData = selectedFlatRows.map((row) => row.original); onComponentOptionChanged(component, 'selectedRows', selectedRowsOriginalData); }, [selectedFlatRows.length]); React.useEffect(() => { if (serverSidePagination || !clientSidePagination) { setPageSize(rows?.length || 10); } if (!serverSidePagination && clientSidePagination) { setPageSize(10); } }, [clientSidePagination, serverSidePagination, rows]); useEffect(() => { const pageData = page.map((row) => row.original); const currentData = rows.map((row) => row.original); onComponentOptionsChanged(component, [ ['currentPageData', pageData], ['currentData', currentData], ]); }, [tableData.length, componentState.changeSet]); useEffect(() => { if (!state.columnResizing.isResizingColumn) { changeCanDrag(true); paramUpdated(id, 'columnSizes', { ...columnSizes, ...state.columnResizing.columnWidths }); } else { changeCanDrag(false); } }, [state.columnResizing.isResizingColumn]); const [paginationInternalPageIndex, setPaginationInternalPageIndex] = useState(pageIndex ?? 1); useEffect(() => { if (pageCount <= pageIndex) gotoPage(pageCount - 1); }, [pageCount]); return (
{ event.stopPropagation(); onComponentClick(id, component, event); }} > {/* Show top bar unless search box is disabled and server pagination is enabled */} {displaySearchBox && (
)}
{headerGroups.map((headerGroup, index) => ( {headerGroup.headers.map((column, index) => ( ))} ))} {!loadingState && page.length === 0 && (
no data
)} {!loadingState && ( {console.log('page', page)} {page.map((row, index) => { prepareRow(row); return ( { e.stopPropagation(); onEvent('onRowClicked', { component, data: row.original, rowId: row.id }); }} > {row.cells.map((cell, index) => { let cellProps = cell.getCellProps(); if (componentState.changeSet) { if (componentState.changeSet[cell.row.index]) { const currentColumn = columnData.find((column) => column.id === cell.column.id); if ( _.get(componentState.changeSet[cell.row.index], currentColumn?.accessor, undefined) !== undefined ) { console.log('componentState.changeSet', componentState.changeSet); cellProps.style.backgroundColor = darkMode ? '#1c252f' : '#ffffde'; cellProps.style['--tblr-table-accent-bg'] = darkMode ? '#1c252f' : '#ffffde'; } } } return ( // Does not require key as its already being passed by react-table via cellProps // eslint-disable-next-line react/jsx-key ); })} ); })} )}
{column.render('Header')}
{cell.render('Cell')}
{loadingState === true && (
)}
{(clientSidePagination || serverSidePagination || Object.keys(componentState.changeSet || {}).length > 0 || showFilterButton || showDownloadButton) && (
{(clientSidePagination || serverSidePagination) && ( )}
{showBulkUpdateActions && Object.keys(componentState.changeSet || {}).length > 0 && (
)}
{showFilterButton && ( showFilters()}> {filters.length > 0 && ( )} )} {showDownloadButton && ( exportData('csv', true)} > )}
)} {isFiltersVisible && (

Filters

{filters.map((filter, index) => (
{index > 0 ? 'and' : 'where'}
{ return { name: column.Header, value: column.id }; })} value={filter.id} search={true} onChange={(value) => { filterColumnChanged(index, value); }} filterOptions={fuzzySearch} placeholder="Select.." />
{ filterOperationChanged(index, value); }} filterOptions={fuzzySearch} placeholder="Select.." />
filterValueChanged(index, e.target.value)} />
))} {filters.length === 0 && (
no filters yet.
)}
)}
); }