diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css
index 773bbb2..4d567f8 100644
--- a/assets/stylesheets/App.css
+++ b/assets/stylesheets/App.css
@@ -513,6 +513,24 @@ body {
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 {
background: #78787854 !important;
}
diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx
index 81337d4..0a67f1a 100644
--- a/src/components/Dashboard/common/ObjectTable.jsx
+++ b/src/components/Dashboard/common/ObjectTable.jsx
@@ -22,7 +22,8 @@ import {
Space,
Tooltip,
Form,
- Splitter
+ Splitter,
+ Card
} from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import PropTypes from 'prop-types'
@@ -49,6 +50,7 @@ import { useActions } from '../context/ActionsContext'
import ActionsIcon from '../../Icons/ActionsIcon'
import FilterIcon from '../../Icons/FilterIcon'
import ScrollBox from './ScrollBox'
+import SimplePropertyFilter from './SimplePropertyFilter'
import {
getActiveFilterValues,
useTableStatePersistence
@@ -68,6 +70,120 @@ const getCardColSpan = (containerWidth) => {
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 (
+
+
+
+
+ } />
+ } />
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+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 [form] = Form.useForm()
useEffect(() => {
@@ -973,36 +1089,21 @@ const ObjectTable = forwardRef(
selectedKeys,
confirm,
clearFilters,
- propertyName
- }) => {
- return (
-
-
-
- setSelectedKeys(e.target.value ? [e.target.value] : [])
- }
- onPressEnter={() => confirm()}
- style={{ width: 200, display: 'block' }}
- />
-
-
- )
- }
+ visible,
+ propertyName,
+ propertyLabel
+ }) => (
+
+ )
const handleTableChange = (pagination, filters, sorter) => {
if (isInternalSortUpdateRef.current) return
@@ -1010,8 +1111,9 @@ const ObjectTable = forwardRef(
const next = { ...sidebarFilter }
Object.entries(filters).forEach(([key, value]) => {
- if (value && value.length > 0) {
- next[key] = value[0]
+ const expression = toFilterExpression(value)
+ if (expression !== undefined) {
+ next[key] = expression
} else {
delete next[key]
}
@@ -1182,18 +1284,21 @@ const ObjectTable = forwardRef(
setSelectedKeys,
selectedKeys,
confirm,
- clearFilters
+ clearFilters,
+ visible
}) =>
getFilterDropdown({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
- propertyName: prop.label
+ visible,
+ propertyName: prop.name,
+ propertyLabel: prop.label
})
- const sidebarVal = sidebarFilter[prop.name]
- columnConfig.filteredValue =
- sidebarVal !== undefined && sidebarVal !== '' ? [sidebarVal] : null
+ columnConfig.filteredValue = fromFilterExpression(
+ sidebarFilter[prop.name]
+ )
}
columnsWithSkeleton.push(columnConfig)
diff --git a/src/components/Dashboard/common/ScrollBox.jsx b/src/components/Dashboard/common/ScrollBox.jsx
index 3c66096..2a3db87 100644
--- a/src/components/Dashboard/common/ScrollBox.jsx
+++ b/src/components/Dashboard/common/ScrollBox.jsx
@@ -7,6 +7,8 @@ const ScrollBox = ({
style,
horizontalBottomPadding = 16,
verticalRightPadding = 16,
+ inner = false,
+ smallPadding = false,
...rest
}) => {
return (
@@ -17,6 +19,7 @@ const ScrollBox = ({
'--scrollbox-vertical-right-padding': `${verticalRightPadding}px`,
'--scrollbox-horizontal-bottom-padding': `${horizontalBottomPadding}px`
}}
+ className={`${inner ? 'scrollbox-inner' : ''} ${smallPadding ? 'scrollbox-inner-small-padding' : ''}`}
>
{children}
@@ -29,7 +32,9 @@ ScrollBox.propTypes = {
children: PropTypes.node,
style: PropTypes.object,
horizontalBottomPadding: PropTypes.number,
- verticalRightPadding: PropTypes.number
+ verticalRightPadding: PropTypes.number,
+ inner: PropTypes.bool,
+ smallPadding: PropTypes.bool
}
export default ScrollBox
diff --git a/src/components/Dashboard/common/SimplePropertyFilter.jsx b/src/components/Dashboard/common/SimplePropertyFilter.jsx
new file mode 100644
index 0000000..7c98e16
--- /dev/null
+++ b/src/components/Dashboard/common/SimplePropertyFilter.jsx
@@ -0,0 +1,168 @@
+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 { 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') {
+ 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)
+ setLocalChecked(matched.length > 0 ? matched : [...value])
+ } else {
+ setLocalChecked(options)
+ }
+ // 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 checkedValues = localChecked ?? options
+
+ const emitChange = (next) => {
+ setLocalChecked(next)
+ // All selected (or empty options) means no filter applied
+ if (options.length > 0 && next.length === options.length) {
+ onChange?.([])
+ } else {
+ onChange?.(next)
+ }
+ }
+
+ const handleChange = (visibleChecked) => {
+ const hiddenSelected = checkedValues.filter(
+ (checked) =>
+ !filteredOptions.some(
+ (option) => getOptionKey(option) === getOptionKey(checked)
+ )
+ )
+ emitChange([...hiddenSelected, ...visibleChecked])
+ }
+
+ return (
+ }>
+
+
+ {filteredOptions.map((option) => (
+
+
+
+ {property ? (
+
+ ) : (
+ {String(option)}
+ )}
+
+
+ ))}
+
+
+
+ )
+}
+
+SimplePropertyFilter.propTypes = {
+ modelType: PropTypes.string,
+ propertyName: PropTypes.string,
+ value: PropTypes.array,
+ onChange: PropTypes.func,
+ search: PropTypes.string
+}
+
+export default SimplePropertyFilter