Compare commits

..

3 Commits

Author SHA1 Message Date
a8932f1e66 Update gcode-preview Dependency and Refactor GCodePreview Component
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Upgraded gcode-preview package from version 2.18.0 to 3.0.0-alpha.6, incorporating breaking changes and new features.
- Refactored GCodePreview component to align with the updated API, including changes to color handling and camera management.
- Improved rendering logic to utilize sceneManager for better performance and maintainability.
- Enhanced user interaction by updating event handling and resizing logic in the GCodePreview component.
2026-09-14 10:21:28 +01:00
5dca5ef42f Enhance ObjectSelect Component with Improved Value Handling and State Management
- Updated value comparison logic in areValuesEqual to handle null values and case insensitivity.
- Introduced load generation tracking to manage asynchronous updates and prevent stale data handling.
- Refactored onTreeSelectChange to improve selection handling for both single and multiple selections.
- Added useEffect hooks to manage state changes based on type and masterFilter updates, enhancing component responsiveness.
- Implemented findObjectById function for better object retrieval from the list, improving performance and clarity.
2026-09-14 10:20:53 +01:00
4d2ec19ff1 Enhance GCodePreview Component with Render Mode and Travel Visibility Options
- Added render mode selection to toggle between 'lines' and 'pipes' for GCode visualization.
- Introduced travel visibility options to show or hide travel paths in the preview.
- Updated rendering logic to reflect changes in user-selected options, improving the flexibility and usability of the GCodePreview component.
2026-09-14 09:57:30 +01:00
5 changed files with 268 additions and 184 deletions

View File

@ -56,7 +56,7 @@
"echarts": "^6.1.0",
"echarts-for-react": "^3.0.6",
"electron-store": "^11.0.2",
"gcode-preview": "^2.18.0",
"gcode-preview": "3.0.0-alpha.6",
"json-schema-traverse": "^1.0.0",
"keycloak-js": "^26.2.0",
"lodash": "^4.17.23",
@ -1500,7 +1500,7 @@
"galactus": ["galactus@1.0.0", "", { "dependencies": { "debug": "^4.3.4", "flora-colossus": "^2.0.0", "fs-extra": "^10.1.0" } }, "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ=="],
"gcode-preview": ["gcode-preview@2.18.0", "", { "dependencies": { "lil-gui": "0.19.2", "three": "0.159.0" } }, "sha512-uc9QYciG6ES/A6BWJpUZk4fHxCPvt5EnvDhHIHDbNdR/m3f9VkGvpSMh9HDygXjAXX0x1Lbz/e9ZGlIrYNB29A=="],
"gcode-preview": ["gcode-preview@3.0.0-alpha.6", "", { "dependencies": { "lil-gui": "^0.20.0", "three": ">=0.166.0 <0.186.0" } }, "sha512-pLgIhufGNdo6ChTrFeuRsjvl/eg2rH1LK4E+qUaLsL0BBWIE/phry2zfxuXHxBSdAKA5+cbXGpCmp/RsxHgVUQ=="],
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
@ -1748,7 +1748,7 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "1.2.1", "type-check": "0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lil-gui": ["lil-gui@0.19.2", "", {}, "sha512-nU8j4ND702ouGfQZoaTN4dfXxacvGOAVK0DtmZBVcUYUAeYQXLQAjAN50igMHiba3T5jZyKEjXZU+Ntm1Qs6ZQ=="],
"lil-gui": ["lil-gui@0.20.0", "", {}, "sha512-k7Ipr0ztqslMA2XvM5z5ZaWhxQtnEOwJBfI/hmSuRh6q4iMG9L0boqqrnZSzBR1jzyJ28OMl47l65ILzRe1TdA=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
@ -2894,8 +2894,6 @@
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"gcode-preview/three": ["three@0.159.0", "", {}, "sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA=="],
"get-package-info/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"get-source/data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="],

View File

@ -60,7 +60,7 @@
"echarts": "^6.1.0",
"echarts-for-react": "^3.0.6",
"electron-store": "^11.0.2",
"gcode-preview": "^2.18.0",
"gcode-preview": "3.0.0-alpha.6",
"json-schema-traverse": "^1.0.0",
"keycloak-js": "^26.2.0",
"lodash": "^4.17.23",

25
pnpm-lock.yaml generated
View File

@ -170,8 +170,8 @@ importers:
specifier: ^11.0.2
version: 11.0.2
gcode-preview:
specifier: ^2.18.0
version: 2.18.0
specifier: 3.0.0-alpha.6
version: 3.0.0-alpha.6
json-schema-traverse:
specifier: ^1.0.0
version: 1.0.0
@ -3798,8 +3798,8 @@ packages:
resolution: {integrity: sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==}
engines: {node: '>= 12'}
gcode-preview@2.18.0:
resolution: {integrity: sha512-uc9QYciG6ES/A6BWJpUZk4fHxCPvt5EnvDhHIHDbNdR/m3f9VkGvpSMh9HDygXjAXX0x1Lbz/e9ZGlIrYNB29A==}
gcode-preview@3.0.0-alpha.6:
resolution: {integrity: sha512-pLgIhufGNdo6ChTrFeuRsjvl/eg2rH1LK4E+qUaLsL0BBWIE/phry2zfxuXHxBSdAKA5+cbXGpCmp/RsxHgVUQ==}
generator-function@2.0.1:
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
@ -4325,8 +4325,8 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lil-gui@0.19.2:
resolution: {integrity: sha512-nU8j4ND702ouGfQZoaTN4dfXxacvGOAVK0DtmZBVcUYUAeYQXLQAjAN50igMHiba3T5jZyKEjXZU+Ntm1Qs6ZQ==}
lil-gui@0.20.0:
resolution: {integrity: sha512-k7Ipr0ztqslMA2XvM5z5ZaWhxQtnEOwJBfI/hmSuRh6q4iMG9L0boqqrnZSzBR1jzyJ28OMl47l65ILzRe1TdA==}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@ -6134,9 +6134,6 @@ packages:
text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
three@0.159.0:
resolution: {integrity: sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA==}
three@0.165.0:
resolution: {integrity: sha512-cc96IlVYGydeceu0e5xq70H8/yoVT/tXBxV/W8A/U6uOq7DXc4/s1Mkmnu6SqoYGhSRWWYFOhVwvq6V0VtbplA==}
@ -10857,10 +10854,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
gcode-preview@2.18.0:
gcode-preview@3.0.0-alpha.6:
dependencies:
lil-gui: 0.19.2
three: 0.159.0
lil-gui: 0.20.0
three: 0.179.1
generator-function@2.0.1: {}
@ -11443,7 +11440,7 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lil-gui@0.19.2: {}
lil-gui@0.20.0: {}
lines-and-columns@1.2.4: {}
@ -13813,8 +13810,6 @@ snapshots:
text-table@0.2.0: {}
three@0.159.0: {}
three@0.165.0: {}
three@0.176.0: {}

View File

@ -1,6 +1,6 @@
import * as GCodePreview from 'gcode-preview'
import { GCodePreview } from 'gcode-preview'
import PropTypes from 'prop-types'
import { Button, Card, Flex, Slider } from 'antd'
import { Button, Card, Flex, Select, Slider } from 'antd'
import { useCallback, useEffect, useRef, useState } from 'react'
import * as THREE from 'three'
import PlusIcon from '../../Icons/PlusIcon.jsx'
@ -15,11 +15,11 @@ import usePinchZoom, {
} from '../hooks/usePinchZoom.js'
import { useThemeContext } from '../context/ThemeContext'
function toThreeColorHex(color, fallback) {
function toCssColor(color, fallback) {
try {
return new THREE.Color(color || fallback).getHex()
return `#${new THREE.Color(color || fallback).getHexString()}`
} catch {
return new THREE.Color(fallback).getHex()
return fallback
}
}
@ -38,19 +38,21 @@ function getPreviewThemeColors({
}
}
function getCameraDistance(preview) {
const distance = preview.camera.position.distanceTo(preview.controls.target)
function getCameraDistance(sceneManager) {
const distance = sceneManager.camera.position.distanceTo(
sceneManager.controls.target
)
return Number.isFinite(distance) && distance > 0 ? distance : null
}
function getCameraScale(preview, baseDistance) {
const distance = getCameraDistance(preview)
function getCameraScale(sceneManager, baseDistance) {
const distance = getCameraDistance(sceneManager)
return distance == null ? null : clampPreviewScale(baseDistance / distance)
}
function applyCameraScale(preview, baseDistance, scale) {
const { camera, controls } = preview
const distance = getCameraDistance(preview)
function applyCameraScale(sceneManager, baseDistance, scale) {
const { camera, controls } = sceneManager
const distance = getCameraDistance(sceneManager)
if (distance == null) return
const nextDistance = baseDistance / scale
@ -84,9 +86,15 @@ function GCodePreviewUI(props) {
const [previewScale, setPreviewScale] = useState(1)
const [panMode, setPanMode] = useState(false)
const [isDragging, setIsDragging] = useState(false)
const [renderMode, setRenderMode] = useState('lines')
const [travelVisibility, setTravelVisibility] = useState('hide')
const renderModeRef = useRef(renderMode)
const travelVisibilityRef = useRef(travelVisibility)
renderModeRef.current = renderMode
travelVisibilityRef.current = travelVisibility
const resizePreview = useCallback(() => {
previewRef.current?.resize()
previewRef.current?.sceneManager?.resize()
}, [])
useEffect(() => {
@ -99,28 +107,27 @@ function GCodePreviewUI(props) {
lastSegmentColor
})
previewRef.current?.dispose?.()
const instance = GCodePreview.init({
previewRef.current?.dispose()
const instance = new GCodePreview({
canvas: canvasRef.current,
startLayer,
endLayer,
lineWidth,
backgroundColor: toThreeColorHex(themeColors.backgroundColor, '#ffffff'),
extrusionColor: toThreeColorHex(themeColors.extrusionColor, '#0091FF'),
travelColor: toThreeColorHex(themeColors.travelColor, '#8c8c8c'),
topLayerColor: toThreeColorHex(themeColors.topLayerColor, '#FF9230'),
lastSegmentColor: toThreeColorHex(
themeColors.lastSegmentColor,
'#FF69B4'
),
backgroundColor: toCssColor(themeColors.backgroundColor, '#ffffff'),
extrusionColor: toCssColor(themeColors.extrusionColor, '#0091ff'),
travelColor: toCssColor(themeColors.travelColor, '#8c8c8c'),
topLayerColor: toCssColor(themeColors.topLayerColor, '#ff9230'),
lastSegmentColor: toCssColor(themeColors.lastSegmentColor, '#ff69b4'),
buildVolume: { x: 250, y: 220, z: 150 },
initialCameraPosition: [0, 400, 450],
allowDragNDrop: false
droppable: false,
renderTubes: renderModeRef.current === 'pipes',
renderTravel: travelVisibilityRef.current === 'show'
})
previewRef.current = instance
const { camera, controls } = instance
baseDistanceRef.current = getCameraDistance(instance) || 1
const { camera, controls } = instance.sceneManager
baseDistanceRef.current = getCameraDistance(instance.sceneManager) || 1
// The camera dollies instead of scaling pixels, so the frustum has to
// cover the full zoom range.
camera.far = Math.max(
@ -133,7 +140,10 @@ function GCodePreviewUI(props) {
const handleControlsChange = () => {
if (applyingScaleRef.current) return
const scale = getCameraScale(instance, baseDistanceRef.current)
const scale = getCameraScale(
instance.sceneManager,
baseDistanceRef.current
)
if (scale == null) return
setPreviewScale((prev) => (Math.abs(prev - scale) < 0.001 ? prev : scale))
}
@ -144,7 +154,7 @@ function GCodePreviewUI(props) {
controls.addEventListener('start', handleControlsStart)
controls.addEventListener('end', handleControlsEnd)
instance.resize()
instance.sceneManager.resize()
setPreview(instance)
const pane = previewPaneRef.current
@ -163,7 +173,8 @@ function GCodePreviewUI(props) {
controls.removeEventListener('end', handleControlsEnd)
setPreview(null)
setIsDragging(false)
instance.dispose?.()
instance.clear()
instance.dispose()
if (previewRef.current === instance) {
previewRef.current = null
}
@ -184,18 +195,39 @@ function GCodePreviewUI(props) {
useEffect(() => {
if (!preview) return
applyingScaleRef.current = true
applyCameraScale(preview, baseDistanceRef.current, previewScale)
applyCameraScale(
preview.sceneManager,
baseDistanceRef.current,
previewScale
)
applyingScaleRef.current = false
}, [preview, previewScale])
useEffect(() => {
if (!preview) return
preview.controls.mouseButtons = {
const { sceneManager } = preview
const renderTubes = renderMode === 'pipes'
const renderTravel = travelVisibility === 'show'
if (
sceneManager.renderTubes === renderTubes &&
sceneManager.renderTravel === renderTravel
) {
return
}
sceneManager.renderTubes = renderTubes
sceneManager.renderTravel = renderTravel
sceneManager.render()
}, [preview, renderMode, travelVisibility])
useEffect(() => {
if (!preview) return
const { controls } = preview.sceneManager
controls.mouseButtons = {
LEFT: panMode ? THREE.MOUSE.PAN : THREE.MOUSE.ROTATE,
MIDDLE: THREE.MOUSE.DOLLY,
RIGHT: panMode ? THREE.MOUSE.ROTATE : THREE.MOUSE.PAN
}
preview.controls.touches = {
controls.touches = {
ONE: panMode ? THREE.TOUCH.PAN : THREE.TOUCH.ROTATE,
TWO: THREE.TOUCH.DOLLY_PAN
}
@ -209,30 +241,49 @@ function GCodePreviewUI(props) {
})
const resetView = useCallback(() => {
preview?.controls.reset()
preview?.sceneManager.controls.reset()
setPreviewScale(1)
}, [preview])
useEffect(() => {
let cancelled = false
const abortController = new AbortController()
const loadFromSrc = async () => {
const instance = previewRef.current
if (!src || !instance) return
try {
const response = await fetch(src)
const text = await response.text()
if (cancelled || previewRef.current !== instance) return
instance.processGCode(text)
const response = await fetch(src, { signal: abortController.signal })
if (!response.ok || !response.body) {
throw new Error(`Failed to load G-code (${response.status})`)
}
if (abortController.signal.aborted || previewRef.current !== instance) {
return
}
if (instance.countLayers > 0) {
instance.clear()
instance.sceneManager.startLayer = startLayer
instance.sceneManager.endLayer = endLayer
}
const stream = response.body.pipeThrough(new TextDecoderStream())
await instance.processGCodeStream(stream)
if (abortController.signal.aborted || previewRef.current !== instance) {
return
}
if (typeof startLayer === 'number') {
instance.sceneManager.startLayer = startLayer
}
if (typeof endLayer === 'number') {
instance.sceneManager.endLayer = endLayer
}
} catch (e) {
if (cancelled) return
if (abortController.signal.aborted) return
console.error('Failed to load G-code from src', e)
}
}
loadFromSrc()
return () => {
cancelled = true
abortController.abort()
}
}, [
colorPink,
@ -253,7 +304,7 @@ function GCodePreviewUI(props) {
gap={'middle'}
style={{ width: '100%', height: '100%', minHeight: 0, ...style }}
>
<Flex gap={'small'}>
<Flex gap={'small'} wrap='wrap' align='center'>
<KeyboardShortcut
shortcut={'alt+p'}
hint={'ALT P'}
@ -293,6 +344,26 @@ function GCodePreviewUI(props) {
setPreviewScale((prev) => clampPreviewScale(prev - 0.05))
}}
/>
<Select
value={renderMode}
options={[
{ value: 'lines', label: 'Lines' },
{ value: 'pipes', label: 'Pipes' }
]}
style={{ minWidth: 77 }}
popupMatchSelectWidth={false}
onChange={setRenderMode}
/>
<Select
value={travelVisibility}
options={[
{ value: 'show', label: 'Show Travel' },
{ value: 'hide', label: 'Hide Travel' }
]}
style={{ minWidth: 118 }}
popupMatchSelectWidth={false}
onChange={setTravelVisibility}
/>
</Flex>
<div

View File

@ -22,7 +22,8 @@ const EMPTY_OBJECT = {}
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)
if (id1 == null || id2 == null) return id1 == id2
return String(id1).toLowerCase() === String(id2).toLowerCase()
}
const toSelectValue = (id) => {
@ -249,6 +250,15 @@ const ObjectSelect = ({
const filterRef = useRef(filter)
const masterFilterRef = useRef(masterFilter)
const clearedMissingValueRef = useRef(false)
const loadGenerationRef = useRef(0)
const getSelectKey = useCallback(
(selectType, selectMasterFilter) =>
`${selectType}::${JSON.stringify(selectMasterFilter ?? {})}`,
[]
)
const [committedSelectKey, setCommittedSelectKey] = useState(() =>
getSelectKey(type, masterFilter)
)
filterRef.current = filter
masterFilterRef.current = masterFilter
@ -366,12 +376,14 @@ const ObjectSelect = ({
// Fetch the object properties tree from the API
const handleFetchObjectsProperties = useCallback(
async (customFilter = filter, { replace = false } = {}) => {
const generation = loadGenerationRef.current
try {
const data = await fetchObjectsByProperty(type, {
properties: properties,
filter: customFilter,
masterFilter
})
if (generation !== loadGenerationRef.current) return null
if (Array.isArray(data)) {
setObjectPropertiesTree((prev) =>
@ -551,6 +563,7 @@ const ObjectSelect = ({
(data) => {
const { treeNodes, objects } = buildTreeData(data)
setObjectList(objects)
objectListRef.current = objects
setTreeData(treeNodes)
treeDataRef.current = treeNodes
@ -674,52 +687,64 @@ const ObjectSelect = ({
updateEventHandlerRef.current = updateEventHandler
newEventHandlerRef.current = newEventHandler
const findObjectById = useCallback((id) => {
if (id == null || id === '') return null
return (
objectListRef.current.find((obj) => areValuesEqual(obj._id, id)) || null
)
}, [])
const onTreeSelectChange = useCallback(
(value) => {
(nextValue) => {
const isEmptySelection = multiple
? !Array.isArray(value) || value.length === 0
: value == null || value === ''
if (
isEmptySelection &&
treeDataRef.current.length === 0 &&
valueRef.current != null
) {
? !Array.isArray(nextValue) || nextValue.length === 0
: nextValue == null || nextValue === ''
const pendingId = getValueId(valueRef.current)
const pendingInTree =
pendingId != null &&
(isValueInTree(treeDataRef.current, pendingId) ||
findObjectById(pendingId) != null)
// TreeSelect emits empty/null when treeData is replaced or a controlled
// value is not in the tree yet. Don't wipe an external value we still own.
if (isEmptySelection && pendingId != null && !pendingInTree) {
return
}
setValueNotFound(false)
clearedMissingValueRef.current = false
// Mark this as an internal change
if (!multiple) setIsSearching(false)
isInternalChangeRef.current = true
// value can be a string (single) or array (multiple)
if (multiple) {
// Multiple selection
let selectedObjects = []
if (Array.isArray(value)) {
selectedObjects = value
.map((id) => objectList.find((obj) => areValuesEqual(obj._id, id)))
.filter(Boolean)
if (Array.isArray(nextValue)) {
selectedObjects = nextValue.map(findObjectById).filter(Boolean)
}
setTreeSelectValue(value)
setTreeSelectValue(nextValue)
onChange?.(selectedObjects)
} else {
// Single selection: replace the previous object instead of emitting
// undefined (lodash merge skips undefined and would keep the old value).
if (value == null || value === '') {
setTreeSelectValue(null)
onChange?.(null)
return
}
const selectedObject = objectList.find((obj) =>
areValuesEqual(obj._id, value)
)
setTreeSelectValue(value)
onChange?.(selectedObject ?? null)
return
}
if (nextValue == null || nextValue === '') {
setTreeSelectValue(null)
onChange?.(null)
return
}
const selectedObject = findObjectById(nextValue)
setTreeSelectValue(nextValue)
if (selectedObject) {
onChange?.(selectedObject)
return
}
if (pendingId != null && areValuesEqual(pendingId, nextValue)) {
return
}
onChange?.(null)
},
[multiple, objectList, onChange]
[multiple, onChange, findObjectById]
)
const onSearch = useCallback(
@ -831,8 +856,74 @@ const ObjectSelect = ({
treeDataRef.current = treeData
}, [treeData])
const prevValuesRef = useRef({ type, masterFilter })
useEffect(() => {
const prevValues = prevValuesRef.current
// Deep comparison for objects, simple comparison for primitives
const hasChanged =
prevValues.type !== type ||
JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)
if (hasChanged) {
loadGenerationRef.current += 1
searchRequestIdRef.current += 1
setIsSearching(false)
setSearchValue('')
setObjectPropertiesTree({})
setObjectList([])
setTreeData([])
treeDataRef.current = []
setTreeVersion((v) => v + 1)
setExpandedKeys([])
setInitialized(false)
valueRef.current = null
setTreeSelectValue(null)
setInitialLoading(true)
setReloading(false)
setError(false)
setValueNotFound(false)
clearedMissingValueRef.current = false
prevValuesRef.current = { type, masterFilter }
setCommittedSelectKey(getSelectKey(type, masterFilter))
}
}, [type, masterFilter, getSelectKey])
useEffect(() => {
const currentValueIdentity = getValueIdentity(value)
const hasValueChanged =
prevValueIdentityRef.current !== currentValueIdentity
if (hasValueChanged) {
const changeSource = isInternalChangeRef.current ? 'internal' : 'external'
if (changeSource == 'external') {
loadGenerationRef.current += 1
setObjectPropertiesTree({})
setTreeData([])
treeDataRef.current = []
setInitialized(false)
setInitialLoading(true)
valueRef.current = null
clearedMissingValueRef.current = false
setValueNotFound(false)
}
isInternalChangeRef.current = false
prevValueRef.current = value
prevValueIdentityRef.current = currentValueIdentity
}
}, [value, getValueIdentity])
useEffect(() => {
if (getSelectKey(type, masterFilter) !== committedSelectKey) {
return
}
const generation = loadGenerationRef.current
const handleValue = async () => {
if (generation !== loadGenerationRef.current) return
if (
multiple &&
Array.isArray(value) &&
@ -841,12 +932,15 @@ const ObjectSelect = ({
) {
valueRef.current = value
const fullValues = await Promise.all(value.map(fetchFullObjectIfNeeded))
if (generation !== loadGenerationRef.current) return
const pathKeys = []
if (fullValues.length === 0) {
handleFetchObjectsProperties()
const data = await handleFetchObjectsProperties()
if (generation !== loadGenerationRef.current) return
if (Array.isArray(data)) applyTreeFromData(data)
} else {
fullValues.forEach((fullValue) => {
for (const fullValue of fullValues) {
const valueFilter = { ...filter }
const parentKeys = []
@ -874,8 +968,10 @@ const ObjectSelect = ({
parentKeys.push(valueString)
})
handleFetchObjectsProperties(valueFilter)
})
const data = await handleFetchObjectsProperties(valueFilter)
if (generation !== loadGenerationRef.current) return
if (Array.isArray(data)) applyTreeFromData(data)
}
}
setExpandedKeys([...new Set(pathKeys)])
@ -895,9 +991,8 @@ const ObjectSelect = ({
type != 'unknown'
) {
valueRef.current = value
// Check if value is a minimal object and fetch full object if needed
const fullValue = await fetchFullObjectIfNeeded(value)
// Build a new filter from value's properties that are in the properties list
if (generation !== loadGenerationRef.current) return
const valueFilter = { ...filter }
const pathKeys = []
const parentKeys = []
@ -922,7 +1017,6 @@ const ObjectSelect = ({
valueFilter[prop] = filterValue
valueString = filterValue
}
// Build the path key for this property level
const nodeKey = parentKeys
.concat(prop + ':' + valueString)
.join('-')
@ -930,10 +1024,10 @@ const ObjectSelect = ({
parentKeys.push(valueString)
}
})
// Expand the path to the object
setExpandedKeys(pathKeys)
// Fetch with the new filter
handleFetchObjectsProperties(valueFilter)
const data = await handleFetchObjectsProperties(valueFilter)
if (generation !== loadGenerationRef.current) return
if (Array.isArray(data)) applyTreeFromData(data)
setTreeSelectValue(toSelectValue(valueRef.current._id))
setInitialized(true)
return
@ -959,13 +1053,7 @@ const ObjectSelect = ({
setInitialized(true)
}
}
const timeoutId = setTimeout(() => {
handleValue()
}, 10)
return () => {
clearTimeout(timeoutId)
}
handleValue()
}, [
value,
filter,
@ -975,73 +1063,15 @@ const ObjectSelect = ({
token,
fetchFullObjectIfNeeded,
type,
masterFilter,
committedSelectKey,
getSelectKey,
connected,
getValueIdentity,
multiple
multiple,
applyTreeFromData
])
const prevValuesRef = useRef({ type, masterFilter })
useEffect(() => {
const prevValues = prevValuesRef.current
// Deep comparison for objects, simple comparison for primitives
const hasChanged =
prevValues.type !== type ||
JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)
if (hasChanged) {
searchRequestIdRef.current += 1
setIsSearching(false)
setSearchValue('')
setObjectPropertiesTree({})
setObjectList([])
setTreeData([])
setTreeVersion((v) => v + 1)
setExpandedKeys([])
setInitialized(false)
valueRef.current = null
onTreeSelectChange(null)
setTreeSelectValue(null)
setInitialLoading(true)
setReloading(false)
setError(false)
setValueNotFound(false)
clearedMissingValueRef.current = false
prevValuesRef.current = { type, masterFilter }
}
}, [type, masterFilter, onTreeSelectChange])
useEffect(() => {
// Check if value has actually changed
const currentValueIdentity = getValueIdentity(value)
const hasValueChanged =
prevValueIdentityRef.current !== currentValueIdentity
if (hasValueChanged) {
const changeSource = isInternalChangeRef.current ? 'internal' : 'external'
if (changeSource == 'external') {
setObjectPropertiesTree({})
setTreeData([])
treeDataRef.current = []
setInitialized(false)
setInitialLoading(true)
valueRef.current = null
clearedMissingValueRef.current = false
setValueNotFound(false)
prevValuesRef.current = { type, masterFilter }
}
// Reset the internal change flag
isInternalChangeRef.current = false
// Update the previous value reference
prevValueRef.current = value
prevValueIdentityRef.current = currentValueIdentity
}
}, [value, getValueIdentity, type, masterFilter])
useEffect(() => {
objectListRef.current = objectList
}, [objectList])
@ -1137,16 +1167,6 @@ const ObjectSelect = ({
if (missing) {
setValueNotFound(true)
if (
value != null &&
!(Array.isArray(value) && value.length === 0) &&
!clearedMissingValueRef.current
) {
clearedMissingValueRef.current = true
isInternalChangeRef.current = true
setTreeSelectValue(multiple ? [] : null)
onChange?.(multiple ? [] : null)
}
return
}
@ -1154,7 +1174,7 @@ const ObjectSelect = ({
if (value != null) {
setValueNotFound(false)
}
}, [value, multiple, objectList, treeData, valueReady, onChange])
}, [value, multiple, objectList, treeData, valueReady])
const displayPlaceholder = valueNotFound
? `${modelLabel.charAt(0).toUpperCase()}${modelLabel.slice(1).toLowerCase()} not found.`