Compare commits
No commits in common. "38e89e45c73743a54bc9022894784bce4001a985" and "ed4f1c7505adc9c1a7323fcb3e9bb09f00b7a04b" have entirely different histories.
38e89e45c7
...
ed4f1c7505
@ -513,24 +513,6 @@ body {
|
|||||||
height: 8px !important;
|
height: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbox-inner .simplebar-track.simplebar-vertical {
|
|
||||||
right: var(--scrollbox-vertical-right-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbox-inner .simplebar-track.simplebar-horizontal {
|
|
||||||
bottom: var(--scrollbox-horizontal-bottom-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbox-inner-small-padding .simplebar-track.simplebar-vertical {
|
|
||||||
right: 8px;
|
|
||||||
bottom: 8px;
|
|
||||||
top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbox-inner-small-padding .simplebar-track.simplebar-horizontal {
|
|
||||||
bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simplebar-scrollbar:before {
|
.simplebar-scrollbar:before {
|
||||||
background: #78787854 !important;
|
background: #78787854 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,8 +22,7 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Form,
|
Form,
|
||||||
Splitter,
|
Splitter
|
||||||
Card
|
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
@ -50,7 +49,6 @@ import { useActions } from '../context/ActionsContext'
|
|||||||
import ActionsIcon from '../../Icons/ActionsIcon'
|
import ActionsIcon from '../../Icons/ActionsIcon'
|
||||||
import FilterIcon from '../../Icons/FilterIcon'
|
import FilterIcon from '../../Icons/FilterIcon'
|
||||||
import ScrollBox from './ScrollBox'
|
import ScrollBox from './ScrollBox'
|
||||||
import SimplePropertyFilter from './SimplePropertyFilter'
|
|
||||||
import {
|
import {
|
||||||
getActiveFilterValues,
|
getActiveFilterValues,
|
||||||
useTableStatePersistence
|
useTableStatePersistence
|
||||||
@ -70,120 +68,6 @@ const getCardColSpan = (containerWidth) => {
|
|||||||
return 24
|
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 ColumnFilterDropdown = ({
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
visible,
|
|
||||||
propertyName,
|
|
||||||
propertyLabel,
|
|
||||||
modelType
|
|
||||||
}) => {
|
|
||||||
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 = (e) => {
|
|
||||||
const text = e.target.value
|
|
||||||
setExpression(text)
|
|
||||||
setDraft(fromFilterExpression(text) || [])
|
|
||||||
}
|
|
||||||
|
|
||||||
const applyFilter = () => {
|
|
||||||
const trimmed = expression.trim()
|
|
||||||
if (!trimmed) {
|
|
||||||
clearFilters()
|
|
||||||
} else if (draft?.length) {
|
|
||||||
setSelectedKeys(draft)
|
|
||||||
} else {
|
|
||||||
setSelectedKeys([trimmed])
|
|
||||||
}
|
|
||||||
confirm()
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetFilter = () => {
|
|
||||||
setExpression('')
|
|
||||||
setDraft([])
|
|
||||||
clearFilters()
|
|
||||||
confirm()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 8 }}>
|
|
||||||
<Flex vertical gap='small'>
|
|
||||||
<Space.Compact>
|
|
||||||
<Input
|
|
||||||
placeholder={'Filter ' + propertyLabel}
|
|
||||||
value={expression}
|
|
||||||
onChange={handleExpressionChange}
|
|
||||||
onPressEnter={applyFilter}
|
|
||||||
style={{ width: 200, display: 'block' }}
|
|
||||||
/>
|
|
||||||
<Button onClick={resetFilter} icon={<XMarkIcon />} />
|
|
||||||
<Button type='primary' onClick={applyFilter} icon={<CheckIcon />} />
|
|
||||||
</Space.Compact>
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</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
|
|
||||||
}
|
|
||||||
|
|
||||||
const RowForm = ({ record, isEditing, onRegister, children }) => {
|
const RowForm = ({ record, isEditing, onRegister, children }) => {
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -1089,21 +973,36 @@ const ObjectTable = forwardRef(
|
|||||||
selectedKeys,
|
selectedKeys,
|
||||||
confirm,
|
confirm,
|
||||||
clearFilters,
|
clearFilters,
|
||||||
visible,
|
propertyName
|
||||||
propertyName,
|
}) => {
|
||||||
propertyLabel
|
return (
|
||||||
}) => (
|
<div style={{ padding: 8 }}>
|
||||||
<ColumnFilterDropdown
|
<Space.Compact>
|
||||||
setSelectedKeys={setSelectedKeys}
|
<Input
|
||||||
selectedKeys={selectedKeys}
|
placeholder={'Search ' + propertyName}
|
||||||
confirm={confirm}
|
value={selectedKeys[0]}
|
||||||
clearFilters={clearFilters}
|
onChange={(e) =>
|
||||||
visible={visible}
|
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||||
propertyName={propertyName}
|
}
|
||||||
propertyLabel={propertyLabel}
|
onPressEnter={() => confirm()}
|
||||||
modelType={type}
|
style={{ width: 200, display: 'block' }}
|
||||||
/>
|
/>
|
||||||
)
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
clearFilters()
|
||||||
|
confirm()
|
||||||
|
}}
|
||||||
|
icon={<XMarkIcon />}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
onClick={() => confirm()}
|
||||||
|
icon={<CheckIcon />}
|
||||||
|
/>
|
||||||
|
</Space.Compact>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
if (isInternalSortUpdateRef.current) return
|
if (isInternalSortUpdateRef.current) return
|
||||||
@ -1111,9 +1010,8 @@ const ObjectTable = forwardRef(
|
|||||||
const next = { ...sidebarFilter }
|
const next = { ...sidebarFilter }
|
||||||
|
|
||||||
Object.entries(filters).forEach(([key, value]) => {
|
Object.entries(filters).forEach(([key, value]) => {
|
||||||
const expression = toFilterExpression(value)
|
if (value && value.length > 0) {
|
||||||
if (expression !== undefined) {
|
next[key] = value[0]
|
||||||
next[key] = expression
|
|
||||||
} else {
|
} else {
|
||||||
delete next[key]
|
delete next[key]
|
||||||
}
|
}
|
||||||
@ -1284,21 +1182,18 @@ const ObjectTable = forwardRef(
|
|||||||
setSelectedKeys,
|
setSelectedKeys,
|
||||||
selectedKeys,
|
selectedKeys,
|
||||||
confirm,
|
confirm,
|
||||||
clearFilters,
|
clearFilters
|
||||||
visible
|
|
||||||
}) =>
|
}) =>
|
||||||
getFilterDropdown({
|
getFilterDropdown({
|
||||||
setSelectedKeys,
|
setSelectedKeys,
|
||||||
selectedKeys,
|
selectedKeys,
|
||||||
confirm,
|
confirm,
|
||||||
clearFilters,
|
clearFilters,
|
||||||
visible,
|
propertyName: prop.label
|
||||||
propertyName: prop.name,
|
|
||||||
propertyLabel: prop.label
|
|
||||||
})
|
})
|
||||||
columnConfig.filteredValue = fromFilterExpression(
|
const sidebarVal = sidebarFilter[prop.name]
|
||||||
sidebarFilter[prop.name]
|
columnConfig.filteredValue =
|
||||||
)
|
sidebarVal !== undefined && sidebarVal !== '' ? [sidebarVal] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
columnsWithSkeleton.push(columnConfig)
|
columnsWithSkeleton.push(columnConfig)
|
||||||
|
|||||||
@ -7,8 +7,6 @@ const ScrollBox = ({
|
|||||||
style,
|
style,
|
||||||
horizontalBottomPadding = 16,
|
horizontalBottomPadding = 16,
|
||||||
verticalRightPadding = 16,
|
verticalRightPadding = 16,
|
||||||
inner = false,
|
|
||||||
smallPadding = false,
|
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
@ -19,7 +17,6 @@ const ScrollBox = ({
|
|||||||
'--scrollbox-vertical-right-padding': `${verticalRightPadding}px`,
|
'--scrollbox-vertical-right-padding': `${verticalRightPadding}px`,
|
||||||
'--scrollbox-horizontal-bottom-padding': `${horizontalBottomPadding}px`
|
'--scrollbox-horizontal-bottom-padding': `${horizontalBottomPadding}px`
|
||||||
}}
|
}}
|
||||||
className={`${inner ? 'scrollbox-inner' : ''} ${smallPadding ? 'scrollbox-inner-small-padding' : ''}`}
|
|
||||||
>
|
>
|
||||||
<SimpleBar style={{ height: '100%', ...style }} {...rest}>
|
<SimpleBar style={{ height: '100%', ...style }} {...rest}>
|
||||||
{children}
|
{children}
|
||||||
@ -32,9 +29,7 @@ ScrollBox.propTypes = {
|
|||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
horizontalBottomPadding: PropTypes.number,
|
horizontalBottomPadding: PropTypes.number,
|
||||||
verticalRightPadding: PropTypes.number,
|
verticalRightPadding: PropTypes.number
|
||||||
inner: PropTypes.bool,
|
|
||||||
smallPadding: PropTypes.bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ScrollBox
|
export default ScrollBox
|
||||||
|
|||||||
@ -1,180 +0,0 @@
|
|||||||
import { useState, useEffect, useContext, useMemo } from 'react'
|
|
||||||
import { Flex, Checkbox, Spin, Typography } from 'antd'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { ApiServerContext } from '../context/ApiServerContext'
|
|
||||||
import {
|
|
||||||
getModelByName,
|
|
||||||
getModelProperties
|
|
||||||
} from '../../../database/ObjectModels'
|
|
||||||
import ObjectProperty from './ObjectProperty'
|
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
|
||||||
const { Text } = Typography
|
|
||||||
|
|
||||||
const getOptionKey = (option) => {
|
|
||||||
if (option && typeof option === 'object') {
|
|
||||||
if (option.objectType) {
|
|
||||||
const { prefix } = getModelByName(option.objectType)
|
|
||||||
if (option._reference != null) return `${prefix}:${option._reference}`
|
|
||||||
if (option._id != null) return `${prefix}:${option._id}`
|
|
||||||
}
|
|
||||||
return String(option._id ?? option.type ?? JSON.stringify(option))
|
|
||||||
}
|
|
||||||
return String(option)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDisplayValue = (option, property) => {
|
|
||||||
if (!property) return option
|
|
||||||
if (property.type === 'object') {
|
|
||||||
if (option && typeof option === 'object' && option._id) return option
|
|
||||||
if (option != null) return { _id: option }
|
|
||||||
}
|
|
||||||
if (property.type === 'state') {
|
|
||||||
if (option && typeof option === 'object' && option.type) return option
|
|
||||||
if (option != null) return { type: option }
|
|
||||||
}
|
|
||||||
return option
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchOptions = (options, selected) => {
|
|
||||||
if (!selected?.length) return []
|
|
||||||
const selectedKeys = new Set(selected.map(getOptionKey))
|
|
||||||
return options.filter((option) => selectedKeys.has(getOptionKey(option)))
|
|
||||||
}
|
|
||||||
|
|
||||||
const SimplePropertyFilter = ({
|
|
||||||
modelType,
|
|
||||||
propertyName,
|
|
||||||
value = [],
|
|
||||||
onChange,
|
|
||||||
search = ''
|
|
||||||
}) => {
|
|
||||||
const { getModelPropertyValues } = useContext(ApiServerContext)
|
|
||||||
const [options, setOptions] = useState([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [localChecked, setLocalChecked] = useState(null)
|
|
||||||
|
|
||||||
const property = useMemo(
|
|
||||||
() =>
|
|
||||||
getModelProperties(modelType).find((prop) => prop.name === propertyName),
|
|
||||||
[modelType, propertyName]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false
|
|
||||||
|
|
||||||
const loadOptions = async () => {
|
|
||||||
if (!modelType || !propertyName) return
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const values = await getModelPropertyValues(modelType, propertyName)
|
|
||||||
if (cancelled) return
|
|
||||||
const unique = [
|
|
||||||
...new Set((values || []).filter((v) => v != null && v !== ''))
|
|
||||||
]
|
|
||||||
setOptions(unique)
|
|
||||||
} finally {
|
|
||||||
if (!cancelled) setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadOptions()
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [modelType, propertyName, getModelPropertyValues])
|
|
||||||
|
|
||||||
const valueKey = useMemo(
|
|
||||||
() => JSON.stringify((value || []).map(getOptionKey)),
|
|
||||||
[value]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sync checkbox state from the applied filter (sidebar / column filteredValue).
|
|
||||||
// No active filter => all options checked by default.
|
|
||||||
useEffect(() => {
|
|
||||||
if (options.length === 0) return
|
|
||||||
if (value?.length > 0) {
|
|
||||||
const matched = matchOptions(options, value)
|
|
||||||
const matchedKeys = matched.map(getOptionKey)
|
|
||||||
setLocalChecked(
|
|
||||||
matchedKeys.length > 0 ? matchedKeys : value.map(getOptionKey)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setLocalChecked(options.map(getOptionKey))
|
|
||||||
}
|
|
||||||
// valueKey captures value contents; value is read for matching
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [options, valueKey])
|
|
||||||
|
|
||||||
const filteredOptions = useMemo(() => {
|
|
||||||
const query = search.trim().toLowerCase()
|
|
||||||
if (!query) return options
|
|
||||||
return options.filter((option) =>
|
|
||||||
getOptionKey(option).toLowerCase().includes(query)
|
|
||||||
)
|
|
||||||
}, [options, search])
|
|
||||||
|
|
||||||
const optionKeys = useMemo(() => options.map(getOptionKey), [options])
|
|
||||||
const checkedKeys = localChecked ?? optionKeys
|
|
||||||
|
|
||||||
const emitChange = (nextKeys) => {
|
|
||||||
setLocalChecked(nextKeys)
|
|
||||||
// All selected (or empty options) means no filter applied
|
|
||||||
if (optionKeys.length > 0 && nextKeys.length === optionKeys.length) {
|
|
||||||
onChange?.([])
|
|
||||||
} else {
|
|
||||||
onChange?.(nextKeys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = (visibleCheckedKeys) => {
|
|
||||||
const filteredKeys = new Set(filteredOptions.map(getOptionKey))
|
|
||||||
const hiddenSelected = checkedKeys.filter((key) => !filteredKeys.has(key))
|
|
||||||
emitChange([...hiddenSelected, ...visibleCheckedKeys])
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Spin spinning={loading} indicator={<LoadingOutlined spin />}>
|
|
||||||
<Checkbox.Group
|
|
||||||
value={checkedKeys}
|
|
||||||
onChange={handleChange}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<Flex vertical gap={16} style={{ minWidth: 0 }}>
|
|
||||||
{filteredOptions.map((option) => (
|
|
||||||
<Flex gap={14} key={getOptionKey(option)} align='center'>
|
|
||||||
<Checkbox
|
|
||||||
value={getOptionKey(option)}
|
|
||||||
style={{ minWidth: 0 }}
|
|
||||||
></Checkbox>
|
|
||||||
<div style={{ minWidth: 0 }}>
|
|
||||||
{property ? (
|
|
||||||
<ObjectProperty
|
|
||||||
{...property}
|
|
||||||
objectType={option?.objectType ?? property.objectType}
|
|
||||||
modelType={modelType}
|
|
||||||
value={getDisplayValue(option, property)}
|
|
||||||
inTable={true}
|
|
||||||
isEditing={false}
|
|
||||||
showLabel={false}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text>{String(option)}</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
</Checkbox.Group>
|
|
||||||
</Spin>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SimplePropertyFilter.propTypes = {
|
|
||||||
modelType: PropTypes.string,
|
|
||||||
propertyName: PropTypes.string,
|
|
||||||
value: PropTypes.array,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
search: PropTypes.string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SimplePropertyFilter
|
|
||||||
Loading…
x
Reference in New Issue
Block a user