Tom Butcher 9d1a032e10 Refactor Dashboard Components to Remove Unused State Management
- Removed unnecessary useState hooks for view state management in multiple dashboard components (Invoices, PaymentPolicies, Payments, TaxRecords, FilamentStocks, OrderItems, PartStocks, ProductStocks, PurchaseOrders, Shipments, StockAudits, StockEvents, StockLocations, StockTransfers, AppPasswords, AuditLogs, Couriers, CourierServices, DocumentJobs, DocumentPrinters, DocumentSizes, DocumentTemplates, Filaments, FilamentSkus).
- Updated ObjectListViewProvider integration to streamline component functionality and improve maintainability.
- Adjusted CSS variables in App.css for enhanced styling consistency across components.
2026-09-02 22:41:49 +01:00

1957 lines
58 KiB
JavaScript

import {
forwardRef,
useImperativeHandle,
useRef,
useEffect,
useLayoutEffect,
useState,
useCallback,
useMemo,
createElement
} from 'react'
import { flushSync } from 'react-dom'
import {
Table,
Skeleton,
Row,
Col,
Flex,
Spin,
Button,
Space,
Form,
Splitter,
Card
} from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types'
import { useMediaQuery } from 'react-responsive'
import { useContext } from 'react'
import { ApiServerContext } from '../context/ApiServerContext'
import config from '../../../config'
import loglevel from 'loglevel'
import {
getModelProperties,
getModelByName
} from '../../../database/ObjectModels'
import ObjectProperty from './ObjectProperty'
import ObjectCard from './ObjectCard'
import FilterSidebar from './FilterSidebar'
import SortSidebar from './SortSidebar'
import CheckIcon from '../../Icons/CheckIcon'
import { useLocation } from 'react-router-dom'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
import { AuthContext } from '../context/AuthContext'
import { ElectronContext } from '../context/ElectronContext'
import { useActions } from '../context/ActionsContext'
import ActionsIcon from '../../Icons/ActionsIcon'
import FilterIcon from '../../Icons/FilterIcon'
import ScrollBox from './ScrollBox'
import SimplePropertyFilter from './SimplePropertyFilter'
import QuickPropertyFilters from './QuickPropertyFilters'
import FilterInput from './FilterInput'
import {
getActiveFilterValues,
useTableState,
useTableStatePersistence
} from '../context/TableStateContext'
import { hasActionPermission } from '../../../database/permissions'
import Tooltip from './Tooltip'
import { ObjectTableFilterContext } from './ObjectTableFilterContext'
import ObjectListViewContext from '../context/ObjectListViewContext'
const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel)
const SCROLL_THRESHOLD = 50
const SKELETON_HEIGHT = 49.5
const getCardColSpan = (containerWidth) => {
if (containerWidth >= 2980) return 2
if (containerWidth >= 1780) return 4
if (containerWidth >= 1080) return 8
if (containerWidth >= 680) return 12
return 24
}
const toFilterExpression = (values) => {
if (!values?.length) return undefined
const parts = values.map((value) => {
if (value && typeof value === 'object') {
return String(value._id ?? value.type ?? JSON.stringify(value))
}
return String(value)
})
return parts.length === 1 ? parts[0] : parts.join('|')
}
const fromFilterExpression = (expr) => {
if (expr === undefined || expr === null || expr === '') return null
if (typeof expr === 'string' && expr.includes('|')) {
return expr.split('|').filter((part) => part !== '')
}
return [expr]
}
const areValuesEqual = (v1, v2) => {
const id1 = v1 && typeof v1 === 'object' && v1._id ? v1._id : v1
const id2 = v2 && typeof v2 === 'object' && v2._id ? v2._id : v2
return String(id1) === String(id2)
}
const getChangedKeys = (existing, updated) => {
if (!updated || typeof updated !== 'object') return []
return Object.keys(updated).filter((key) => {
if (key === '_id' || key === 'objectType') return false
if (!existing) return true
return !areValuesEqual(existing[key], updated[key])
})
}
const updateAffectsFilterOrSort = (
changedKeys,
filter,
masterFilter,
sorter
) => {
if (!changedKeys.length) return false
const filterKeys = new Set([
...Object.keys(filter || {}),
...Object.keys(masterFilter || {})
])
if (changedKeys.some((key) => filterKeys.has(key))) return true
if (sorter?.field && changedKeys.includes(sorter.field)) return true
return false
}
const ColumnFilterDropdown = ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
visible,
propertyName,
propertyLabel,
modelType,
filter = {},
masterFilter = {}
}) => {
const [expression, setExpression] = useState('')
const [draft, setDraft] = useState(selectedKeys || [])
const selectedKeysRef = useRef(selectedKeys)
selectedKeysRef.current = selectedKeys
// Re-sync from the applied sidebar/column filter each time the dropdown opens
useEffect(() => {
if (!visible) return
const keys = selectedKeysRef.current || []
setDraft(keys)
setExpression(toFilterExpression(keys) ?? '')
}, [visible])
const handleDraftChange = (next) => {
setDraft(next)
setExpression(toFilterExpression(next) ?? '')
}
const handleExpressionChange = (text) => {
setExpression(text)
setDraft(fromFilterExpression(text) || [])
}
const applyFilter = () => {
const trimmed = expression.trim()
if (!trimmed) {
clearFilters()
} else if (draft?.length) {
setSelectedKeys(draft)
} else {
setSelectedKeys([trimmed])
}
confirm()
}
return (
<div style={{ padding: 8 }}>
<Flex vertical gap='small'>
<Space.Compact>
<FilterInput
placeholder={'Filter ' + propertyLabel}
value={expression}
onChange={handleExpressionChange}
onPressEnter={applyFilter}
style={{ width: 260 }}
/>
<Button type='primary' onClick={applyFilter} icon={<CheckIcon />} />
</Space.Compact>
<QuickPropertyFilters
modelType={modelType}
propertyName={propertyName}
value={draft}
onChange={handleDraftChange}
/>
<Card size='small' styles={{ body: { padding: 0, height: 200 } }}>
<ScrollBox inner={true} smallPadding={true}>
<div style={{ padding: '18px 20px', minWidth: 0 }}>
<SimplePropertyFilter
modelType={modelType}
propertyName={propertyName}
value={draft}
onChange={handleDraftChange}
filter={filter}
masterFilter={masterFilter}
visible={visible}
/>
</div>
</ScrollBox>
</Card>
</Flex>
</div>
)
}
ColumnFilterDropdown.propTypes = {
setSelectedKeys: PropTypes.func,
selectedKeys: PropTypes.array,
confirm: PropTypes.func,
clearFilters: PropTypes.func,
visible: PropTypes.bool,
propertyName: PropTypes.string,
propertyLabel: PropTypes.string,
modelType: PropTypes.string,
filter: PropTypes.object,
masterFilter: PropTypes.object
}
const RowForm = ({ record, isEditing, onRegister, children }) => {
const [form] = Form.useForm()
useEffect(() => {
if (isEditing && record && !record.isSkeleton) {
form.setFieldsValue(record)
onRegister(record._id, form)
}
return () => {
if (record?._id) onRegister(record._id, null)
}
}, [isEditing, record, form, onRegister])
return (
<Form form={form} component={false}>
{children}
</Form>
)
}
RowForm.propTypes = {
record: PropTypes.object,
isEditing: PropTypes.bool,
onRegister: PropTypes.func,
children: PropTypes.node
}
const EditableRow = ({ record, isEditing, onRegister, ...props }) => {
return (
<RowForm record={record} isEditing={isEditing} onRegister={onRegister}>
<tr {...props} />
</RowForm>
)
}
EditableRow.propTypes = {
record: PropTypes.object,
isEditing: PropTypes.bool,
onRegister: PropTypes.func
}
const ObjectTable = forwardRef(
(
{
type,
pageSize = 25,
scrollHeight = 'calc(var(--unit-100vh) - 258px)',
onDataChange,
initialPage = 1,
cards = false,
visibleColumns = {},
masterFilter = {},
size = 'middle',
onStateChange,
showFilterSidebar = false,
showSortSidebar = false,
expandHeight = false,
showActions = true,
saveFilterInSession = false,
saveFilterInUrl = false,
useFilterInSession = false,
useFilterInUrl = false,
saveSortInSession = false,
saveSortInUrl = false,
useSortInSession = false,
useSortInUrl = false,
onRowAction
},
ref
) => {
const { token, userProfile } = useContext(AuthContext)
const { isElectron } = useContext(ElectronContext)
const { callAction } = useActions()
const objectListView = useContext(ObjectListViewContext)
const listViewId = objectListView?.listViewId ?? null
const listViewFilter = objectListView?.listViewFilter ?? null
const listViewSort = objectListView?.listViewSort ?? null
const activeObjectView = objectListView?.activeObjectView ?? null
const onObjectViewFilterSortChange = objectListView?.isEditing
? objectListView.handleFilterSortChange
: null
const onStateChangeRef = useRef(onStateChange)
useEffect(() => {
onStateChangeRef.current = onStateChange
}, [onStateChange])
const {
fetchObjects,
connected,
subscribeToAllObjectUpdates,
subscribeToObjectTypeUpdates,
updateMultipleObjects,
setObjectActivity,
clearObjectActivity
} = useContext(ApiServerContext)
const isMobile = useMediaQuery({ maxWidth: 768 })
const location = useLocation()
const {
getPersistedFilter,
getPersistedSorter,
getAllTabPersistedFilter,
getAllTabPersistedSorter,
persistFilter,
persistSort,
persistTableState,
persistAllTabTableState,
registerPageFilter,
registerPageSorter,
registerObjectListFilter,
registerObjectListSorter
} = useTableStatePersistence({
scope: type,
viewId: listViewId,
pagePath: location.pathname,
saveFilterInSession,
saveFilterInUrl,
useFilterInSession,
useFilterInUrl,
saveSortInSession,
saveSortInUrl,
useSortInSession,
useSortInUrl
})
const { getViewFromUrl } = useTableState()
var adjustedScrollHeight = scrollHeight
if (isMobile) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 298px)'
}
if (cards) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 210px)'
}
if (isElectron) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 238px)'
}
if (isMobile && isElectron) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 282px)'
}
if (cards && isElectron) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 258px)'
}
const tableRef = useRef(null)
const model = getModelByName(type)
const activeFilterRef = useRef({})
const tableSorterRef = useRef({})
const listViewIdRef = useRef(listViewId)
const listViewFilterRef = useRef(listViewFilter)
const activeObjectViewRef = useRef(activeObjectView)
activeObjectViewRef.current = activeObjectView
const onObjectViewFilterSortChangeRef = useRef(onObjectViewFilterSortChange)
onObjectViewFilterSortChangeRef.current = onObjectViewFilterSortChange
const [sidebarFilter, setSidebarFilter] = useState({})
const [tableSorter, setTableSorter] = useState({})
const [initialized, setInitialized] = useState(false)
// Keep refs in sync with props. applyViewState may update them earlier in
// the same tick; props remain the source of truth after commit.
useEffect(() => {
listViewIdRef.current = listViewId
listViewFilterRef.current = listViewFilter || {}
}, [listViewId, listViewFilter])
const lastAppliedViewKeyRef = useRef(null)
const buildEffectiveFilter = useCallback((userFilter, viewFilter = listViewFilterRef.current) => {
const active = getActiveFilterValues(userFilter)
const viewId = listViewIdRef.current
if (!viewId) return active
return {
...getActiveFilterValues(viewFilter || {}),
...active
}
}, [])
// Table state
const [pages, setPages] = useState([])
const pagesRef = useRef(pages)
const tableData = useMemo(() => {
const items = pages.flatMap((page) => page.items)
const seen = new Set()
return items.filter((item) => {
const id = item?._id
if (id == null) return true
if (seen.has(id)) {
logger.warn('Duplicate table row omitted:', id)
return false
}
seen.add(id)
return true
})
}, [pages])
const [loading, setLoading] = useState(true)
const [lazyLoading, setLazyLoading] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const [editLoading, setEditLoading] = useState(false)
const rowFormsRef = useRef({})
const registerForm = useCallback((id, form) => {
if (form) {
rowFormsRef.current[id] = form
} else {
delete rowFormsRef.current[id]
}
}, [])
useEffect(() => {
onStateChangeRef.current?.({ isEditing, editLoading })
}, [isEditing, editLoading])
const updateEventHandlerRef = useRef()
const subscribeToObjectTypeUpdatesRef = useRef(null)
const subscribedTypeRef = useRef(null)
const subscribeToAllObjectUpdatesRef = useRef(null)
const newEventHandlerRef = useRef()
const subscriptionFilterRef = useRef()
const effectiveSorterRef = useRef({})
const reloadRef = useRef(null)
const silentReloadRef = useRef(null)
const subscribeToObjectTypeUpdatesFnRef = useRef(
subscribeToObjectTypeUpdates
)
const prevValuesRef = useRef({ type, masterFilter })
const getMasterFilter = useCallback(() => masterFilter || {}, [masterFilter])
const resolveSorter = useCallback((userSorter) => {
if (userSorter?.field && userSorter?.order) {
return { field: userSorter.field, order: userSorter.order }
}
return {}
}, [])
const rowActions =
model.actions?.filter((action) => action.row == true) || []
const createSkeletonData = useCallback(
(pageNum) => {
return Array(pageSize)
.fill(null)
.map((_, index) => ({
_id: `skeleton-${pageNum}-${index}`,
isSkeleton: true
}))
},
[pageSize]
)
const createSkeletonPage = useCallback(
(pageNum) => ({
pageNum,
items: createSkeletonData(pageNum),
isSkeletonPage: true
}),
[createSkeletonData]
)
const loadingPagesRef = useRef(new Set())
const pendingScrollAnchorRef = useRef(null)
const dataLoadGenerationRef = useRef(0)
const [tableListKey, setTableListKey] = useState(0)
// Suppress Ant Design Table onChange while we remount / set filter+sort
// programmatically. Remounting with controlled filteredValue often fires
// onChange with all-null filters (sorter intact) and would wipe the view.
const isInternalTableChangeRef = useRef(false)
const internalTableChangeClearRafRef = useRef(null)
const suppressTableChange = useCallback(() => {
isInternalTableChangeRef.current = true
if (internalTableChangeClearRafRef.current != null) {
cancelAnimationFrame(internalTableChangeClearRafRef.current)
}
// Double rAF: wait until after the remounted Table commits and any
// synchronous/mount onChange from Ant Design has run.
internalTableChangeClearRafRef.current = requestAnimationFrame(() => {
internalTableChangeClearRafRef.current = requestAnimationFrame(() => {
internalTableChangeClearRafRef.current = null
isInternalTableChangeRef.current = false
})
})
}, [])
const beginDataReload = useCallback(() => {
dataLoadGenerationRef.current += 1
loadingPagesRef.current.clear()
pendingScrollAnchorRef.current = null
pagesRef.current = []
suppressTableChange()
setTableListKey((key) => key + 1)
return dataLoadGenerationRef.current
}, [suppressTableChange])
const isStaleDataLoad = useCallback((generation) => {
return generation !== dataLoadGenerationRef.current
}, [])
const clearTablePages = useCallback(() => {
pagesRef.current = []
flushSync(() => {
setPages([])
})
}, [])
const setTablePages = useCallback((nextPages) => {
pagesRef.current = nextPages
setPages(nextPages)
}, [])
const mergeLoadedPage = useCallback((currentPages, loadedPage) => {
const withoutSkeletons = currentPages.filter(
(page) => !page.isSkeletonPage
)
const withoutDuplicate = withoutSkeletons.filter(
(page) => page.pageNum !== loadedPage.pageNum
)
return [...withoutDuplicate, loadedPage].sort(
(a, b) => a.pageNum - b.pageNum
)
}, [])
const renderActions = (objectData) => {
return (
<Flex gap='small' align='center' justify='center'>
{rowActions.map((action, index) => {
const denied = !hasActionPermission(userProfile, model, action.name)
var disabled = denied
if (action.disabled) {
if (typeof action.disabled === 'function') {
disabled =
denied ||
action.disabled({
...objectData,
_user: userProfile
})
} else {
disabled = denied || action.disabled
}
}
return (
<Tooltip key={index} title={action.label}>
<Button
icon={
action.icon ? (
createElement(action.icon)
) : (
<QuestionCircleIcon />
)
}
disabled={disabled || objectData?.isSkeleton}
type={'text'}
size={'small'}
danger={action?.danger || false}
onClick={() => {
if (denied) return
if (
onRowAction &&
onRowAction(action, objectData) !== false
) {
return
}
callAction(
action,
{ ...objectData, _user: userProfile },
type
)
}}
/>
</Tooltip>
)
})}
</Flex>
)
}
const fetchPage = useCallback(
async (pageNum = 1, filter = null, sorter = null) => {
if (filter == null) {
filter = activeFilterRef.current
} else {
activeFilterRef.current = filter
}
if (sorter == null) {
sorter = tableSorterRef.current
} else {
tableSorterRef.current = {
field: sorter.field,
order: sorter.order
}
}
return fetchObjects(type, {
page: pageNum,
limit: pageSize,
filter: { ...getMasterFilter(), ...filter },
sorter: resolveSorter(sorter),
onDataChange
})
},
[
type,
getMasterFilter,
pageSize,
onDataChange,
fetchObjects,
resolveSorter
]
)
const skeletonsHeight = useMemo(() => {
return SKELETON_HEIGHT * pageSize
}, [pageSize])
const skeletonBoundaryIds = useMemo(() => {
const firstPage = pages[0]
const lastPage = pages[pages.length - 1]
return {
previous:
firstPage?.isSkeletonPage && firstPage.items.length > 0
? firstPage.items[firstPage.items.length - 1]._id
: null,
next:
lastPage?.isSkeletonPage && lastPage.items[0]
? lastPage.items[0]._id
: null
}
}, [pages])
const createPageWindow = useCallback(
(loadedPages, direction) => {
const dedupedByPageNum = new Map()
loadedPages.forEach((page) => {
dedupedByPageNum.set(page.pageNum, page)
})
const sortedPages = [...dedupedByPageNum.values()].sort(
(a, b) => a.pageNum - b.pageNum
)
const visiblePages =
direction === 'previous'
? sortedPages.slice(0, 2)
: sortedPages.slice(-2)
const firstPage = visiblePages[0]
const lastPage = visiblePages[visiblePages.length - 1]
const nextPages = []
if (firstPage?.pageNum > 1) {
nextPages.push(createSkeletonPage(firstPage.pageNum - 1))
}
nextPages.push(...visiblePages)
if (lastPage?.hasMore) {
nextPages.push(createSkeletonPage(lastPage.pageNum + 1))
}
return nextPages
},
[createSkeletonPage]
)
const fetchData = useCallback(
async (pageNum = 1, filter = null, sorter = null) => {
const generation = dataLoadGenerationRef.current
try {
const result = await fetchPage(pageNum, filter, sorter)
if (isStaleDataLoad(generation)) return []
const loadedPage = {
pageNum,
items: result.data || [],
hasMore: result.hasMore
}
setPages((prev) =>
prev.map((page) => (page.pageNum === pageNum ? loadedPage : page))
)
setLoading(false)
return result.data || []
} catch (error) {
if (!isStaleDataLoad(generation)) {
setLoading(false)
}
throw error
}
},
[fetchPage, isStaleDataLoad]
)
const findRenderedRow = useCallback((scrollTarget, id) => {
if (!scrollTarget || id == null) return null
return Array.from(scrollTarget.querySelectorAll('tr[data-row-key]')).find(
(row) => row.getAttribute('data-row-key') === String(id)
)
}, [])
const captureScrollAnchor = useCallback(
(scrollTarget, placeholderPage, direction, currentPages) => {
if (!scrollTarget) return null
const targetTop = scrollTarget.getBoundingClientRect().top
const closestRow = (items) => {
return items
.map((item, index) => {
const element = findRenderedRow(scrollTarget, item._id)
return element
? {
index,
id: item._id,
top: element.getBoundingClientRect().top
}
: null
})
.filter(Boolean)
.sort(
(a, b) =>
Math.abs(a.top - targetTop) - Math.abs(b.top - targetTop)
)[0]
}
const placeholderAnchor = closestRow(placeholderPage.items)
const loadedPages = currentPages.filter((page) => !page.isSkeletonPage)
const retainedPage =
direction === 'previous'
? loadedPages[0]
: loadedPages[loadedPages.length - 1]
const fallbackAnchor = retainedPage
? closestRow(retainedPage.items)
: null
return { placeholderAnchor, fallbackAnchor }
},
[findRenderedRow]
)
const restoreScrollAnchor = useCallback(
(scrollTarget, anchor, loadedPage) => {
if (!scrollTarget || !anchor) return
const loadedItem = loadedPage.items[anchor.placeholderAnchor?.index]
const anchorId = loadedItem?._id ?? anchor.fallbackAnchor?.id
const previousTop = loadedItem
? anchor.placeholderAnchor?.top
: anchor.fallbackAnchor?.top
if (anchorId == null || previousTop == null) return
const element = findRenderedRow(scrollTarget, anchorId)
if (!element) return
const renderedTop = element.getBoundingClientRect().top
scrollTarget.scrollTop += renderedTop - previousTop
},
[findRenderedRow]
)
useLayoutEffect(() => {
const pendingAnchor = pendingScrollAnchorRef.current
if (!pendingAnchor) return
pendingScrollAnchorRef.current = null
restoreScrollAnchor(
pendingAnchor.scrollTarget,
pendingAnchor.anchor,
pendingAnchor.loadedPage
)
}, [pages, restoreScrollAnchor])
const loadBoundaryPage = useCallback(
async (pageNum, direction, scrollTarget) => {
if (loadingPagesRef.current.size > 0) return
const currentPages = pagesRef.current
const placeholderPage = currentPages.find(
(page) => page.pageNum === pageNum && page.isSkeletonPage
)
if (!placeholderPage) return
const scrollAnchor = captureScrollAnchor(
scrollTarget,
placeholderPage,
direction,
currentPages
)
const generation = dataLoadGenerationRef.current
loadingPagesRef.current.add(pageNum)
setLazyLoading(true)
logger.debug(`Loading ${direction} page...`)
try {
const result = await fetchPage(pageNum)
if (isStaleDataLoad(generation)) return
const loadedPage = {
pageNum,
items: result.data || [],
hasMore: result.hasMore
}
pendingScrollAnchorRef.current = {
scrollTarget,
anchor: scrollAnchor,
loadedPage
}
setPages((prev) => {
if (
!prev.some(
(page) => page.pageNum === pageNum && page.isSkeletonPage
)
) {
return prev
}
const loadedPages = mergeLoadedPage(prev, loadedPage)
return createPageWindow(loadedPages, direction)
})
} catch (error) {
logger.error(`Error loading page ${pageNum}:`, error)
} finally {
loadingPagesRef.current.delete(pageNum)
if (
!isStaleDataLoad(generation) &&
loadingPagesRef.current.size === 0
) {
setLazyLoading(false)
}
}
},
[
captureScrollAnchor,
createPageWindow,
fetchPage,
isStaleDataLoad,
mergeLoadedPage
]
)
const loadNextPage = useCallback(
(scrollTarget) => {
const nextPage = pagesRef.current[pagesRef.current.length - 1]
if (nextPage?.isSkeletonPage) {
loadBoundaryPage(nextPage.pageNum, 'next', scrollTarget)
}
},
[loadBoundaryPage]
)
const loadPreviousPage = useCallback(
(scrollTarget) => {
const previousPage = pagesRef.current[0]
if (previousPage?.isSkeletonPage) {
loadBoundaryPage(previousPage.pageNum, 'previous', scrollTarget)
}
},
[loadBoundaryPage]
)
const handleScroll = useCallback(
(e) => {
const { target } = e
const firstPage = pagesRef.current[0]
const lastPage = pagesRef.current[pagesRef.current.length - 1]
if (
lastPage?.isSkeletonPage &&
target.scrollHeight - target.scrollTop - target.clientHeight <=
skeletonsHeight + SCROLL_THRESHOLD
) {
loadNextPage(target)
} else if (
firstPage?.isSkeletonPage &&
target.scrollTop <= skeletonsHeight + SCROLL_THRESHOLD
) {
loadPreviousPage(target)
}
},
[loadNextPage, loadPreviousPage, skeletonsHeight]
)
const reloadLoadedPages = useCallback(
async ({ silent = false } = {}) => {
if (!silent) {
setLazyLoading(true)
}
try {
const loadedPages = pagesRef.current.filter(
(page) => !page.isSkeletonPage
)
for (let i = 0; i < loadedPages.length; i++) {
await fetchData(loadedPages[i].pageNum)
}
} finally {
if (!silent) {
setLazyLoading(false)
}
}
},
[fetchData]
)
const reload = useCallback(() => reloadLoadedPages(), [reloadLoadedPages])
const silentReload = useCallback(
() => reloadLoadedPages({ silent: true }),
[reloadLoadedPages]
)
reloadRef.current = reload
silentReloadRef.current = silentReload
const startEditing = useCallback(() => {
setIsEditing(true)
tableData.forEach((item) => {
if (!item.isSkeleton) {
setObjectActivity(item._id, type, 'editing')
}
})
}, [tableData, setObjectActivity, type])
const cancelEditing = useCallback(() => {
setIsEditing(false)
tableData.forEach((item) => {
if (!item.isSkeleton) {
clearObjectActivity(item._id, type)
}
})
}, [tableData, clearObjectActivity, type])
const handleUpdate = useCallback(async () => {
setEditLoading(true)
try {
const updates = await Promise.all(
Object.entries(rowFormsRef.current).map(async ([id, form]) => {
const values = await form.validateFields()
return { _id: id, ...values }
})
)
await updateMultipleObjects(type, updates)
setIsEditing(false)
reload()
tableData.forEach((item) => {
if (!item.isSkeleton) {
clearObjectActivity(item._id, type)
}
})
} catch (err) {
logger.error('Error updating objects:', err)
} finally {
setEditLoading(false)
}
}, [type, updateMultipleObjects, reload, tableData, clearObjectActivity])
const findTableItem = useCallback((id) => {
for (const page of pagesRef.current) {
const item = page.items?.find(
(entry) => String(entry._id) === String(id) && !entry.isSkeleton
)
if (item) return item
}
return null
}, [])
// Update event handler for real-time updates
const updateEventHandler = useCallback(
(id, updatedData) => {
const existingItem = findTableItem(id)
const changedKeys = getChangedKeys(existingItem, updatedData)
if (
updateAffectsFilterOrSort(
changedKeys,
subscriptionFilterRef.current,
{},
effectiveSorterRef.current
)
) {
silentReloadRef.current?.()
return
}
if (!existingItem) return
setPages((prevPages) =>
prevPages.map((page) => {
const updatedItems = page.items.map((item) => {
if (String(item._id) === String(id)) {
return { ...item, ...updatedData }
}
return item
})
return {
...page,
items: updatedItems
}
})
)
if (rowFormsRef.current[id]) {
rowFormsRef.current[id].setFieldsValue(updatedData)
}
},
[findTableItem]
)
// Store the latest updateEventHandler in a ref
updateEventHandlerRef.current = updateEventHandler
const newEventHandler = useCallback((params) => {
logger.debug('New event handler:', params)
silentReloadRef.current?.()
}, [])
// Effective filter for ObjectTableFilterContext, breadcrumb, and neighbors:
// view definition + any user-entered sidebar filters.
const effectiveFilter = useMemo(() => {
const userFilter = getActiveFilterValues(sidebarFilter)
if (!listViewId) return userFilter
return {
...getActiveFilterValues(listViewFilter || {}),
...userFilter
}
}, [listViewId, listViewFilter, sidebarFilter])
const effectiveSorter = useMemo(() => {
if (tableSorter?.field && tableSorter?.order) {
return { field: tableSorter.field, order: tableSorter.order }
}
if (listViewId && listViewSort?.field && listViewSort?.order) {
return { field: listViewSort.field, order: listViewSort.order }
}
return {}
}, [listViewId, listViewSort, tableSorter])
const subscriptionFilter = useMemo(
() => ({ ...masterFilter, ...effectiveFilter }),
[effectiveFilter, masterFilter]
)
const tableFilterContextValue = useMemo(
() => ({
filter: effectiveFilter,
masterFilter
}),
[effectiveFilter, masterFilter]
)
newEventHandlerRef.current = newEventHandler
subscriptionFilterRef.current = subscriptionFilter
effectiveSorterRef.current = effectiveSorter
subscribeToObjectTypeUpdatesFnRef.current = subscribeToObjectTypeUpdates
// Cleanup subscriptions on unmount
useEffect(() => {
return () => {
if (connected == true && subscribeToObjectTypeUpdatesRef.current) {
subscribeToObjectTypeUpdatesRef.current()
subscribeToObjectTypeUpdatesRef.current = null
}
if (connected == true && subscribeToAllObjectUpdatesRef.current) {
subscribeToAllObjectUpdatesRef.current()
subscribeToAllObjectUpdatesRef.current = null
}
}
}, [connected])
// Subscribe to all object updates for this type
useEffect(() => {
if (connected !== true || !type) return
const unsubscribe = subscribeToAllObjectUpdates(type, (id, updateData) => {
updateEventHandlerRef.current(id, updateData)
})
subscribeToAllObjectUpdatesRef.current = unsubscribe
return () => {
if (unsubscribe) unsubscribe()
if (subscribeToAllObjectUpdatesRef.current === unsubscribe) {
subscribeToAllObjectUpdatesRef.current = null
}
}
}, [type, connected, subscribeToAllObjectUpdates])
useEffect(() => {
if (connected !== true) return
if (subscribedTypeRef.current === type) return
const unsubscribe = subscribeToObjectTypeUpdatesFnRef.current(
type,
subscriptionFilterRef.current,
(params) => newEventHandlerRef.current(params)
)
subscribeToObjectTypeUpdatesRef.current = unsubscribe
subscribedTypeRef.current = type
return () => {
if (unsubscribe) unsubscribe()
if (subscribeToObjectTypeUpdatesRef.current === unsubscribe) {
subscribeToObjectTypeUpdatesRef.current = null
}
if (subscribedTypeRef.current === type) {
subscribedTypeRef.current = null
}
}
}, [type, connected])
const updateData = useCallback(
(id, updatedData) => {
updateEventHandler(id, updatedData)
},
[updateEventHandler]
)
const loadPage = useCallback(
async (pageNum, filter = null, sorter = null) => {
const generation = beginDataReload()
clearTablePages()
if (isStaleDataLoad(generation)) return
const skeletonPage = createSkeletonPage(pageNum)
flushSync(() => {
setTablePages([skeletonPage])
setLoading(true)
})
if (isStaleDataLoad(generation)) return
try {
const firstResult = await fetchPage(pageNum, filter, sorter)
if (isStaleDataLoad(generation)) return
const loadedPages = [
{
pageNum,
items: firstResult.data || [],
hasMore: firstResult.hasMore
}
]
if (!isStaleDataLoad(generation)) {
setTablePages(createPageWindow(loadedPages, 'next'))
}
} catch (error) {
if (!isStaleDataLoad(generation)) {
logger.error(`Error loading page ${pageNum}:`, error)
setTablePages([])
}
} finally {
if (!isStaleDataLoad(generation)) {
setLoading(false)
// Data/column swap can also trigger a spurious Table onChange.
suppressTableChange()
}
}
},
[
beginDataReload,
clearTablePages,
createPageWindow,
createSkeletonPage,
fetchPage,
isStaleDataLoad,
setTablePages,
suppressTableChange
]
)
const loadInitialPage = useCallback(async () => {
// A selected view applies its own filter/sort via applyViewState.
// Do not load (or clear) filters here — that races and wipes the view.
if (getViewFromUrl() || listViewIdRef.current) {
return
}
const filter = getPersistedFilter()
const sorter = getPersistedSorter()
const activeFilter = getActiveFilterValues(filter)
const resolvedSorter = resolveSorter(sorter)
if (Object.keys(filter).length > 0) {
setSidebarFilter(filter)
}
if (sorter?.field) {
tableSorterRef.current = sorter
setTableSorter(sorter)
}
loadPage(
initialPage,
activeFilter,
resolvedSorter.field ? resolvedSorter : null
)
}, [
getPersistedFilter,
getPersistedSorter,
getViewFromUrl,
initialPage,
loadPage,
resolveSorter
])
// When a view becomes available, apply its definition once. Skip empty
// prop-lag updates so we never clear an already-applied view filter.
useEffect(() => {
if (!listViewId || activeObjectView) {
if (!listViewId) lastAppliedViewKeyRef.current = null
return
}
const viewFilter = getActiveFilterValues(listViewFilter || {})
const viewSort =
listViewSort?.field && listViewSort?.order
? { field: listViewSort.field, order: listViewSort.order }
: {}
const viewKey = `${listViewId}:${JSON.stringify(viewFilter)}:${JSON.stringify(viewSort)}`
if (lastAppliedViewKeyRef.current === viewKey) return
// Prop lag: listViewId is set but filter/sort props are still empty.
// Never overwrite a non-empty sidebar/query with {}.
const hasViewState =
Object.keys(viewFilter).length > 0 || Boolean(viewSort.field)
if (!hasViewState) return
lastAppliedViewKeyRef.current = viewKey
setSidebarFilter(viewFilter)
setTableSorter(viewSort)
tableSorterRef.current = viewSort
activeFilterRef.current = viewFilter
loadPage(
initialPage,
viewFilter,
viewSort.field ? viewSort : null
)
}, [
activeObjectView,
initialPage,
listViewFilter,
listViewId,
listViewSort,
loadPage
])
const applyViewState = useCallback(
(filterState, sorterState, options = {}) => {
let filter
let sorter
if (filterState == null && sorterState == null) {
// Ignore accidental All restores while a view is still selected
// (e.g. load race). Explicit All tab switches pass forceAll.
if (
!options.forceAll &&
(listViewIdRef.current || getViewFromUrl())
) {
return
}
listViewIdRef.current = null
listViewFilterRef.current = {}
lastAppliedViewKeyRef.current = null
filter = getAllTabPersistedFilter()
sorter = getAllTabPersistedSorter()
const resolvedSorter = resolveSorter(sorter)
const nextSorter = sorter?.field ? sorter : {}
const activeFilter = getActiveFilterValues(filter)
setSidebarFilter(filter)
tableSorterRef.current = nextSorter
setTableSorter(nextSorter)
activeFilterRef.current = activeFilter
persistAllTabTableState(filter, nextSorter)
loadPage(
initialPage,
activeFilter,
resolvedSorter.field ? resolvedSorter : null
)
return
}
// View tab: apply the view definition. Sidebar starts as the view
// filter so the UI matches; All-tab user settings must not leak in.
const viewId =
options.viewId !== undefined
? options.viewId
: listViewIdRef.current
const viewFilter =
options.viewFilter !== undefined
? options.viewFilter || {}
: filterState || {}
listViewIdRef.current = viewId
listViewFilterRef.current = viewFilter
filter = viewFilter
sorter =
options.viewSort?.field
? options.viewSort
: sorterState?.field
? sorterState
: {}
const resolvedSorter = resolveSorter(sorter)
const nextSorter = sorter?.field ? sorter : {}
const activeFilter = getActiveFilterValues(filter)
lastAppliedViewKeyRef.current = `${viewId || ''}:${JSON.stringify(activeFilter)}:${JSON.stringify(nextSorter)}`
setSidebarFilter(filter)
tableSorterRef.current = nextSorter
setTableSorter(nextSorter)
activeFilterRef.current = activeFilter
loadPage(
initialPage,
activeFilter,
resolvedSorter.field ? resolvedSorter : null
)
},
[
getAllTabPersistedFilter,
getAllTabPersistedSorter,
getViewFromUrl,
initialPage,
loadPage,
persistAllTabTableState,
resolveSorter
]
)
useImperativeHandle(ref, () => ({
reload,
applyViewState,
setData: (newData) => {
setPages([{ pageNum: 1, items: newData }])
},
updateData,
startEditing,
cancelEditing,
handleUpdate,
isEditing,
editLoading
}))
useEffect(() => {
if (
connected == true &&
token != null &&
!pages.includes(initialPage) &&
!initialized
) {
loadInitialPage()
setInitialized(true)
}
}, [token, loadInitialPage, initialPage, pages, initialized, connected])
// Watch for changes in type and masterFilter, reset component state when they change
useEffect(() => {
const prevValues = prevValuesRef.current
const hasChanged =
prevValues.type !== type ||
JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)
if (hasChanged) {
beginDataReload()
pagesRef.current = []
setPages([])
activeFilterRef.current = {}
tableSorterRef.current = {}
setSidebarFilter({})
setTableSorter({})
setInitialized(false)
setLoading(true)
setLazyLoading(false)
prevValuesRef.current = { type, masterFilter }
}
}, [type, masterFilter, beginDataReload])
useEffect(() => {
registerPageFilter(effectiveFilter)
registerObjectListFilter(effectiveFilter)
return () => registerPageFilter({})
}, [effectiveFilter, registerPageFilter, registerObjectListFilter])
useEffect(() => {
registerPageSorter(effectiveSorter)
registerObjectListSorter(effectiveSorter)
return () => registerPageSorter({})
}, [effectiveSorter, registerPageSorter, registerObjectListSorter])
const getFilterDropdown = ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
visible,
propertyName,
propertyLabel
}) => (
<ColumnFilterDropdown
setSelectedKeys={setSelectedKeys}
selectedKeys={selectedKeys}
confirm={confirm}
clearFilters={clearFilters}
visible={visible}
propertyName={propertyName}
propertyLabel={propertyLabel}
modelType={type}
filter={effectiveFilter}
masterFilter={masterFilter}
/>
)
const handleTableChange = (pagination, filters, sorter) => {
if (isInternalTableChangeRef.current) return
const next = { ...sidebarFilter }
Object.entries(filters).forEach(([key, value]) => {
const expression = toFilterExpression(value)
if (expression !== undefined) {
next[key] = expression
} else {
delete next[key]
}
})
const nextSorter = {
field: sorter.columnKey,
order: sorter.order
}
setSidebarFilter(next)
setTableSorter(nextSorter)
const effective = buildEffectiveFilter(next)
activeFilterRef.current = effective
if (activeObjectViewRef.current) {
// Editing a view: keep filter/sort on the draft view only
onObjectViewFilterSortChangeRef.current?.(next, nextSorter)
} else if (!listViewIdRef.current) {
// All tab: persist personal filter/sort to URL/session
persistTableState(next, nextSorter)
}
loadPage(initialPage, effective, resolveSorter(nextSorter))
}
const handleSidebarFilterChange = useCallback(
(newSidebarFilter) => {
setSidebarFilter(newSidebarFilter)
const effective = buildEffectiveFilter(newSidebarFilter)
activeFilterRef.current = effective
if (activeObjectViewRef.current) {
onObjectViewFilterSortChangeRef.current?.(newSidebarFilter, tableSorter)
} else if (!listViewIdRef.current) {
persistFilter(newSidebarFilter)
}
loadPage(
initialPage,
effective,
resolveSorter(tableSorter)
)
},
[
buildEffectiveFilter,
initialPage,
loadPage,
persistFilter,
resolveSorter,
tableSorter
]
)
const handleSidebarSortChange = useCallback(
(newSorter) => {
const nextSorter = newSorter?.field
? { field: newSorter.field, order: newSorter.order }
: {}
suppressTableChange()
setTableSorter(nextSorter)
tableSorterRef.current = nextSorter
if (activeObjectViewRef.current) {
onObjectViewFilterSortChangeRef.current?.(sidebarFilter, nextSorter)
} else if (!listViewIdRef.current) {
persistSort(nextSorter)
}
loadPage(
initialPage,
buildEffectiveFilter(sidebarFilter),
resolveSorter(nextSorter)
)
},
[
buildEffectiveFilter,
initialPage,
loadPage,
persistSort,
resolveSorter,
sidebarFilter,
suppressTableChange
]
)
const modelProperties = getModelProperties(type)
// Table columns from model properties
const columnsWithSkeleton = [
{
title: lazyLoading ? <LoadingOutlined /> : cards ? model.icon : null,
key: 'icon',
width: 45,
fixed: 'left',
render: () => {
return <Flex justify='center'>{createElement(model.icon)}</Flex>
}
}
]
useEffect(() => {
pagesRef.current = pages
}, [pages])
useEffect(() => {
if (!expandHeight || cards) return
const findAntTableBody = (element) => {
if (!element) return null
if (element.classList?.contains('ant-table-body')) return element
for (const child of element.children) {
const found = findAntTableBody(child)
if (found) return found
}
return null
}
const root = tableRef.current?.nativeElement ?? tableRef.current
const tableBody = findAntTableBody(root)
if (!tableBody) return
tableBody.style.minHeight = adjustedScrollHeight
return () => {
tableBody.style.minHeight = ''
}
}, [expandHeight, adjustedScrollHeight, cards, loading, tableData])
// Add columns in the order specified by model.columns
model.columns.forEach((colName) => {
const prop = modelProperties.find((p) => p.name === colName)
if (prop) {
// Check if column should be visible based on visibleColumns prop
if (
Object.keys(visibleColumns).length > 0 &&
visibleColumns[prop.name] === false
) {
return // Skip this column if it's not visible
}
var fixed = prop.columnFixed || undefined
var width = 200
switch (prop.type) {
case 'text':
width = 200
break
case 'number':
width = 100
break
case 'dateTime':
width = 200
break
case 'state':
width = 200
break
case 'id':
width = 180
break
default:
break
}
// Check if this property should be filterable based on model.filters
const isFilterable = model.filters && model.filters.includes(prop.name)
// Check if this property should be sortable based on model.sorters
const isSortable = model.sorters && model.sorters.includes(prop.name)
const columnConfig = {
sorter: isSortable ? { multiple: 1 } : undefined,
sortOrder:
tableSorter?.field === prop.name ? tableSorter.order : null,
title: prop.label,
width: prop.columnWidth || width,
fixed: isMobile ? undefined : fixed,
key: prop.name,
filterIcon: () => {
return (
<Tooltip title='Filter' listenParents={1}>
<FilterIcon />
</Tooltip>
)
},
render: (text, record) => {
if (record?.isSkeleton) {
return (
<Skeleton.Input
active
size='small'
style={{ width: '100%', height: 24.5 }}
/>
)
}
return (
<ObjectProperty
{...prop}
longId={false}
inTable={true}
objectData={record}
isEditing={isEditing}
/>
)
}
}
if (isFilterable && !Object.keys(masterFilter).includes(prop.name)) {
columnConfig.filterDropdown = ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
visible
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
visible,
propertyName: prop.name,
propertyLabel: prop.label
})
columnConfig.filteredValue = fromFilterExpression(
effectiveFilter[prop.name]
)
}
columnsWithSkeleton.push(columnConfig)
}
})
if (
showActions &&
rowActions.length > 0 &&
tableData.some((item) => !item?.isSkeleton)
) {
columnsWithSkeleton.push({
title: (
<Flex gap='small' align='center' justify='center'>
<ActionsIcon />
</Flex>
),
key: 'actions',
fixed: 'right',
width: 20 + rowActions.length * 30, // Adjust width based on number of actions
render: (record) => {
return renderActions(record)
}
})
}
// Card view rendering
const [cardColSpan, setCardColSpan] = useState(24)
const cardsContainerNodeRef = useRef(null)
const cardsResizeObserverRef = useRef(null)
const measureCardColSpan = useCallback(() => {
const node = cardsContainerNodeRef.current
if (!node) return
const panel = node.closest('.ant-splitter-panel')
const width = (panel ?? node).getBoundingClientRect().width
if (width <= 0) return
const nextSpan = getCardColSpan(width)
setCardColSpan((prev) => (prev === nextSpan ? prev : nextSpan))
}, [])
const objectTableCardsContainerRef = useCallback(
(node) => {
cardsResizeObserverRef.current?.disconnect()
cardsResizeObserverRef.current = null
cardsContainerNodeRef.current = node
if (!node) return
let rafId = null
const scheduleMeasure = () => {
if (rafId != null) cancelAnimationFrame(rafId)
rafId = requestAnimationFrame(() => {
rafId = null
measureCardColSpan()
})
}
scheduleMeasure()
const observer = new ResizeObserver(scheduleMeasure)
const panel = node.closest('.ant-splitter-panel')
if (panel) {
observer.observe(panel)
} else {
observer.observe(node)
}
cardsResizeObserverRef.current = observer
},
[measureCardColSpan]
)
useLayoutEffect(() => {
if (!cards) return
measureCardColSpan()
}, [cards, showFilterSidebar, showSortSidebar, measureCardColSpan])
useEffect(() => {
return () => cardsResizeObserverRef.current?.disconnect()
}, [])
// Card view: load when the leading skeleton card in a boundary page enters view
useLayoutEffect(() => {
if (!cards) return
const container = cardsContainerNodeRef.current
if (!container) return
const scrollEl = container.querySelector('.simplebar-content-wrapper')
if (!scrollEl) return
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue
const boundary = entry.target.dataset.skeletonBoundary
if (boundary === 'next') {
loadNextPage(scrollEl)
} else if (boundary === 'previous') {
loadPreviousPage(scrollEl)
}
}
},
{
root: scrollEl,
rootMargin: `${SCROLL_THRESHOLD}px`,
threshold: 0
}
)
container
.querySelectorAll('[data-skeleton-boundary]')
.forEach((el) => observer.observe(el))
return () => observer.disconnect()
}, [cards, skeletonBoundaryIds, loadNextPage, loadPreviousPage])
const renderCards = () => {
return (
<ScrollBox
style={{
maxHeight: `100%`
}}
>
<div className='objectTableCards'>
<Row gutter={[16, 16]} key={`${cardColSpan}-${tableListKey}`}>
{tableData.map((record) => {
if (record?._id == undefined) {
return null
}
const skeletonBoundary =
record._id === skeletonBoundaryIds.next
? 'next'
: record._id === skeletonBoundaryIds.previous
? 'previous'
: undefined
return (
<Col span={cardColSpan} key={record._id}>
<div
style={{ width: '100%', overflow: 'hidden' }}
data-skeleton-boundary={skeletonBoundary}
>
<RowForm
record={record}
isEditing={isEditing}
onRegister={registerForm}
>
<Flex align={'center'} vertical gap={'middle'}>
<ObjectCard
isSkeleton={record?.isSkeleton || false}
model={model}
modelProperties={modelProperties}
visibleColumns={visibleColumns}
record={record}
isEditing={isEditing}
rowActions={rowActions}
renderActions={renderActions}
/>
</Flex>
</RowForm>
</div>
</Col>
)
})}
</Row>
</div>
</ScrollBox>
)
}
const components = useMemo(
() => ({
body: {
row: EditableRow
}
}),
[]
)
const onRow = useCallback(
(record) => ({
record,
isEditing,
onRegister: registerForm
}),
[isEditing, registerForm]
)
const tableContent = (
<Flex
gap={'middle'}
vertical
style={{ flex: 1, minWidth: 0, minHeight: 0 }}
>
<Splitter
className={'farmcontrol-splitter large'}
onResize={measureCardColSpan}
onResizeEnd={measureCardColSpan}
>
<Splitter.Panel>
{cards ? (
<div
className='objectTableCardsContainer'
ref={objectTableCardsContainerRef}
>
<Spin
indicator={<LoadingOutlined />}
spinning={loading}
style={{ height: '100%' }}
>
{renderCards()}
</Spin>
</div>
) : (
<Table
key={tableListKey}
ref={tableRef}
dataSource={tableData}
columns={columnsWithSkeleton}
className='dashboard-table'
pagination={false}
scroll={{ y: adjustedScrollHeight }}
rowKey='_id'
loading={{
spinning: loading,
indicator: <LoadingOutlined spin />
}}
onScroll={handleScroll}
onChange={handleTableChange}
showSorterTooltip={false}
style={{ height: '100%' }}
size={size}
components={components}
onRow={onRow}
/>
)}
</Splitter.Panel>
{(showSortSidebar || showFilterSidebar) && (
<Splitter.Panel defaultSize={'25%'}>
<Flex
vertical
gap='large'
style={{ height: '100%', minHeight: 0 }}
>
{showSortSidebar && (
<SortSidebar
type={type}
sorter={effectiveSorter}
onSortChange={handleSidebarSortChange}
stretchHeight={showSortSidebar && !showFilterSidebar}
/>
)}
{showFilterSidebar && (
<Flex style={{ flex: 1, minHeight: 0 }}>
<FilterSidebar
type={type}
filter={effectiveFilter}
onFilterChange={handleSidebarFilterChange}
masterFilter={masterFilter}
/>
</Flex>
)}
</Flex>
</Splitter.Panel>
)}
</Splitter>
</Flex>
)
return (
<ObjectTableFilterContext.Provider value={tableFilterContextValue}>
{tableContent}
</ObjectTableFilterContext.Provider>
)
}
)
ObjectTable.displayName = 'ObjectTable'
ObjectTable.propTypes = {
type: PropTypes.string.isRequired,
pageSize: PropTypes.number,
scrollHeight: PropTypes.string,
onDataChange: PropTypes.func,
initialPage: PropTypes.number,
cards: PropTypes.bool,
cardRenderer: PropTypes.func,
visibleColumns: PropTypes.object,
masterFilter: PropTypes.object,
size: PropTypes.string,
onStateChange: PropTypes.func,
showFilterSidebar: PropTypes.bool,
showSortSidebar: PropTypes.bool,
expandHeight: PropTypes.bool,
showActions: PropTypes.bool,
saveFilterInSession: PropTypes.bool,
saveFilterInUrl: PropTypes.bool,
useFilterInSession: PropTypes.bool,
useFilterInUrl: PropTypes.bool,
saveSortInSession: PropTypes.bool,
saveSortInUrl: PropTypes.bool,
useSortInSession: PropTypes.bool,
useSortInUrl: PropTypes.bool,
onRowAction: PropTypes.func
}
export default ObjectTable