Compare commits
2 Commits
a896cdf223
...
62805b0d6a
| Author | SHA1 | Date | |
|---|---|---|---|
| 62805b0d6a | |||
| 612b250529 |
@ -196,6 +196,7 @@ code {
|
|||||||
/* --- Start of src/index.css --- */
|
/* --- Start of src/index.css --- */
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-modal-mask {
|
.ant-modal-mask {
|
||||||
@ -602,3 +603,32 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 20px;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { ElectronContext } from '../context/ElectronContext'
|
import { ElectronContext } from '../context/ElectronContext'
|
||||||
import { getSidebarIconNode } from '../../Icons/sidebarIconMap'
|
import { getSidebarIconNode } from '../../Icons/sidebarIconMap'
|
||||||
|
import SimpleBar from 'simplebar-react'
|
||||||
|
import { useThemeContext } from '../context/ThemeContext'
|
||||||
const { Sider } = Layout
|
const { Sider } = Layout
|
||||||
|
|
||||||
const DashboardSidebar = ({
|
const DashboardSidebar = ({
|
||||||
@ -24,7 +26,7 @@ const DashboardSidebar = ({
|
|||||||
})
|
})
|
||||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { isDarkMode } = useThemeContext()
|
||||||
const { isElectron } = useContext(ElectronContext)
|
const { isElectron } = useContext(ElectronContext)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -90,11 +92,14 @@ const DashboardSidebar = ({
|
|||||||
className={isElectron ? 'electron-sider' : ''}
|
className={isElectron ? 'electron-sider' : ''}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
style={{ height: '100%' }}
|
style={{ height: '100%', minHeight: 0 }}
|
||||||
vertical
|
vertical
|
||||||
className='ant-menu-root ant-menu-inline ant-menu-light'
|
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
|
<Menu
|
||||||
mode='inline'
|
mode='inline'
|
||||||
selectedKeys={[selectedKey]}
|
selectedKeys={[selectedKey]}
|
||||||
@ -103,7 +108,7 @@ const DashboardSidebar = ({
|
|||||||
style={{ flexGrow: 1, border: 'none' }}
|
style={{ flexGrow: 1, border: 'none' }}
|
||||||
_internalDisableMenuItemTitleTooltip
|
_internalDisableMenuItemTitleTooltip
|
||||||
/>
|
/>
|
||||||
</div>
|
</SimpleBar>
|
||||||
<Flex style={{ padding: '4px', width: '100%' }}>
|
<Flex style={{ padding: '4px', width: '100%' }}>
|
||||||
<Button
|
<Button
|
||||||
size={isElectron ? 'middle' : 'large'}
|
size={isElectron ? 'middle' : 'large'}
|
||||||
|
|||||||
@ -26,9 +26,19 @@ const areValuesEqual = (v1, v2) => {
|
|||||||
return String(id1) === String(id2)
|
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 = ({
|
const ObjectSelect = ({
|
||||||
type = 'unknown',
|
type = 'unknown',
|
||||||
showSearch = false,
|
showSearch = true,
|
||||||
multiple = false,
|
multiple = false,
|
||||||
treeSelectProps = EMPTY_OBJECT,
|
treeSelectProps = EMPTY_OBJECT,
|
||||||
filter = EMPTY_OBJECT,
|
filter = EMPTY_OBJECT,
|
||||||
@ -38,7 +48,7 @@ const ObjectSelect = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const { fetchObjectsByProperty, fetchObject, connected } =
|
const { fetchObjectsByProperty, fetchObject, connected, searchObjects } =
|
||||||
useContext(ApiServerContext)
|
useContext(ApiServerContext)
|
||||||
const { token } = useContext(AuthContext)
|
const { token } = useContext(AuthContext)
|
||||||
// --- State ---
|
// --- State ---
|
||||||
@ -52,6 +62,9 @@ const ObjectSelect = ({
|
|||||||
const [initialLoading, setInitialLoading] = useState(true)
|
const [initialLoading, setInitialLoading] = useState(true)
|
||||||
const [expandedKeys, setExpandedKeys] = useState([])
|
const [expandedKeys, setExpandedKeys] = useState([])
|
||||||
const [treeVersion, setTreeVersion] = useState(0)
|
const [treeVersion, setTreeVersion] = useState(0)
|
||||||
|
const [isSearching, setIsSearching] = useState(false)
|
||||||
|
const [searchValue, setSearchValue] = useState('')
|
||||||
|
const searchRequestIdRef = useRef(0)
|
||||||
const valueRef = useRef(null)
|
const valueRef = useRef(null)
|
||||||
|
|
||||||
// Refs to track value changes
|
// Refs to track value changes
|
||||||
@ -345,6 +358,7 @@ const ObjectSelect = ({
|
|||||||
const onTreeSelectChange = useCallback(
|
const onTreeSelectChange = useCallback(
|
||||||
(value) => {
|
(value) => {
|
||||||
// Mark this as an internal change
|
// Mark this as an internal change
|
||||||
|
setIsSearching(false)
|
||||||
isInternalChangeRef.current = true
|
isInternalChangeRef.current = true
|
||||||
|
|
||||||
// value can be a string (single) or array (multiple)
|
// value can be a string (single) or array (multiple)
|
||||||
@ -368,8 +382,55 @@ const ObjectSelect = ({
|
|||||||
[multiple, objectList, onChange]
|
[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
|
// Update treeData when objectPropertiesTree changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isSearching) return
|
||||||
if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) {
|
if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) {
|
||||||
const newTreeData = buildTreeData(objectPropertiesTree)
|
const newTreeData = buildTreeData(objectPropertiesTree)
|
||||||
setTreeData((prev) => {
|
setTreeData((prev) => {
|
||||||
@ -379,7 +440,7 @@ const ObjectSelect = ({
|
|||||||
return prev
|
return prev
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [objectPropertiesTree, properties, buildTreeData])
|
}, [objectPropertiesTree, properties, buildTreeData, isSearching])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleValue = async () => {
|
const handleValue = async () => {
|
||||||
@ -480,6 +541,9 @@ const ObjectSelect = ({
|
|||||||
JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)
|
JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged) {
|
||||||
|
searchRequestIdRef.current += 1
|
||||||
|
setIsSearching(false)
|
||||||
|
setSearchValue('')
|
||||||
setObjectPropertiesTree({})
|
setObjectPropertiesTree({})
|
||||||
setObjectList([])
|
setObjectList([])
|
||||||
setTreeData([])
|
setTreeData([])
|
||||||
@ -559,14 +623,18 @@ const ObjectSelect = ({
|
|||||||
onTreeExpand={setExpandedKeys}
|
onTreeExpand={setExpandedKeys}
|
||||||
treeData={treeData}
|
treeData={treeData}
|
||||||
showSearch={showSearch}
|
showSearch={showSearch}
|
||||||
|
filterTreeNode={() => true}
|
||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
loadData={loadData}
|
loadData={isSearching ? undefined : loadData}
|
||||||
showCheckedStrategy={SHOW_CHILD}
|
showCheckedStrategy={SHOW_CHILD}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
{...treeSelectProps}
|
{...treeSelectProps}
|
||||||
{...rest}
|
{...rest}
|
||||||
value={treeSelectValue}
|
value={treeSelectValue}
|
||||||
onChange={onTreeSelectChange}
|
onChange={onTreeSelectChange}
|
||||||
|
onSearch={onSearch}
|
||||||
|
onInputKeyDown={onInputKeyDown}
|
||||||
|
searchValue={searchValue}
|
||||||
disabled={disabled || type == 'unknown' || type == undefined}
|
disabled={disabled || type == 'unknown' || type == undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -744,20 +744,23 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
[offLockEvent]
|
[offLockEvent]
|
||||||
)
|
)
|
||||||
|
|
||||||
const showError = useCallback((error, callback = null) => {
|
const showError = useCallback(
|
||||||
const code = error.response.data.code || 'UNKNOWN'
|
(error, callback = null) => {
|
||||||
if (code == 'UNAUTHORIZED') {
|
const code = error.response.data.code || 'UNKNOWN'
|
||||||
setUnauthenticated()
|
if (code == 'UNAUTHORIZED') {
|
||||||
return
|
setUnauthenticated()
|
||||||
}
|
return
|
||||||
var content = `Error ${error.code} (${error.status}): ${error.message}`
|
}
|
||||||
if (error.response?.data?.error) {
|
var content = `Error ${error.code} (${error.status}): ${error.message}`
|
||||||
content = `${error.response?.data?.error} (${error.status})`
|
if (error.response?.data?.error) {
|
||||||
}
|
content = `${error.response?.data?.error} (${error.status})`
|
||||||
setErrorModalContent(content)
|
}
|
||||||
setRetryCallback(() => callback)
|
setErrorModalContent(content)
|
||||||
setShowErrorModal(true)
|
setRetryCallback(() => callback)
|
||||||
}, [setUnauthenticated])
|
setShowErrorModal(true)
|
||||||
|
},
|
||||||
|
[setUnauthenticated]
|
||||||
|
)
|
||||||
|
|
||||||
const handleRetry = () => {
|
const handleRetry = () => {
|
||||||
setShowErrorModal(false)
|
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
|
// Update filament information
|
||||||
const updateObject = async (id, type, value) => {
|
const updateObject = async (id, type, value) => {
|
||||||
const updateUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
|
const updateUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
|
||||||
@ -1824,6 +1848,7 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
fetchObject,
|
fetchObject,
|
||||||
fetchObjects,
|
fetchObjects,
|
||||||
fetchObjectsByProperty,
|
fetchObjectsByProperty,
|
||||||
|
searchObjects,
|
||||||
fetchSpotlightData,
|
fetchSpotlightData,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
getModelHistory,
|
getModelHistory,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user