Compare commits

..

2 Commits

4 changed files with 150 additions and 22 deletions

View File

@ -196,6 +196,7 @@ code {
/* --- Start of src/index.css --- */
body {
margin: 0;
overflow: hidden;
}
.ant-modal-mask {
@ -602,3 +603,32 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
min-width: 0;
height: 20px;
}
.sidebar-scroll .simplebar-track.simplebar-vertical {
right: -1px;
width: 0px !important;
transition:
width 0.15s ease-in-out,
right 0.15s ease-in-out;
}
.sidebar-scroll.dark .simplebar-track.simplebar-vertical {
background-color: #141414;
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
border-bottom: 1px solid rgba(253, 253, 253, 0.12);
border-radius: 0 0 5px 0;
}
.sidebar-scroll.simplebar-scrolling .simplebar-track.simplebar-vertical {
width: 10px !important;
right: -11px;
}
.sidebar-scroll
.simplebar-track.simplebar-vertical
.simplebar-scrollbar:before {
top: 3px;
bottom: 3px;
left: 3px;
right: 3px;
}

View File

@ -8,6 +8,8 @@ import { useNavigate } from 'react-router-dom'
import PropTypes from 'prop-types'
import { ElectronContext } from '../context/ElectronContext'
import { getSidebarIconNode } from '../../Icons/sidebarIconMap'
import SimpleBar from 'simplebar-react'
import { useThemeContext } from '../context/ThemeContext'
const { Sider } = Layout
const DashboardSidebar = ({
@ -24,7 +26,7 @@ const DashboardSidebar = ({
})
const isMobile = useMediaQuery({ maxWidth: 768 })
const navigate = useNavigate()
const { isDarkMode } = useThemeContext()
const { isElectron } = useContext(ElectronContext)
useEffect(() => {
@ -90,11 +92,14 @@ const DashboardSidebar = ({
className={isElectron ? 'electron-sider' : ''}
>
<Flex
style={{ height: '100%' }}
style={{ height: '100%', minHeight: 0 }}
vertical
className='ant-menu-root ant-menu-inline ant-menu-light'
>
<div style={{ maxHeight: '100%', overflowY: 'auto', height: '100%' }}>
<SimpleBar
style={{ flexGrow: 1, minHeight: 0 }}
className={`sidebar-scroll ${isDarkMode ? 'dark' : 'light'}`}
>
<Menu
mode='inline'
selectedKeys={[selectedKey]}
@ -103,7 +108,7 @@ const DashboardSidebar = ({
style={{ flexGrow: 1, border: 'none' }}
_internalDisableMenuItemTitleTooltip
/>
</div>
</SimpleBar>
<Flex style={{ padding: '4px', width: '100%' }}>
<Button
size={isElectron ? 'middle' : 'large'}

View File

@ -26,9 +26,19 @@ const areValuesEqual = (v1, v2) => {
return String(id1) === String(id2)
}
const getFirstSelectableLeaf = (nodes) => {
if (!Array.isArray(nodes)) return null
for (const node of nodes) {
if (node.isLeaf && node.selectable !== false) return node
const found = getFirstSelectableLeaf(node.children)
if (found) return found
}
return null
}
const ObjectSelect = ({
type = 'unknown',
showSearch = false,
showSearch = true,
multiple = false,
treeSelectProps = EMPTY_OBJECT,
filter = EMPTY_OBJECT,
@ -38,7 +48,7 @@ const ObjectSelect = ({
disabled = false,
...rest
}) => {
const { fetchObjectsByProperty, fetchObject, connected } =
const { fetchObjectsByProperty, fetchObject, connected, searchObjects } =
useContext(ApiServerContext)
const { token } = useContext(AuthContext)
// --- State ---
@ -52,6 +62,9 @@ const ObjectSelect = ({
const [initialLoading, setInitialLoading] = useState(true)
const [expandedKeys, setExpandedKeys] = useState([])
const [treeVersion, setTreeVersion] = useState(0)
const [isSearching, setIsSearching] = useState(false)
const [searchValue, setSearchValue] = useState('')
const searchRequestIdRef = useRef(0)
const valueRef = useRef(null)
// Refs to track value changes
@ -345,6 +358,7 @@ const ObjectSelect = ({
const onTreeSelectChange = useCallback(
(value) => {
// Mark this as an internal change
setIsSearching(false)
isInternalChangeRef.current = true
// value can be a string (single) or array (multiple)
@ -368,8 +382,55 @@ const ObjectSelect = ({
[multiple, objectList, onChange]
)
const onSearch = useCallback(
async (search) => {
setSearchValue(search)
const trimmed = search?.trim() ?? ''
if (!trimmed) {
searchRequestIdRef.current += 1
setIsSearching(false)
setTreeData(buildTreeData(objectPropertiesTree))
return
}
const requestId = ++searchRequestIdRef.current
setIsSearching(true)
const data = await searchObjects(type, trimmed)
if (requestId !== searchRequestIdRef.current) return
if (!Array.isArray(data)) return
setTreeData(buildTreeData(data, properties.length))
},
[
searchObjects,
type,
buildTreeData,
objectPropertiesTree,
properties.length
]
)
const onInputKeyDown = useCallback(
(e) => {
treeSelectProps.onInputKeyDown?.(e)
if (e.defaultPrevented || e.key !== 'Enter' || !isSearching) return
const firstLeaf = getFirstSelectableLeaf(treeData)
if (!firstLeaf?.value) return
e.preventDefault()
e.stopPropagation()
onTreeSelectChange(
multiple ? [firstLeaf.value] : firstLeaf.value
)
setSearchValue('')
onSearch('')
},
[treeSelectProps, isSearching, treeData, multiple, onTreeSelectChange, onSearch]
)
// Update treeData when objectPropertiesTree changes
useEffect(() => {
if (isSearching) return
if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) {
const newTreeData = buildTreeData(objectPropertiesTree)
setTreeData((prev) => {
@ -379,7 +440,7 @@ const ObjectSelect = ({
return prev
})
}
}, [objectPropertiesTree, properties, buildTreeData])
}, [objectPropertiesTree, properties, buildTreeData, isSearching])
useEffect(() => {
const handleValue = async () => {
@ -480,6 +541,9 @@ const ObjectSelect = ({
JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)
if (hasChanged) {
searchRequestIdRef.current += 1
setIsSearching(false)
setSearchValue('')
setObjectPropertiesTree({})
setObjectList([])
setTreeData([])
@ -559,14 +623,18 @@ const ObjectSelect = ({
onTreeExpand={setExpandedKeys}
treeData={treeData}
showSearch={showSearch}
filterTreeNode={() => true}
multiple={multiple}
loadData={loadData}
loadData={isSearching ? undefined : loadData}
showCheckedStrategy={SHOW_CHILD}
placeholder={placeholder}
{...treeSelectProps}
{...rest}
value={treeSelectValue}
onChange={onTreeSelectChange}
onSearch={onSearch}
onInputKeyDown={onInputKeyDown}
searchValue={searchValue}
disabled={disabled || type == 'unknown' || type == undefined}
/>
)

View File

@ -744,20 +744,23 @@ const ApiServerProvider = ({ children }) => {
[offLockEvent]
)
const showError = useCallback((error, callback = null) => {
const code = error.response.data.code || 'UNKNOWN'
if (code == 'UNAUTHORIZED') {
setUnauthenticated()
return
}
var content = `Error ${error.code} (${error.status}): ${error.message}`
if (error.response?.data?.error) {
content = `${error.response?.data?.error} (${error.status})`
}
setErrorModalContent(content)
setRetryCallback(() => callback)
setShowErrorModal(true)
}, [setUnauthenticated])
const showError = useCallback(
(error, callback = null) => {
const code = error.response.data.code || 'UNKNOWN'
if (code == 'UNAUTHORIZED') {
setUnauthenticated()
return
}
var content = `Error ${error.code} (${error.status}): ${error.message}`
if (error.response?.data?.error) {
content = `${error.response?.data?.error} (${error.status})`
}
setErrorModalContent(content)
setRetryCallback(() => callback)
setShowErrorModal(true)
},
[setUnauthenticated]
)
const handleRetry = () => {
setShowErrorModal(false)
@ -911,6 +914,27 @@ const ApiServerProvider = ({ children }) => {
}
}
const searchObjects = async (type, search) => {
const searchUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/search`
logger.debug('Searching objects for ' + type, { search })
try {
const response = await axios.get(searchUrl, {
params: { search },
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
return response.data
} catch (err) {
console.error(err)
showError(err, () => {
searchObjects(type, search)
})
return []
}
}
// Update filament information
const updateObject = async (id, type, value) => {
const updateUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
@ -1824,6 +1848,7 @@ const ApiServerProvider = ({ children }) => {
fetchObject,
fetchObjects,
fetchObjectsByProperty,
searchObjects,
fetchSpotlightData,
getModelStats,
getModelHistory,