diff --git a/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx b/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx
index 3e5ecd6d9..528cfbfa8 100644
--- a/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx
+++ b/packages/@dcl/inspector/src/components/EntityInspector/EntityInspector.tsx
@@ -63,7 +63,7 @@ const MultiEntityInspector = withSdk<{ entities: Entity[] }>(({ sdk, entities })
{inspectors.map(
({ name, component: Inspector }, index) =>
- !hiddenComponents[name] &&
+ !hiddenComponents[name] &&
)}
)
@@ -165,14 +165,14 @@ const SingleEntityInspector = withSdk<{ entity: Entity | null }>(({ sdk, entity
{inspectors.map(
({ name, component: Inspector }, index) =>
- !hiddenComponents[name] &&
+ !hiddenComponents[name] &&
)}
{isBasicViewEnabled ? (
) : (
advancedInspectorComponents.map(
({ name, component: Inspector }, index) =>
- !hiddenComponents[name] &&
+ !hiddenComponents[name] &&
)
)}
>
diff --git a/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx b/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx
index 2ca684c10..ca02c2fe4 100644
--- a/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx
+++ b/packages/@dcl/inspector/src/components/EntityInspector/TransformInspector/TransformInspector.tsx
@@ -1,6 +1,7 @@
+import { Entity } from '@dcl/ecs'
import { useEffect } from 'react'
-import { isValidNumericInput, useComponentInput, useComponentInput2 } from '../../../hooks/sdk/useComponentInput'
+import { isValidNumericInput, useComponentInput, useMultiComponentInput } from '../../../hooks/sdk/useComponentInput'
import { useHasComponent } from '../../../hooks/sdk/useHasComponent'
import { withSdk } from '../../../hoc/withSdk'
@@ -20,7 +21,7 @@ export default withSdk(({ sdk, entities }) => {
const hasTransform = useHasComponent(entity, Transform)
const transform = Transform.getOrNull(entity) ?? undefined
const config = TransformConfig.getOrNull(entity) ?? undefined
- const { getInputProps } = useComponentInput2(
+ const { getInputProps } = useMultiComponentInput(
entities,
Transform,
fromTransform,
diff --git a/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts b/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts
index e709d993b..52fc7918a 100644
--- a/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts
+++ b/packages/@dcl/inspector/src/hooks/sdk/useComponentInput.ts
@@ -1,8 +1,11 @@
-import { InputHTMLAttributes, useCallback, useEffect, useRef, useState } from 'react'
-import { Entity } from '@dcl/ecs'
+import { InputHTMLAttributes, useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { CrdtMessageType, Entity } from '@dcl/ecs'
+import { recursiveCheck as hasDiff } from 'jest-matcher-deep-close-to/lib/recursiveCheck'
import { getValue, NestedKey, setValue } from '../../lib/logic/get-set-value'
import { Component } from '../../lib/sdk/components'
-import { useComponentValue } from './useComponentValue'
+import { getComponentValue, isLastWriteWinComponent, useComponentValue } from './useComponentValue'
+import { useSdk } from './useSdk'
+import { useChange } from './useChange'
type Input = {
[key: string]: boolean | string | string[] | any[] | Record
@@ -15,6 +18,9 @@ export function isValidNumericInput(input: Input[keyof Input]): boolean {
if (typeof input === 'boolean') {
return !!input
}
+ if (typeof input === 'number') {
+ return !isNaN(input)
+ }
return input.length > 0 && !isNaN(Number(input))
}
@@ -114,6 +120,28 @@ export const useComponentInput = {
+ // Base case - if any value is not an object, compare directly
+ if (!values.every(val => val && typeof val === 'object')) {
+ return values.every(val => val === values[0]) ? values[0] : '--' // symbol?
+ }
+
+ // Get all keys from all objects
+ const allKeys = [...new Set(values.flatMap(Object.keys))]
+
+ // Create result object
+ const result: any = {}
+
+ // For each key, recursively merge values
+ for (const key of allKeys) {
+ const valuesForKey = values.map(obj => obj[key])
+ result[key] = mergeValues(valuesForKey)
+ }
+
+ return result
+}
+
const mergeComponentValues = (
values: ComponentValueType[],
fromComponentValueToInput: (componentValue: ComponentValueType) => InputType
@@ -129,124 +157,128 @@ const mergeComponentValues = {
- const value = input[key]
- if (typeof value === 'object' && value !== null) {
- // For objects, compare stringified versions
- return JSON.stringify(value) === JSON.stringify(firstValue)
- }
- return value === firstValue
- })
-
- // Set result value based on whether all inputs match
- result[key] = allSame ? firstValue : ('--' as any)
+ const valuesForKey = inputs.map(input => input[key])
+ result[key] = mergeValues(valuesForKey)
}
return result
}
-export const useComponentInput2 = (
+const getEntityAndComponentValue = (entities: Entity[], component: Component): [Entity, ComponentValueType][] => {
+ return entities.map((entity) => [entity, getComponentValue(entity, component) as ComponentValueType])
+}
+
+export const useMultiComponentInput = (
entities: Entity[],
component: Component,
fromComponentValueToInput: (componentValue: ComponentValueType) => InputType,
fromInputToComponentValue: (input: InputType) => ComponentValueType,
validateInput: (input: InputType) => boolean = () => true,
- deps: unknown[] = []
) => {
- const componentValues = entities.map((entity) => useComponentValue(entity, component))
- const [componentValue, _, isEqual] = componentValues[0]
- const asd = mergeComponentValues(
- componentValues.map(([value]) => value),
- fromComponentValueToInput
- )
- const [input, setInput] = useState(
- componentValue === null ? null : fromComponentValueToInput(componentValue)
+ // If there's only one entity, use the single entity version just to be safe for now
+ if (entities.length === 1) {
+ return useComponentInput(
+ entities[0],
+ component,
+ fromComponentValueToInput,
+ fromInputToComponentValue,
+ validateInput
+ )
+ }
+ const sdk = useSdk()
+
+ // Get initial merged value from all entities
+ const initialEntityValues = getEntityAndComponentValue(entities, component)
+ const initialMergedValue = useMemo(() =>
+ mergeComponentValues(
+ initialEntityValues.map(([_, component]) => component),
+ fromComponentValueToInput
+ ),
+ [] // only compute on mount
)
- const [focusedOn, setFocusedOn] = useState(null)
- const skipSyncRef = useRef(false)
- const [isValid, setIsValid] = useState(true)
- const updateInputs = useCallback((value: InputType | null, skipSync = false) => {
- skipSyncRef.current = skipSync
- setInput(value)
- }, [])
+ const [value, setMergeValue] = useState(initialMergedValue)
+ const [isValid, setIsValid] = useState(true)
+ const [isFocused, setIsFocused] = useState(false)
- const handleUpdate =
+ // Handle input updates
+ const handleUpdate = useCallback(
(path: NestedKey, getter: (event: React.ChangeEvent) => any = (e) => e.target.value) =>
(event: React.ChangeEvent) => {
- if (input === null) return
- const newInputs = setValue(input, path, getter(event))
- updateInputs(newInputs)
- }
+ if (!value) return
- const handleFocus = useCallback(
- (path: NestedKey) => () => {
- setFocusedOn(path)
- },
- []
- )
+ const newValue = setValue(value, path, getter(event))
+ if (!hasDiff(value, newValue, 2)) return
- const handleBlur = useCallback(() => {
- if (componentValue === null) return
- setFocusedOn(null)
- updateInputs(fromComponentValueToInput(componentValue))
- }, [componentValue])
+ // Only update if component is last-write-win and SDK exists
+ if (!isLastWriteWinComponent(component) || !sdk) {
+ setMergeValue(newValue)
+ return
+ }
- const validate = useCallback(
- (input: InputType | null): input is InputType => input !== null && validateInput(input),
- [input, ...deps]
+ // Validate and update all entities
+ const entityUpdates = getEntityAndComponentValue(entities, component).map(([entity, componentValue]) => {
+ const updatedInput = setValue(fromComponentValueToInput(componentValue as any), path, getter(event))
+ const newComponentValue = fromInputToComponentValue(updatedInput)
+ return {
+ entity,
+ value: newComponentValue,
+ isValid: validateInput(updatedInput)
+ }
+ })
+
+ const allUpdatesValid = entityUpdates.every(({ isValid }) => isValid)
+
+ if (allUpdatesValid) {
+ entityUpdates.forEach(({ entity, value }) => {
+ sdk.operations.updateValue(component, entity, value)
+ })
+ void sdk.operations.dispatch()
+ }
+
+ setMergeValue(newValue)
+ setIsValid(allUpdatesValid)
+ },
+ [value, sdk, component, entities, fromInputToComponentValue, fromComponentValueToInput, validateInput]
)
- // sync inputs -> engine
- useEffect(() => {
- if (skipSyncRef.current) return
- if (validate(input)) {
- const newComponentValue = { ...componentValue, ...fromInputToComponentValue(input) }
- if (isEqual(newComponentValue)) return
+ // Sync with engine changes
+ useChange(
+ (event) => {
+ const isRelevantUpdate =
+ entities.includes(event.entity) &&
+ component.componentId === event.component?.componentId &&
+ event.value &&
+ event.operation === CrdtMessageType.PUT_COMPONENT
- for (const [_, setComponentValue] of componentValues) {
- setComponentValue(newComponentValue)
- }
- }
- }, [input])
+ if (!isRelevantUpdate) return
- // sync engine -> inputs
- useEffect(() => {
- if (componentValue === null) return
+ const updatedEntityValues = getEntityAndComponentValue(entities, component)
+ const newMergedValue = mergeComponentValues(
+ updatedEntityValues.map(([_, component]) => component),
+ fromComponentValueToInput
+ )
- let newInputs = fromComponentValueToInput(componentValue) as any
- if (focusedOn) {
- // skip sync from state while editing, to avoid overriding the user input
- const current = getValue(input, focusedOn)
- newInputs = setValue(newInputs, focusedOn, current)
- }
- // set "skipSync" to avoid cyclic component value change
- updateInputs(newInputs, true)
- }, [componentValue, ...deps])
+ if (!hasDiff(value, newMergedValue, 2) || isFocused) return
- useEffect(() => {
- setIsValid(validate(input))
- }, [input, ...deps])
+ setMergeValue(newMergedValue)
+ },
+ [entities, component, fromComponentValueToInput, value, isFocused]
+ )
- const getProps = useCallback(
+ // Input props getter
+ const getInputProps = useCallback(
(
path: NestedKey,
getter?: (event: React.ChangeEvent) => any
- ): Pick, 'value' | 'onChange' | 'onFocus' | 'onBlur'> => {
- const value = (getValue(asd, path) || '').toString()
-
- return {
- value,
- onChange: handleUpdate(path, getter),
- onFocus: handleFocus(path),
- onBlur: handleBlur
- }
- },
- [handleUpdate, handleFocus, handleBlur, input]
+ ): Pick, 'value' | 'onChange' | 'onFocus' | 'onBlur'> => ({
+ value: (getValue(value, path) || '').toString(),
+ onChange: handleUpdate(path, getter),
+ onFocus: () => setIsFocused(true),
+ onBlur: () => setIsFocused(false)
+ }),
+ [value, handleUpdate]
)
- return { getInputProps: getProps, isValid }
+ return { getInputProps, isValid }
}