From e0b7dc985efe1a92e2b829bc4a71d73031c96729 Mon Sep 17 00:00:00 2001 From: Xinyi Xu Date: Mon, 27 Mar 2023 17:45:02 -0700 Subject: [PATCH] feat(composer): visibility toggles for overlays in settings panel --- packages/scene-composer/package.json | 2 +- .../scene-composer/public/overlay.scene.json | 546 ++++++++++++++++++ .../scene-composer/src/common/constants.ts | 5 + .../components/SceneComposerInternal.spec.tsx | 60 +- .../src/components/panels/SettingsPanel.tsx | 34 +- .../panels/__tests__/SettingsPanel.spec.tsx | 15 + .../__snapshots__/SettingsPanel.spec.tsx.snap | 57 ++ ...tsx => ComponentVisibilityToggle.spec.tsx} | 14 +- .../ComponentVisibilityToggle.tsx | 64 ++ .../MotionIndicatorVisibilityToggle.tsx | 34 -- .../OverlayPanelVisibilityToggle.spec.tsx | 118 ++++ .../OverlayPanelVisibilityToggle.tsx | 62 ++ ...> ComponentVisibilityToggle.spec.tsx.snap} | 4 +- .../DataOverlayContainer.tsx | 25 +- .../DataOverlayContainerSnap.spec.tsx | 35 +- .../__tests__/DataOverlayRowsSnap.spec.tsx | 2 +- .../DataOverlayContainerSnap.spec.tsx.snap | 26 + .../MotionIndicatorComponent.tsx | 5 +- .../src/hooks/useOverlayVisible.spec.tsx | 65 +++ .../src/hooks/useOverlayVisible.ts | 27 + .../src/hooks/useTagSettings.ts | 4 +- .../src/interfaces/componentSettings.ts | 6 +- packages/scene-composer/src/store/Store.ts | 6 +- .../src/store/StoreOperations.ts | 4 +- .../store/slices/SceneDocumentSlice.spec.ts | 171 +++--- .../src/store/slices/SceneDocumentSlice.ts | 22 +- .../store/slices/ViewOptionStateSlice.spec.ts | 68 ++- .../src/store/slices/ViewOptionStateSlice.ts | 21 +- .../src/utils/componentSettingsUtils.spec.ts | 20 +- .../src/utils/componentSettingsUtils.ts | 4 +- .../Developer/SceneComposer.stories.mdx | 4 +- .../stories/components/scene-composer.tsx | 2 +- .../IotAppKitSceneComposer.en_US.json | 8 + 33 files changed, 1343 insertions(+), 197 deletions(-) create mode 100644 packages/scene-composer/public/overlay.scene.json rename packages/scene-composer/src/components/panels/scene-settings/{MotionIndicatorVisibilityToggle.spec.tsx => ComponentVisibilityToggle.spec.tsx} (55%) create mode 100644 packages/scene-composer/src/components/panels/scene-settings/ComponentVisibilityToggle.tsx delete mode 100644 packages/scene-composer/src/components/panels/scene-settings/MotionIndicatorVisibilityToggle.tsx create mode 100644 packages/scene-composer/src/components/panels/scene-settings/OverlayPanelVisibilityToggle.spec.tsx create mode 100644 packages/scene-composer/src/components/panels/scene-settings/OverlayPanelVisibilityToggle.tsx rename packages/scene-composer/src/components/panels/scene-settings/__snapshots__/{MotionIndicatorVisibilityToggle.spec.tsx.snap => ComponentVisibilityToggle.spec.tsx.snap} (67%) create mode 100644 packages/scene-composer/src/hooks/useOverlayVisible.spec.tsx create mode 100644 packages/scene-composer/src/hooks/useOverlayVisible.ts rename packages/scene-composer/{tests => src}/store/slices/SceneDocumentSlice.spec.ts (83%) diff --git a/packages/scene-composer/package.json b/packages/scene-composer/package.json index 7ee84e76f..92a820579 100644 --- a/packages/scene-composer/package.json +++ b/packages/scene-composer/package.json @@ -46,7 +46,7 @@ "convert-svg": "npx @svgr/cli --out-dir src/assets/auto-gen/icons/ --typescript --index-template tools/index-template.js -- src/assets/icons/", "release": "run-s compile copy-assets", "copy-assets": "copyfiles -e \"**/*.tsx\" -e \"**/*.ts\" -e \"**/*.snap\" -e \"**/*.js\" -e \"**/*.jsx\" -e \"**/*.json\" \"src/**/*\" dist/", - "lint": "eslint . --max-warnings=825", + "lint": "eslint . --max-warnings=726", "fix": "eslint --fix .", "test": "jest --config jest.config.ts --coverage --silent", "test:dev": "jest --config jest.config.ts --coverage", diff --git a/packages/scene-composer/public/overlay.scene.json b/packages/scene-composer/public/overlay.scene.json new file mode 100644 index 000000000..78fd6e356 --- /dev/null +++ b/packages/scene-composer/public/overlay.scene.json @@ -0,0 +1,546 @@ +{ + "specVersion": "1.0", + "version": "1", + "unit": "meters", + "properties": { + "environmentPreset": "neutral", + "componentSettings": { + "DataOverlay": { + "overlayPanelVisible": true + }, + "Tag": { + "autoRescale": true, + "scale": 2 + } + }, + "dataBindingConfig": { + "template": { + "sel_entity": "room1", + "sel_comp": "temperatureSensor2" + }, + "fieldMapping": { + "entityId": [ + "sel_entity" + ], + "componentName": [ + "sel_comp" + ] + } + } + }, + "nodes": [ + { + "name": "Pallet Jack", + "transform": { + "position": [ + 1, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + "transformConstraint": { + + }, + "children": [ + 1, + 4, + 5, + 7, + 9 + ], + "components": [ + { + "type": "ModelRef", + "uri": "PALLET_JACK.glb", + "modelType": "GLB", + "unitOfMeasure": "meters" + } + ], + "properties": { + + } + }, + { + "name": "Obstruction Alarm", + "transform": { + "position": [ + 0, + 2.2462426122230426, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + "transformConstraint": { + + }, + "children": [ + 8 + ], + "components": [ + { + "type": "Tag", + "ruleBasedMapId": "sampleTimeSeriesIconRule", + "valueDataBinding": { + "dataBindingContext": { + "entityId": "${sel_entity}", + "componentName": "${sel_comp}", + "propertyName": "temperature" + } + }, + "navLink": { + "destination": "http://localhost:4300/d/KKIARDInk/new-dashboard-copy", + "params": { + "foo": "bar" + } + } + } + ], + "properties": { + + } + }, + { + "name": "directional light", + "transform": { + "position": [ + -5, + 10, + 10 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + "transformConstraint": { + + }, + "components": [ + { + "type": "Light", + "lightType": "Directional", + "lightSettings": { + "color": 16777215, + "intensity": 1, + "castShadow":true + } + } + ], + "properties": { + + } + }, + { + "name": "ambient light", + "transform": { + "position": [ + 10, + 10, + 10 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + "transformConstraint": { + + }, + "components": [ + { + "type": "Light", + "lightType": "Ambient", + "lightSettings": { + "color": 16777215, + "intensity": 0.2 + } + } + ], + "properties": { + + } + }, + { + "name": "Lift Direction", + "transform": { + "position": [ + 0.026177569273909795, + 0.12487268539819854, + -0.6475704349193628 + ], + "rotation": [ + 3.0891109067350975, + 1.5423817718730837, + -3.089601298590836 + ], + "scale": [ + 1.3444898056140844, + 1, + 1.008107454341504 + ] + }, + "transformConstraint": { + + }, + "components": [ + { + "type": "MotionIndicator", + "shape": "LinearPlane", + "valueDataBindings": { + "foregroundColor": { + + } + }, + "config": { + "numOfRepeatInY": 2, + "backgroundColorOpacity": 1, + "defaultSpeed": "0.5", + "defaultForegroundColor": "#29f502" + } + } + ], + "properties": { + + } + }, + { + "name": "Fork Lift", + "transform": { + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + "transformConstraint": { + + }, + "components": [ + { + "parentRef": "1BB44716-4325-436C-A098-CD2A53A8336C", + "selector": "Scene491", + "type": "SubModelRef" + } + ], + "properties": { + "subModelId": "Scene491" + } + }, + { + "name": "Camera1", + "transform": { + "position": [ + 3.8592914668132337, + 1.660816595658845, + -0.29774633590225036 + ], + "rotation": [ + -1.6451249704899187, + 1.4253822264470961, + 1.6459148400213455 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + "transformConstraint": { + + }, + "components": [ + { + "type": "Camera", + "cameraIndex": 0 + } + ], + "properties": { + + } + }, + { + "name": "Text Annotation", + "transform": { + "position": [ + 0, + 0, + 0.25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + "transformConstraint": { + + }, + "components": [ + { + "type": "DataOverlay", + "subType": "TextAnnotation", + "valueDataBindings": [], + "dataRows": [ + { + "rowType": "Markdown", + "content": "# || Annotation || \n Second line" + } + ] + } + ], + "properties": { + + } + }, + { + "name": "Overlay Panel", + "transform": { + "position": [ + 0, + 0.25, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + "transformConstraint": { + + }, + "components": [ + { + "type": "DataOverlay", + "subType": "OverlayPanel", + "config": { + "isPinned": true + }, + "valueDataBindings": [ + { + "bindingName": "binding a", + "valueDataBinding" : { + "dataBindingContext": { + "entityId": "room1", + "componentName": "temperatureSensor1", + "propertyName": "temperature" + } + } + }, + { + "bindingName": "binding b", + "valueDataBinding" : { + "dataBindingContext": { + "entityId": "room2", + "componentName": "temperatureSensor1", + "propertyName": "temperature" + } + } + } + ], + "dataRows": [ + { + "rowType": "Markdown", + "content": "## || Panel || \n ## [Click me](https://github.com/awslabs/iot-app-kit) \n ${binding a} ${binding b}xxxx" + } + ] + } + ], + "properties": { + + } + }, + { + "name": "Overlay Panel 2", + "transform": { + "position": [ + 0, + 1, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + "transformConstraint": { + + }, + "components": [ + { + "type": "DataOverlay", + "subType": "OverlayPanel", + "config": { + "isPinned": false + }, + "valueDataBindings": [ + { + "bindingName": "binding a", + "valueDataBinding" : { + "dataBindingContext": { + "entityId": "room1", + "componentName": "temperatureSensor1", + "propertyName": "temperature" + } + } + }, + { + "bindingName": "binding b", + "valueDataBinding" : { + "dataBindingContext": { + "entityId": "room2", + "componentName": "temperatureSensor1", + "propertyName": "temperature" + } + } + } + ], + "dataRows": [ + { + "rowType": "Markdown", + "content": "## || Panel || \n ## [Click me](https://github.com/awslabs/iot-app-kit) \n ${binding a} ${binding b}xxxx" + } + ] + } + ], + "properties": { + + } + } + ], + "rootNodeIndexes": [ + 0, + 2, + 3, + 6 + ], + "cameras": [ + { + "cameraType": "Perspective", + "fov": 53.13, + "far": 1000, + "near": 0.1, + "zoom": 1 + } + ], + "rules": { + "sampleAlarmIconRule": { + "statements": [ + { + "expression": "alarm_status == 'ACTIVE'", + "target": "iottwinmaker.common.icon:Error" + }, + { + "expression": "alarm_status == 'ACKNOWLEDGED'", + "target": "iottwinmaker.common.icon:Warning" + }, + { + "expression": "alarm_status == 'SNOOZE_DISABLED'", + "target": "iottwinmaker.common.icon:Warning" + }, + { + "expression": "alarm_status == 'NORMAL'", + "target": "iottwinmaker.common.icon:Info" + } + ] + }, + "sampleTimeSeriesIconRule": { + "statements": [ + { + "expression": "temperature >= 20", + "target": "iottwinmaker.common.icon:Error" + }, + { + "expression": "temperature >= 30", + "target": "iottwinmaker.common.icon:Warning" + }, + { + "expression": "temperature < 30", + "target": "iottwinmaker.common.icon:Info" + } + ] + }, + "sampleTimeSeriesColorRule": { + "statements": [ + { + "expression": "temperature >= 37", + "target": "iottwinmaker.common.color:#ff0000" + }, + { + "expression": "temperature >= 20", + "target": "iottwinmaker.common.color:#ffff00" + }, + { + "expression": "temperature < 20", + "target": "iottwinmaker.common.color:#00ff00" + } + ] + }, + "AlwaysOn": { + "statements": [ + { + "expression": "1==1", + "target": "iottwinmaker.common.color:#d13212" + } + ] + } + }, + "defaultCameraIndex": 0 +} diff --git a/packages/scene-composer/src/common/constants.ts b/packages/scene-composer/src/common/constants.ts index 4265ee972..e6e08d4bf 100644 --- a/packages/scene-composer/src/common/constants.ts +++ b/packages/scene-composer/src/common/constants.ts @@ -8,6 +8,7 @@ import { DistanceUnit, Vector3, ITagSettings, + IOverlaySettings, } from '../interfaces'; import { CameraControlImpl } from '../store/internalInterfaces'; @@ -126,6 +127,10 @@ export const DEFAULT_TAG_GLOBAL_SETTINGS: ITagSettings = { scale: 1, }; +export const DEFAULT_OVERLAY_GLOBAL_SETTINGS: IOverlaySettings = { + overlayPanelVisible: false, +}; + /****************************************************************************** * Camera Constants ******************************************************************************/ diff --git a/packages/scene-composer/src/components/SceneComposerInternal.spec.tsx b/packages/scene-composer/src/components/SceneComposerInternal.spec.tsx index 493217b3f..f5ae3a509 100644 --- a/packages/scene-composer/src/components/SceneComposerInternal.spec.tsx +++ b/packages/scene-composer/src/components/SceneComposerInternal.spec.tsx @@ -38,13 +38,13 @@ describe('SceneComposerInternal', () => { it('should render correctly with an empty scene in editing mode', async () => { let container; - act(() => { + await act(async () => { container = renderer.create( , ); - }); - await flushPromises(); + await flushPromises(); + }); // shows the scene hierarchy browser expect(container).toMatchSnapshot(); @@ -52,13 +52,13 @@ describe('SceneComposerInternal', () => { it('should render correctly with an empty scene in viewing mode', async () => { let container; - act(() => { + await act(async () => { container = renderer.create( , ); - }); - await flushPromises(); + await flushPromises(); + }); // shows the scene hierarchy browser expect(container).toMatchSnapshot(); @@ -66,13 +66,13 @@ describe('SceneComposerInternal', () => { it('should render correctly with a valid scene in editing mode', async () => { let container; - act(() => { + await act(async () => { container = renderer.create( , ); - }); - await flushPromises(); + await flushPromises(); + }); // shows the scene hierarchy browser expect(container).toMatchSnapshot(); @@ -80,64 +80,64 @@ describe('SceneComposerInternal', () => { it('should render warning when minor version is newer', async () => { let container; - act(() => { + await act(async () => { container = renderer.create( , ); - }); - await flushPromises(); + await flushPromises(); + }); expect(container).toMatchSnapshot(); }); it('should render error when major version is newer', async () => { let container; - act(() => { + await act(async () => { container = renderer.create( , ); - }); - await flushPromises(); + await flushPromises(); + }); expect(container).toMatchSnapshot(); }); it('should render error when specVersion is invalid', async () => { let container; - act(() => { + await act(async () => { container = renderer.create( , ); - }); - await flushPromises(); + await flushPromises(); + }); expect(container).toMatchSnapshot(); }); it('should support rendering multiple valid scenes', async () => { let container; - act(() => { + await act(async () => { container = renderer.create(
, ); - }); - await flushPromises(); + await flushPromises(); + }); // verify that 2 different scenes are rendered expect(container).toMatchSnapshot(); @@ -145,7 +145,7 @@ describe('SceneComposerInternal', () => { it('should render both valid and invalid scene correctly', async () => { let container; - act(() => { + await act(async () => { container = renderer.create(
{
, ); - }); - await flushPromises(); + await flushPromises(); + }); expect(container).toMatchSnapshot(); }); it('should render a default error view when loading a bad scene content', async () => { let container; - act(() => { + await act(async () => { container = renderer.create( , ); - }); - await flushPromises(); + await flushPromises(); + }); expect(container).toMatchSnapshot(); }); @@ -195,7 +195,7 @@ describe('SceneComposerInternal', () => { }); describe('useSceneComposerApi', () => { - it('should return an api object', () => { + it('should return an api object', async () => { let sut: SceneComposerApi | null = null; const TestComponent = () => { @@ -211,8 +211,10 @@ describe('SceneComposerInternal', () => { ); }; - act(() => { + await act(async () => { renderer.create(); + + await flushPromises(); }); expect(sut).toHaveProperty('setCameraTarget'); diff --git a/packages/scene-composer/src/components/panels/SettingsPanel.tsx b/packages/scene-composer/src/components/panels/SettingsPanel.tsx index 4a9450c15..8f5a291d1 100644 --- a/packages/scene-composer/src/components/panels/SettingsPanel.tsx +++ b/packages/scene-composer/src/components/panels/SettingsPanel.tsx @@ -6,13 +6,15 @@ import useLifecycleLogging from '../../logger/react-logger/hooks/useLifecycleLog import { presets } from '../three-fiber/Environment'; import { sceneComposerIdContext } from '../../common/sceneComposerIdContext'; import { useStore } from '../../store'; -import { COMPOSER_FEATURES, IValueDataBindingProvider, KnownSceneProperty } from '../../interfaces'; +import { COMPOSER_FEATURES, IValueDataBindingProvider, KnownComponentType, KnownSceneProperty } from '../../interfaces'; import { pascalCase } from '../../utils/stringUtils'; import { getGlobalSettings } from '../../common/GlobalSettings'; +import { Component } from '../../models/SceneModels'; import { ExpandableInfoSection } from './CommonPanelComponents'; import { MatterportIntegration, SceneDataBindingTemplateEditor, SceneTagSettingsEditor } from './scene-settings'; -import { MotionIndicatorVisibilityToggle } from './scene-settings/MotionIndicatorVisibilityToggle'; +import { ComponentVisibilityToggle } from './scene-settings/ComponentVisibilityToggle'; +import { OverlayPanelVisibilityToggle } from './scene-settings/OverlayPanelVisibilityToggle'; export interface SettingsPanelProps { valueDataBindingProvider?: IValueDataBindingProvider; @@ -29,6 +31,7 @@ export const SettingsPanel: React.FC = ({ valueDataBindingPr const tagResizeEnabled = getGlobalSettings().featureConfig[COMPOSER_FEATURES.TagResize]; const matterportEnabled = getGlobalSettings().featureConfig[COMPOSER_FEATURES.Matterport]; + const overlayEnabled = getGlobalSettings().featureConfig[COMPOSER_FEATURES.Overlay]; const selectedEnvPreset = useStore(sceneComposerId)((state) => state.getSceneProperty(KnownSceneProperty.EnvironmentPreset), @@ -55,6 +58,17 @@ export const SettingsPanel: React.FC = ({ valueDataBindingPr }, }); + const visibilityToggleLabels = defineMessages({ + [KnownComponentType.MotionIndicator]: { + defaultMessage: 'Motion indicator', + description: 'Sub section label', + }, + [Component.DataOverlaySubType.TextAnnotation]: { + defaultMessage: 'Annotation', + description: 'Sub section label', + }, + }); + const presetOptions = [ { label: intl.formatMessage(i18nPresetsStrings['No Preset']), value: NO_PRESET_VALUE }, ...Object.keys(presets).map((preset) => ({ @@ -78,7 +92,16 @@ export const SettingsPanel: React.FC = ({ valueDataBindingPr })} defaultExpanded > - + + {overlayEnabled && ( + + )} {isEditing && ( @@ -113,12 +136,13 @@ export const SettingsPanel: React.FC = ({ valueDataBindingPr )} - {tagResizeEnabled && ( + {(tagResizeEnabled || overlayEnabled) && ( - + {tagResizeEnabled && } + {overlayEnabled && } )} diff --git a/packages/scene-composer/src/components/panels/__tests__/SettingsPanel.spec.tsx b/packages/scene-composer/src/components/panels/__tests__/SettingsPanel.spec.tsx index fc1a2ac8c..ff6f5753b 100644 --- a/packages/scene-composer/src/components/panels/__tests__/SettingsPanel.spec.tsx +++ b/packages/scene-composer/src/components/panels/__tests__/SettingsPanel.spec.tsx @@ -70,4 +70,19 @@ describe('SettingsPanel contains expected elements.', () => { expect(queryByTitle('Tag Settings')).toBeTruthy(); expect(container).toMatchSnapshot(); }); + + it('should contains settings element for overlay.', async () => { + setFeatureConfig({ [COMPOSER_FEATURES.Overlay]: true }); + const setSceneProperty = jest.fn(); + useStore('default').setState({ + setSceneProperty: setSceneProperty, + getSceneProperty: jest.fn().mockReturnValue('neutral'), + }); + + const { container, queryByTitle } = render(); + + expect(queryByTitle('Current View Settings')).toBeTruthy(); + expect(queryByTitle('Tag Settings')).toBeTruthy(); + expect(container).toMatchSnapshot(); + }); }); diff --git a/packages/scene-composer/src/components/panels/__tests__/__snapshots__/SettingsPanel.spec.tsx.snap b/packages/scene-composer/src/components/panels/__tests__/__snapshots__/SettingsPanel.spec.tsx.snap index bf16441c9..9454e312d 100644 --- a/packages/scene-composer/src/components/panels/__tests__/__snapshots__/SettingsPanel.spec.tsx.snap +++ b/packages/scene-composer/src/components/panels/__tests__/__snapshots__/SettingsPanel.spec.tsx.snap @@ -77,6 +77,63 @@ exports[`SettingsPanel contains expected elements. should contains default eleme `; +exports[`SettingsPanel contains expected elements. should contains settings element for overlay. 1`] = ` +
+
+
+ Motion indicator +
+
+ Visibility +
+
+ Annotation +
+
+ Visibility +
+
+
+
+ Overlay +
+
+ Visibility +
+
+
+`; + exports[`SettingsPanel contains expected elements. should contains tag settings element. 1`] = `
{ +describe('ComponentVisibilityToggle', () => { const getComponentRefByType = jest.fn(); const createState = (visible: boolean) => ({ noHistoryStates: { ...useStore('default').getState().noHistoryStates, - motionIndicatorVisible: visible, - toggleMotionIndicatorVisibility: jest.fn(), + componentVisibilities: { [KnownComponentType.MotionIndicator]: visible }, + toggleComponentVisibility: jest.fn(), }, getComponentRefByType, }); - it('should render correctly', async () => { + it('should render correctly for motion indicator', async () => { getComponentRefByType.mockReturnValue({ type: KnownComponentType.MotionIndicator }); useStore('default').setState(createState(true)); - const { container } = render(); + const { container } = render( + , + ); expect(container).toMatchSnapshot(); }); diff --git a/packages/scene-composer/src/components/panels/scene-settings/ComponentVisibilityToggle.tsx b/packages/scene-composer/src/components/panels/scene-settings/ComponentVisibilityToggle.tsx new file mode 100644 index 000000000..8d64d60c2 --- /dev/null +++ b/packages/scene-composer/src/components/panels/scene-settings/ComponentVisibilityToggle.tsx @@ -0,0 +1,64 @@ +import React, { useContext, useMemo } from 'react'; +import { useIntl } from 'react-intl'; +import { Box, Toggle } from '@awsui/components-react'; + +import { IDataOverlayComponentInternal, useStore, useViewOptionState } from '../../../store'; +import { sceneComposerIdContext } from '../../../common/sceneComposerIdContext'; +import { KnownComponentType } from '../../../interfaces'; +import { Component } from '../../../models/SceneModels'; +import { findComponentByType } from '../../../utils/nodeUtils'; + +export interface ComponentVisibilityToggleProps { + componentType: KnownComponentType | Component.DataOverlaySubType; + label: string; + onChange?: (newValue: boolean) => void; +} + +export const ComponentVisibilityToggle: React.FC = ({ + componentType, + label, + onChange, +}) => { + const sceneComposerId = useContext(sceneComposerIdContext); + const toggleComponentVisibility = useViewOptionState(sceneComposerId).toggleComponentVisibility; + const componentVisible = useViewOptionState(sceneComposerId).componentVisibilities[componentType]; + const getComponentRefByType = useStore(sceneComposerId)((state) => state.getComponentRefByType); + const getSceneNodeByRef = useStore(sceneComposerId)((state) => state.getSceneNodeByRef); + const componentNodeMap = useStore(sceneComposerId)((state) => state.document.componentNodeMap); + const { formatMessage } = useIntl(); + + const hasComponent = useMemo(() => { + if (Component.DataOverlaySubType[componentType]) { + const overlays = Object.keys(getComponentRefByType(KnownComponentType.DataOverlay)); + return overlays.find( + (overlayRef) => + ( + findComponentByType( + getSceneNodeByRef(overlayRef), + KnownComponentType.DataOverlay, + ) as IDataOverlayComponentInternal + ).subType === componentType, + ); + } else { + return Object.keys(getComponentRefByType(componentType as KnownComponentType)).length > 0; + } + }, [componentNodeMap, getComponentRefByType]); + + return ( + + + {label} + + { + toggleComponentVisibility(componentType); + onChange?.(!componentVisible); + }} + > + {formatMessage({ description: 'Toggle label', defaultMessage: 'Visibility' })} + + + ); +}; diff --git a/packages/scene-composer/src/components/panels/scene-settings/MotionIndicatorVisibilityToggle.tsx b/packages/scene-composer/src/components/panels/scene-settings/MotionIndicatorVisibilityToggle.tsx deleted file mode 100644 index c415638aa..000000000 --- a/packages/scene-composer/src/components/panels/scene-settings/MotionIndicatorVisibilityToggle.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useContext, useMemo } from 'react'; -import { useIntl } from 'react-intl'; -import { Box, Toggle } from '@awsui/components-react'; - -import { useStore, useViewOptionState } from '../../../store'; -import { sceneComposerIdContext } from '../../../common/sceneComposerIdContext'; -import { KnownComponentType } from '../../../interfaces'; - -export const MotionIndicatorVisibilityToggle: React.FC = () => { - const sceneComposerId = useContext(sceneComposerIdContext); - const { motionIndicatorVisible, toggleMotionIndicatorVisibility } = useViewOptionState(sceneComposerId); - const getComponentRefByType = useStore(sceneComposerId)((state) => state.getComponentRefByType); - const componentNodeMap = useStore(sceneComposerId)((state) => state.document.componentNodeMap); - const { formatMessage } = useIntl(); - - const hasMotionIndicator = useMemo(() => { - return Object.keys(getComponentRefByType(KnownComponentType.MotionIndicator)).length > 0; - }, [componentNodeMap, getComponentRefByType]); - - return ( - - - {formatMessage({ description: 'Sub section label', defaultMessage: 'Motion indicator' })} - - - {formatMessage({ description: 'Toggle label', defaultMessage: 'Visibility' })} - - - ); -}; diff --git a/packages/scene-composer/src/components/panels/scene-settings/OverlayPanelVisibilityToggle.spec.tsx b/packages/scene-composer/src/components/panels/scene-settings/OverlayPanelVisibilityToggle.spec.tsx new file mode 100644 index 000000000..05c9b566f --- /dev/null +++ b/packages/scene-composer/src/components/panels/scene-settings/OverlayPanelVisibilityToggle.spec.tsx @@ -0,0 +1,118 @@ +import { act, render } from '@testing-library/react'; +import React from 'react'; +import wrapper from '@awsui/components-react/test-utils/dom'; + +import { KnownComponentType, KnownSceneProperty } from '../../../interfaces'; +import { Component } from '../../../models/SceneModels'; +import { useStore } from '../../../store'; + +import { OverlayPanelVisibilityToggle } from './OverlayPanelVisibilityToggle'; + +jest.mock('@awsui/components-react', () => ({ + ...jest.requireActual('@awsui/components-react'), +})); + +describe('OverlayPanelVisibilityToggle', () => { + const setSceneProperty = jest.fn(); + const getSceneProperty = jest.fn(); + const getComponentRefByType = jest.fn(); + const getSceneNodeByRef = jest.fn(); + const isViewing = jest.fn(); + + const createState = (visible: boolean) => ({ + noHistoryStates: { + ...useStore('default').getState().noHistoryStates, + componentVisibilities: { [Component.DataOverlaySubType.OverlayPanel]: visible }, + }, + setSceneProperty, + getSceneProperty, + getComponentRefByType, + getSceneNodeByRef, + isViewing, + }); + + beforeEach(() => { + jest.clearAllMocks(); + getSceneNodeByRef.mockReturnValue({ + components: [{ type: KnownComponentType.DataOverlay, subType: Component.DataOverlaySubType.OverlayPanel }], + }); + getComponentRefByType.mockReturnValue({ ref: ['comp'] }); + useStore('default').setState(createState(true)); + }); + + it('should update view option with document settings', () => { + render(); + + expect( + useStore('default').getState().noHistoryStates.componentVisibilities[Component.DataOverlaySubType.OverlayPanel], + ).toBeFalsy(); + }); + + it('should update scene property when toggled', () => { + const { container } = render(); + const polarisWrapper = wrapper(container); + const toggle = polarisWrapper.findToggle(); + + expect(toggle).not.toBeNull(); + expect( + useStore('default').getState().noHistoryStates.componentVisibilities[Component.DataOverlaySubType.OverlayPanel], + ).toBeFalsy(); + + // Toggle to true + act(() => { + toggle!.findNativeInput().click(); + }); + + expect(setSceneProperty).toBeCalledTimes(1); + expect(setSceneProperty).toBeCalledWith(KnownSceneProperty.ComponentSettings, { + [KnownComponentType.DataOverlay]: { overlayPanelVisible: true }, + }); + expect( + useStore('default').getState().noHistoryStates.componentVisibilities[Component.DataOverlaySubType.OverlayPanel], + ).toBeTruthy(); + + // Toggle to false + act(() => { + toggle!.findNativeInput().click(); + }); + + expect(setSceneProperty).toBeCalledTimes(2); + expect(setSceneProperty).toBeCalledWith(KnownSceneProperty.ComponentSettings, { + [KnownComponentType.DataOverlay]: { overlayPanelVisible: false }, + }); + expect( + useStore('default').getState().noHistoryStates.componentVisibilities[Component.DataOverlaySubType.OverlayPanel], + ).toBeFalsy(); + }); + + it('should not update view option with document settings in viewing mode', () => { + isViewing.mockReturnValue(true); + render(); + + expect( + useStore('default').getState().noHistoryStates.componentVisibilities[Component.DataOverlaySubType.OverlayPanel], + ).toBeTruthy(); + }); + + it('should not update scene property when toggled in viewing mode', () => { + isViewing.mockReturnValue(true); + const { container } = render(); + const polarisWrapper = wrapper(container); + const toggle = polarisWrapper.findToggle(); + + expect(toggle).not.toBeNull(); + expect( + useStore('default').getState().noHistoryStates.componentVisibilities[Component.DataOverlaySubType.OverlayPanel], + ).toBeTruthy(); + + // Toggle to false + act(() => { + toggle!.findNativeInput().click(); + }); + + expect(setSceneProperty).not.toBeCalled(); + expect( + useStore('default').getState().noHistoryStates.componentVisibilities[Component.DataOverlaySubType.OverlayPanel], + ).toBeFalsy(); + }); +}); diff --git a/packages/scene-composer/src/components/panels/scene-settings/OverlayPanelVisibilityToggle.tsx b/packages/scene-composer/src/components/panels/scene-settings/OverlayPanelVisibilityToggle.tsx new file mode 100644 index 000000000..acbe99083 --- /dev/null +++ b/packages/scene-composer/src/components/panels/scene-settings/OverlayPanelVisibilityToggle.tsx @@ -0,0 +1,62 @@ +import React, { useCallback, useContext, useEffect } from 'react'; +import { useIntl } from 'react-intl'; + +import { useStore, useViewOptionState } from '../../../store'; +import { sceneComposerIdContext } from '../../../common/sceneComposerIdContext'; +import { IComponentSettingsMap, IOverlaySettings, KnownComponentType, KnownSceneProperty } from '../../../interfaces'; +import { Component } from '../../../models/SceneModels'; +import { componentSettingsSelector } from '../../../utils/componentSettingsUtils'; + +import { ComponentVisibilityToggle } from './ComponentVisibilityToggle'; + +export const OverlayPanelVisibilityToggle: React.FC = () => { + const sceneComposerId = useContext(sceneComposerIdContext); + const { formatMessage } = useIntl(); + const overlayPanelVisible = + useViewOptionState(sceneComposerId).componentVisibilities[Component.DataOverlaySubType.OverlayPanel]; + const toggleComponentVisibility = useViewOptionState(sceneComposerId).toggleComponentVisibility; + + const documentSettings: IOverlaySettings = useStore(sceneComposerId)( + (state) => componentSettingsSelector(state, KnownComponentType.DataOverlay) as IOverlaySettings, + ); + + const setSceneProperty = useStore(sceneComposerId)((state) => state.setSceneProperty); + const getSceneProperty = useStore(sceneComposerId)((state) => state.getSceneProperty); + const isViewing = useStore(sceneComposerId)((state) => state.isViewing()); + + // When the document settings is changed from other places (e.g. undo/redo), + // sync the view options visibility value with document settings. + useEffect(() => { + if (!isViewing && !!overlayPanelVisible !== !!documentSettings.overlayPanelVisible) { + toggleComponentVisibility(Component.DataOverlaySubType.OverlayPanel); + } + }, [documentSettings, isViewing]); + + const updateSettings = useCallback( + (newValue: boolean) => { + if (isViewing) return; + + const newSettings: IOverlaySettings = { + overlayPanelVisible: newValue, + }; + const newComponentSettings: IComponentSettingsMap = { + ...getSceneProperty(KnownSceneProperty.ComponentSettings), + [KnownComponentType.DataOverlay]: newSettings, + }; + + setSceneProperty(KnownSceneProperty.ComponentSettings, newComponentSettings); + }, + [getSceneProperty, setSceneProperty, isViewing], + ); + + return ( + + ); +}; diff --git a/packages/scene-composer/src/components/panels/scene-settings/__snapshots__/MotionIndicatorVisibilityToggle.spec.tsx.snap b/packages/scene-composer/src/components/panels/scene-settings/__snapshots__/ComponentVisibilityToggle.spec.tsx.snap similarity index 67% rename from packages/scene-composer/src/components/panels/scene-settings/__snapshots__/MotionIndicatorVisibilityToggle.spec.tsx.snap rename to packages/scene-composer/src/components/panels/scene-settings/__snapshots__/ComponentVisibilityToggle.spec.tsx.snap index 0f1ebd70b..80a74de6b 100644 --- a/packages/scene-composer/src/components/panels/scene-settings/__snapshots__/MotionIndicatorVisibilityToggle.spec.tsx.snap +++ b/packages/scene-composer/src/components/panels/scene-settings/__snapshots__/ComponentVisibilityToggle.spec.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MotionIndicatorVisibilityToggle should render correctly 1`] = ` +exports[`ComponentVisibilityToggle should render correctly for motion indicator 1`] = `
- Motion indicator + Motion indicator label
{ +export const DataOverlayContainer = ({ component, node }: DataOverlayContainerProps): ReactElement | null => { const sceneComposerId = useContext(sceneComposerIdContext); const containerRef = useRef(null); const selectedSceneNodeRef = useStore(sceneComposerId)((state) => state.selectedSceneNodeRef); const setSelectedSceneNodeRef = useStore(sceneComposerId)((state) => state.setSelectedSceneNodeRef); - const [visible, setVisible] = useState( - component.config?.isPinned || component.subType === Component.DataOverlaySubType.TextAnnotation, - ); + const subType = component.subType; + + const componentVisible = useOverlayVisible(subType); + const initialVisibilitySkipped = useRef(false); + + const [visible, setVisible] = useState(component.config?.isPinned || componentVisible); - // Toggle panel visibility + // Toggle panel visibility on selection change useEffect(() => { if (selectedSceneNodeRef === node.ref) { setVisible(true); } }, [selectedSceneNodeRef, node.ref]); + // Toggle visibility on view option change. Skip the first call to make sure the + // isPinned config can keep panel open initially. + useEffect(() => { + if (initialVisibilitySkipped.current) { + setVisible(componentVisible); + } + initialVisibilitySkipped.current = true; + }, [componentVisible]); + // Same behavior as other components to select node when clicked on the panel const [onPointerDown, onPointerUp] = useCallbackWhenNotPanning( (e) => { diff --git a/packages/scene-composer/src/components/three-fiber/DataOverlayComponent/__tests__/DataOverlayContainerSnap.spec.tsx b/packages/scene-composer/src/components/three-fiber/DataOverlayComponent/__tests__/DataOverlayContainerSnap.spec.tsx index bc47e6fcc..d6d265d99 100644 --- a/packages/scene-composer/src/components/three-fiber/DataOverlayComponent/__tests__/DataOverlayContainerSnap.spec.tsx +++ b/packages/scene-composer/src/components/three-fiber/DataOverlayComponent/__tests__/DataOverlayContainerSnap.spec.tsx @@ -6,6 +6,8 @@ import { KnownComponentType } from '../../../../interfaces'; import { Component } from '../../../../models/SceneModels'; import { DataOverlayContainer } from '../DataOverlayContainer'; import { DataOverlayRowsProps } from '../DataOverlayRows'; +import useOverlayVisible from '../../../../hooks/useOverlayVisible'; +import { DEFAULT_OVERLAY_GLOBAL_SETTINGS } from '../../../../common/constants'; jest.mock('../../../../hooks/useCallbackWhenNotPanning', () => (cb) => [ jest.fn(), @@ -14,8 +16,10 @@ jest.mock('../../../../hooks/useCallbackWhenNotPanning', () => (cb) => [ }, ]); +jest.mock('../../../../hooks/useOverlayVisible', () => jest.fn()); + jest.mock('../DataOverlayRows', () => ({ - DataOverlayRows: (...props: [DataOverlayRowsProps, {}]) =>
{JSON.stringify(props)}
, + DataOverlayRows: (...props: [DataOverlayRowsProps, object]) =>
{JSON.stringify(props)}
, })); describe('DataOverlayContainer', () => { @@ -44,6 +48,11 @@ describe('DataOverlayContainer', () => { dataInput: undefined, }; + beforeEach(() => { + jest.clearAllMocks(); + (useOverlayVisible as jest.Mock).mockReturnValue(false); + }); + it('should render with panel visible correctly when the overlay node is selected', () => { useStore('default').setState({ ...baseState, selectedSceneNodeRef: mockNode.ref }); @@ -67,6 +76,20 @@ describe('DataOverlayContainer', () => { it('should render with annotation visible correctly', () => { useStore('default').setState(baseState); + (useOverlayVisible as jest.Mock).mockReturnValue(true); + + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); + + it('should render with annotation invisible correctly', () => { + useStore('default').setState(baseState); + (useOverlayVisible as jest.Mock).mockReturnValue(false); const { container } = render( { ); expect(container).toMatchSnapshot(); }); + + it('should render with panel visible correctly', () => { + useStore('default').setState(baseState); + (useOverlayVisible as jest.Mock).mockReturnValue(true); + + const { container } = render( + , + ); + expect(container).toMatchSnapshot(); + }); }); diff --git a/packages/scene-composer/src/components/three-fiber/DataOverlayComponent/__tests__/DataOverlayRowsSnap.spec.tsx b/packages/scene-composer/src/components/three-fiber/DataOverlayComponent/__tests__/DataOverlayRowsSnap.spec.tsx index 5d0be2b85..c97fb58e9 100644 --- a/packages/scene-composer/src/components/three-fiber/DataOverlayComponent/__tests__/DataOverlayRowsSnap.spec.tsx +++ b/packages/scene-composer/src/components/three-fiber/DataOverlayComponent/__tests__/DataOverlayRowsSnap.spec.tsx @@ -8,7 +8,7 @@ import { DataOverlayRows } from '../DataOverlayRows'; import { DataOverlayDataRowProps } from '../DataOverlayDataRow'; jest.mock('../DataOverlayDataRow', () => ({ - DataOverlayDataRow: (...props: [DataOverlayDataRowProps, {}]) => ( + DataOverlayDataRow: (...props: [DataOverlayDataRowProps, object]) => (
{JSON.stringify(props)}
), })); diff --git a/packages/scene-composer/src/components/three-fiber/DataOverlayComponent/__tests__/__snapshots__/DataOverlayContainerSnap.spec.tsx.snap b/packages/scene-composer/src/components/three-fiber/DataOverlayComponent/__tests__/__snapshots__/DataOverlayContainerSnap.spec.tsx.snap index 2d20380f9..3a712d39b 100644 --- a/packages/scene-composer/src/components/three-fiber/DataOverlayComponent/__tests__/__snapshots__/DataOverlayContainerSnap.spec.tsx.snap +++ b/packages/scene-composer/src/components/three-fiber/DataOverlayComponent/__tests__/__snapshots__/DataOverlayContainerSnap.spec.tsx.snap @@ -1,5 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`DataOverlayContainer should render with annotation invisible correctly 1`] = `
`; + exports[`DataOverlayContainer should render with annotation visible correctly 1`] = `
`; +exports[`DataOverlayContainer should render with panel visible correctly 1`] = ` +
+
+
+
+
+
+ [{"component":{"ref":"comp-ref","type":"DataOverlay","subType":"OverlayPanel","dataRows":[{"rowType":"Markdown","content":"content"}],"valueDataBindings":[]}},{}] +
+
+
+`; + exports[`DataOverlayContainer should render with panel visible correctly when the overlay node is pinned 1`] = `
= ( const MotionIndicatorComponent = ({ component, node }: IMotionIndicatorComponentProps) => { const sceneComposerId = useSceneComposerId(); - const { motionIndicatorVisible } = useViewOptionState(sceneComposerId); + const motionIndicatorVisible = + useViewOptionState(sceneComposerId).componentVisibilities[KnownComponentType.MotionIndicator]; if (motionIndicatorVisible) { return ; diff --git a/packages/scene-composer/src/hooks/useOverlayVisible.spec.tsx b/packages/scene-composer/src/hooks/useOverlayVisible.spec.tsx new file mode 100644 index 000000000..8d466479b --- /dev/null +++ b/packages/scene-composer/src/hooks/useOverlayVisible.spec.tsx @@ -0,0 +1,65 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { IOverlaySettings } from '../interfaces'; +import { Component } from '../models/SceneModels'; +import { useStore } from '../store'; +import { componentSettingsSelector } from '../utils/componentSettingsUtils'; + +import useOverlayVisible from './useOverlayVisible'; + +jest.mock('../utils/componentSettingsUtils'); + +describe('useOverlayVisible', () => { + const isViewingMock = jest.fn(); + const documentSettings: IOverlaySettings = { overlayPanelVisible: true }; + const componentVisibilities = { + [Component.DataOverlaySubType.OverlayPanel]: false, + [Component.DataOverlaySubType.TextAnnotation]: true, + }; + + beforeEach(() => { + (componentSettingsSelector as jest.Mock).mockReturnValue(documentSettings); + useStore('default').setState({ + noHistoryStates: { + ...useStore('default').getState().noHistoryStates, + componentVisibilities, + }, + isViewing: isViewingMock, + }); + isViewingMock.mockReturnValue(true); + }); + + it('should get overlay panel visibility from document in editing mode', () => { + isViewingMock.mockReturnValue(false); + const visible = renderHook(() => useOverlayVisible(Component.DataOverlaySubType.OverlayPanel)).result.current; + + expect(visible).toBeTruthy(); + }); + + it('should get overlay panel visibility from view options in viewing mode', () => { + const visible = renderHook(() => useOverlayVisible(Component.DataOverlaySubType.OverlayPanel)).result.current; + + expect(visible).toBeFalsy(); + }); + + it('should get annotation visibility as true', () => { + isViewingMock.mockReturnValue(false); + const visible = renderHook(() => useOverlayVisible(Component.DataOverlaySubType.TextAnnotation)).result.current; + + expect(visible).toBeTruthy(); + }); + + it('should get annotation visibility as false', () => { + useStore('default').setState({ + noHistoryStates: { + ...useStore('default').getState().noHistoryStates, + componentVisibilities: { ...componentVisibilities, [Component.DataOverlaySubType.TextAnnotation]: false }, + }, + isViewing: isViewingMock, + }); + + const visible = renderHook(() => useOverlayVisible(Component.DataOverlaySubType.TextAnnotation)).result.current; + + expect(visible).toBeFalsy(); + }); +}); diff --git a/packages/scene-composer/src/hooks/useOverlayVisible.ts b/packages/scene-composer/src/hooks/useOverlayVisible.ts new file mode 100644 index 000000000..97b6b4c8e --- /dev/null +++ b/packages/scene-composer/src/hooks/useOverlayVisible.ts @@ -0,0 +1,27 @@ +import { useMemo } from 'react'; + +import { useSceneComposerId } from '../common/sceneComposerIdContext'; +import { IOverlaySettings, KnownComponentType } from '../interfaces'; +import { Component } from '../models/SceneModels'; +import { useStore, useViewOptionState } from '../store'; +import { componentSettingsSelector } from '../utils/componentSettingsUtils'; + +const useOverlayVisible = (subType: Component.DataOverlaySubType): boolean => { + const sceneComposerId = useSceneComposerId(); + const isViewing = useStore(sceneComposerId)((state) => state.isViewing()); + const documentSettings: IOverlaySettings = useStore(sceneComposerId)( + (state) => componentSettingsSelector(state, KnownComponentType.DataOverlay) as IOverlaySettings, + ); + const componentVisible = useViewOptionState(sceneComposerId).componentVisibilities[subType]; + const visible: boolean = useMemo(() => { + if (subType === Component.DataOverlaySubType.TextAnnotation || isViewing) { + return !!componentVisible; + } + + return !!documentSettings.overlayPanelVisible; + }, [isViewing, documentSettings, componentVisible]); + + return visible; +}; + +export default useOverlayVisible; diff --git a/packages/scene-composer/src/hooks/useTagSettings.ts b/packages/scene-composer/src/hooks/useTagSettings.ts index 0d69aa8fb..1791858f4 100644 --- a/packages/scene-composer/src/hooks/useTagSettings.ts +++ b/packages/scene-composer/src/hooks/useTagSettings.ts @@ -8,8 +8,8 @@ import { componentSettingsSelector } from '../utils/componentSettingsUtils'; const useTagSettings = () => { const sceneComposerId = useSceneComposerId(); const isViewing = useStore(sceneComposerId)((state) => state.isViewing()); - const documentTagSettings: ITagSettings = useStore(sceneComposerId)((state) => - componentSettingsSelector(state, KnownComponentType.Tag), + const documentTagSettings: ITagSettings = useStore(sceneComposerId)( + (state) => componentSettingsSelector(state, KnownComponentType.Tag) as ITagSettings, ); const viewingTagSettings: ITagSettings | undefined = useViewOptionState(sceneComposerId).tagSettings; const tagSettings: ITagSettings = useMemo(() => { diff --git a/packages/scene-composer/src/interfaces/componentSettings.ts b/packages/scene-composer/src/interfaces/componentSettings.ts index 2c41ac5ac..13382c711 100644 --- a/packages/scene-composer/src/interfaces/componentSettings.ts +++ b/packages/scene-composer/src/interfaces/componentSettings.ts @@ -5,5 +5,9 @@ export interface ITagSettings { autoRescale: boolean; } -export type IComponentSettings = ITagSettings | any; +export interface IOverlaySettings { + overlayPanelVisible?: boolean; +} + +export type IComponentSettings = ITagSettings | IOverlaySettings; export type IComponentSettingsMap = Record; diff --git a/packages/scene-composer/src/store/Store.ts b/packages/scene-composer/src/store/Store.ts index 9ba5b6ad1..6947e2327 100644 --- a/packages/scene-composer/src/store/Store.ts +++ b/packages/scene-composer/src/store/Store.ts @@ -58,7 +58,7 @@ export type RootState = ISharedState & */ const stateCreator: StateCreator = (set, get, api) => ({ lastOperation: undefined, - ...createSceneDocumentSlice(set, get, api), + ...createSceneDocumentSlice(set, get), ...createEditStateSlice(set, get, api), ...createDataStoreSlice(set, get, api), noHistoryStates: { @@ -149,8 +149,8 @@ const nodeErrorStateSelector = (state: RootState): INodeErrorStateSlice => ({ }); const viewOptionStateSelector = (state: RootState): IViewOptionStateSlice => ({ - motionIndicatorVisible: state.noHistoryStates.motionIndicatorVisible, - toggleMotionIndicatorVisibility: state.noHistoryStates.toggleMotionIndicatorVisibility, + componentVisibilities: state.noHistoryStates.componentVisibilities, + toggleComponentVisibility: state.noHistoryStates.toggleComponentVisibility, tagSettings: state.noHistoryStates.tagSettings, setTagSettings: state.noHistoryStates.setTagSettings, enableMatterportViewer: state.noHistoryStates.enableMatterportViewer, diff --git a/packages/scene-composer/src/store/StoreOperations.ts b/packages/scene-composer/src/store/StoreOperations.ts index 62543cf70..a86a9d3cf 100644 --- a/packages/scene-composer/src/store/StoreOperations.ts +++ b/packages/scene-composer/src/store/StoreOperations.ts @@ -40,7 +40,7 @@ export type SceneComposerDocumentOperation = export type SceneComposerDataOperation = 'setDataInput' | 'setDataBindingTemplate'; export type SceneComposerViewOptionOperation = - | 'toggleMotionIndicatorVisibility' + | 'toggleComponentVisibility' | 'setTagSettings' | 'setMatterportViewerEnabled'; @@ -91,7 +91,7 @@ export const SceneComposerOperationTypeMap: Record { return { mergeDeep: jest.fn() }; @@ -38,17 +40,17 @@ describe('createSceneDocumentSlice', () => { // Arrange const deserializeResult = { document: hasErrors ? undefined : 'This is a mock document', - errors: hasErrors ? [{ category: 'Error', message: 'test Error' }] : undefined, + errors: hasErrors ? [{ category: 'Error', message: 'test Error' }] : [], }; jest.spyOn(serializationHelpers.document, 'deserialize').mockReturnValue(deserializeResult as any); const draft = { lastOperation: undefined, document: deserializeResult.document }; const getReturn = { resetEditorState: jest.fn(), addMessages: jest.fn() }; - const get = jest.fn(() => getReturn); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const get = jest.fn().mockReturnValue(getReturn); // fake out get call + const set = jest.fn((callback) => callback(draft)); const options = { disableMotionIndicator: true }; // Act - const { loadScene } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { loadScene } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed loadScene('sceneContent', options); // Assert @@ -69,6 +71,43 @@ describe('createSceneDocumentSlice', () => { }); }); + it('should load a scene and initialize view options correctly', () => { + // Arrange + const deserializeResult = { + document: { + ...defaultDocumentSliceState, + properties: { + [KnownSceneProperty.ComponentSettings]: { + [KnownComponentType.DataOverlay]: { + overlayPanelVisible: true, + }, + }, + }, + }, + errors: [], + }; + jest.spyOn(serializationHelpers.document, 'deserialize').mockReturnValue(deserializeResult); + const draft = { + lastOperation: undefined, + document: deserializeResult.document, + noHistoryStates: { componentVisibilities: {} }, + }; + const set = jest.fn((callback) => callback(draft)); + const get = jest.fn(); // fake out get call + const getReturn = { resetEditorState: jest.fn(), addMessages: jest.fn() }; + get.mockReturnValue(getReturn); + + // Act + const { loadScene } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed + loadScene('sceneContent'); + + // Assert + expect(draft.lastOperation!).toEqual('loadScene'); + expect(get).toBeCalledTimes(1); + expect(draft.document).toEqual(deserializeResult.document); + expect(draft.noHistoryStates.componentVisibilities[Component.DataOverlaySubType.OverlayPanel]).toEqual(true); + }); + it(`should getSceneNodeByRef`, () => { // Arrange const document = { nodeMap: { testRef: 'node' } }; @@ -77,7 +116,7 @@ describe('createSceneDocumentSlice', () => { const set = jest.fn(); // Act - const { getSceneNodeByRef } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { getSceneNodeByRef } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed const found = getSceneNodeByRef('testRef'); const notFound = getSceneNodeByRef('notHere'); @@ -93,7 +132,7 @@ describe('createSceneDocumentSlice', () => { const set = jest.fn(); // Act - const { getSceneNodesByRefs } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { getSceneNodesByRefs } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed getSceneNodesByRefs(['testRef', 'notHere']); // Assert @@ -137,10 +176,10 @@ describe('createSceneDocumentSlice', () => { }; const draft = { lastOperation: undefined, selectedSceneNodeRef: undefined, document }; const get = jest.fn().mockReturnValue({ document }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { appendSceneNodeInternal } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { appendSceneNodeInternal } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed appendSceneNodeInternal(rootNode as any); if (draft.document) { @@ -168,10 +207,10 @@ describe('createSceneDocumentSlice', () => { const document = validWithPreExistingNode; const draft = { lastOperation: undefined, document }; const get = jest.fn().mockReturnValue({ document }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { appendSceneNodeInternal } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { appendSceneNodeInternal } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed appendSceneNodeInternal(testNode as any); expect(get).toBeCalled(); @@ -196,10 +235,10 @@ describe('createSceneDocumentSlice', () => { // Arrange const draft = { lastOperation: undefined, document: { nodeMap: { testNode: 'testNode' } } }; const get = jest.fn().mockReturnValue(draft); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { updateSceneNodeInternal } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { updateSceneNodeInternal } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed updateSceneNodeInternal('testNode', { test: 'test' } as any, isTransient); // Assert @@ -214,10 +253,10 @@ describe('createSceneDocumentSlice', () => { // Arrange const draft = { document: cloneDeep(document), lastOperation: undefined }; const get = jest.fn().mockReturnValue({ document }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { updateSceneNodeInternal } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { updateSceneNodeInternal } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed updateSceneNodeInternal('testNode1', { parentRef: 'testNode3' }); // Assert @@ -237,10 +276,10 @@ describe('createSceneDocumentSlice', () => { // Arrange const draft = { document: cloneDeep(document), lastOperation: undefined }; const get = jest.fn().mockReturnValue({ document }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { updateSceneNodeInternal } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { updateSceneNodeInternal } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed updateSceneNodeInternal('testNode1', { parentRef: undefined }); // Assert @@ -260,10 +299,10 @@ describe('createSceneDocumentSlice', () => { // Arrange const draft = { document: cloneDeep(document), lastOperation: undefined }; const get = jest.fn().mockReturnValue({ document }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { updateSceneNodeInternal } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { updateSceneNodeInternal } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed updateSceneNodeInternal('testNode3', { parentRef: 'testNode2' }); // Assert @@ -284,10 +323,10 @@ describe('createSceneDocumentSlice', () => { // Arrange const draft = { lastOperation: undefined, document: { nodeMap: { testNode: 'testNode' } } }; const get = jest.fn(); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { updateDocumentInternal } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { updateDocumentInternal } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed updateDocumentInternal('partial' as any); // Assert @@ -305,7 +344,7 @@ describe('createSceneDocumentSlice', () => { const nodeWithChildren = { childRefs: ['ref]'] }; // Act - const { appendSceneNode } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { appendSceneNode } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed appendSceneNode(node); expect(get).toBeCalled(); @@ -321,7 +360,7 @@ describe('createSceneDocumentSlice', () => { const set = jest.fn(); // Act - const { updateSceneNode } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { updateSceneNode } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed updateSceneNode('ref', 'partial' as any); expect(get).toBeCalled(); @@ -335,7 +374,7 @@ describe('createSceneDocumentSlice', () => { const set = jest.fn(); // Act - const { removeSceneNode } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { removeSceneNode } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed expect(() => removeSceneNode('someNode')).toThrow(); expect(get).toBeCalled(); @@ -370,10 +409,10 @@ describe('createSceneDocumentSlice', () => { }; const draft = { lastOperation: undefined, document }; const get = jest.fn().mockReturnValue({ document }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { removeSceneNode } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { removeSceneNode } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed removeSceneNode('testNode'); expect(get).toBeCalled(); @@ -392,7 +431,7 @@ describe('createSceneDocumentSlice', () => { const set = jest.fn(); // Act - const { listSceneRuleMapIds } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { listSceneRuleMapIds } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed const ruleIds = listSceneRuleMapIds(); expect(get).toBeCalled(); @@ -406,7 +445,7 @@ describe('createSceneDocumentSlice', () => { const set = jest.fn(); // Act - const { getSceneRuleMapById } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { getSceneRuleMapById } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed const rule = getSceneRuleMapById('rule1'); expect(get).toBeCalled(); @@ -420,7 +459,7 @@ describe('createSceneDocumentSlice', () => { const set = jest.fn(); // Act - const { getSceneRuleMapById } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { getSceneRuleMapById } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed const rule = getSceneRuleMapById(undefined); expect(get).not.toBeCalled(); @@ -431,10 +470,10 @@ describe('createSceneDocumentSlice', () => { const document = { ruleMap: { rule1: { statements }, rule2: { statements } } }; const draft = { lastOperation: undefined, document }; const get = jest.fn(); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { updateSceneRuleMapById } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { updateSceneRuleMapById } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed updateSceneRuleMapById('rule1', { statements: { testRule: { expression: 1, target: 'target' } } } as any); expect(draft.lastOperation!).toEqual('updateSceneRuleMapById'); @@ -446,10 +485,10 @@ describe('createSceneDocumentSlice', () => { const document = { ruleMap: { rule1: { statements }, rule2: { statements } } }; const draft = { lastOperation: undefined, document }; const get = jest.fn(); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { removeSceneRuleMapById } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { removeSceneRuleMapById } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed removeSceneRuleMapById('rule1'); expect(draft.lastOperation!).toEqual('removeSceneRuleMapById'); @@ -461,11 +500,11 @@ describe('createSceneDocumentSlice', () => { const draft = { lastOperation: undefined, document }; const getSceneNodeByRef = jest.fn().mockReturnValue(document.nodeMap.testNode); const get = jest.fn().mockReturnValue({ getSceneNodeByRef }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); const comp = { ref: 'comp-1', type: 'abc' }; // Act - const { addComponentInternal } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { addComponentInternal } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed addComponentInternal('testNode', comp); expect(draft.lastOperation!).toEqual('addComponentInternal'); @@ -480,10 +519,10 @@ describe('createSceneDocumentSlice', () => { const draft = { lastOperation: undefined, document }; const getSceneNodeByRef = jest.fn(); const get = jest.fn().mockReturnValue({ getSceneNodeByRef }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { addComponentInternal } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { addComponentInternal } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed addComponentInternal('notHere', 'component' as any); expect(draft.lastOperation!).toBeUndefined(); @@ -500,10 +539,10 @@ describe('createSceneDocumentSlice', () => { const draft = { lastOperation: undefined, document }; const getSceneNodeByRef = jest.fn().mockReturnValue(document.nodeMap.testNode); const get = jest.fn().mockReturnValue({ getSceneNodeByRef }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { updateComponentInternal } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { updateComponentInternal } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed updateComponentInternal('testNode', { ref: 'component1', data: 'updated' } as any, replace); expect(draft.lastOperation!).toEqual('updateComponentInternal'); @@ -530,10 +569,10 @@ describe('createSceneDocumentSlice', () => { return undefined; }); const get = jest.fn().mockReturnValue({ getSceneNodeByRef }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { updateComponentInternal } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { updateComponentInternal } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed expect(() => updateComponentInternal('testNode', { data: 'updated' } as any, false)).toThrow(); expect(() => updateComponentInternal('notHere', { ref: 'component1', data: 'updated' } as any, false)).toThrow(); expect(() => updateComponentInternal('testNode', { ref: 'notHere', data: 'updated' } as any, false)).toThrow(); @@ -548,10 +587,10 @@ describe('createSceneDocumentSlice', () => { const draft = { lastOperation: undefined, document }; const getSceneNodeByRef = jest.fn().mockReturnValue(document.nodeMap.testNode); const get = jest.fn().mockReturnValue({ getSceneNodeByRef }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { removeComponent } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { removeComponent } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed removeComponent('testNode', 'component1'); expect(draft.lastOperation!).toEqual('removeComponent'); @@ -574,10 +613,10 @@ describe('createSceneDocumentSlice', () => { return undefined; }); const get = jest.fn().mockReturnValue({ getSceneNodeByRef }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { removeComponent } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { removeComponent } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed removeComponent('notHere', 'component1'); removeComponent('testNode', 'notHere'); @@ -594,7 +633,7 @@ describe('createSceneDocumentSlice', () => { const set = jest.fn(); // Act - const { getSceneProperty } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { getSceneProperty } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed const result = getSceneProperty(KnownSceneProperty.BaseUrl); const result2 = getSceneProperty(KnownSceneProperty.EnvironmentPreset, 'default'); @@ -609,7 +648,7 @@ describe('createSceneDocumentSlice', () => { const set = jest.fn(); // Act - const { getSceneProperty } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { getSceneProperty } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed const result = getSceneProperty(KnownSceneProperty.BaseUrl, 'default'); expect(get).toBeCalled(); @@ -620,10 +659,10 @@ describe('createSceneDocumentSlice', () => { it(`should be able to setSceneProperty ${document.properties ? 'with' : 'without'} set properties`, () => { const draft = { lastOperation: undefined, document }; const get = jest.fn(); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const { setSceneProperty } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { setSceneProperty } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed setSceneProperty(KnownSceneProperty.BaseUrl, 'setValue'); expect(draft.lastOperation!).toEqual('setSceneProperty'); @@ -642,10 +681,10 @@ describe('createSceneDocumentSlice', () => { const document = { nodeMap: { testNode: { ref: 'testRef', components: [component] } } }; const draft = { lastOperation: undefined, document }; const get = jest.fn().mockReturnValue({ document }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const sceneDocumentSlice = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const sceneDocumentSlice = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed sceneDocumentSlice.clearTemplatizedDataBindings(); expect( @@ -661,10 +700,10 @@ describe('createSceneDocumentSlice', () => { const document = { nodeMap: { testNode: { ref: 'testRef', components: [component] } } }; const draft = { lastOperation: undefined, document }; const get = jest.fn().mockReturnValue({ document }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const sceneDocumentSlice = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const sceneDocumentSlice = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed sceneDocumentSlice.clearTemplatizedDataBindings(); expect( @@ -680,10 +719,10 @@ describe('createSceneDocumentSlice', () => { const document = { nodeMap: { testNode: { ref: 'testRef', components: [component] } } }; const draft = { lastOperation: undefined, document }; const get = jest.fn().mockReturnValue({ document }); // fake out get call - const set = jest.fn(((callback) => callback(draft)) as any); + const set = jest.fn((callback) => callback(draft)); // Act - const sceneDocumentSlice = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const sceneDocumentSlice = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed sceneDocumentSlice.clearTemplatizedDataBindings(); expect((draft.document.nodeMap.testNode.components[0] as IAnchorComponentInternal).valueDataBinding).toBe( @@ -704,7 +743,7 @@ describe('createSceneDocumentSlice', () => { get.mockReturnValueOnce({ document }); // Act - const { getComponentRefByType } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { getComponentRefByType } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed const result1 = getComponentRefByType('abc' as any); // empty map const result2 = getComponentRefByType('abc' as any); // type exists const result3 = getComponentRefByType('def' as any); // type undefined @@ -724,13 +763,13 @@ describe('createSceneDocumentSlice', () => { it(`should be able to findSceneNodeByRef of ${ ['tagComponent', 'modelShaderComponent', 'otherComponent', 'none'][index] }`, () => { - (containsMatchingEntityComponent as any).mockReturnValue(true); + (containsMatchingEntityComponent as jest.Mock).mockReturnValue(true); const document = { nodeMap: { testNode: { ref: 'testRef', components: [component] } } }; const get = jest.fn().mockReturnValue({ document }); // fake out get call const set = jest.fn(); // Act - const { findSceneNodeRefBy } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { findSceneNodeRefBy } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed const result = findSceneNodeRefBy('whatever'); if (index <= 1) { @@ -743,7 +782,7 @@ describe('createSceneDocumentSlice', () => { }); it('should not return ref when no matching for the component type filter', () => { - (containsMatchingEntityComponent as any).mockReturnValue(true); + (containsMatchingEntityComponent as jest.Mock).mockReturnValue(true); const document = { nodeMap: { testTagNode: { ref: 'tagRef', components: [tagComponent] }, @@ -753,7 +792,7 @@ describe('createSceneDocumentSlice', () => { const get = jest.fn().mockReturnValue({ document }); // fake out get call const set = jest.fn(); - const { findSceneNodeRefBy } = createSceneDocumentSlice(set, get as any, undefined as any); // api is never used in the function, so it's not needed + const { findSceneNodeRefBy } = createSceneDocumentSlice(set, get); // api is never used in the function, so it's not needed const result1 = findSceneNodeRefBy('whatever', [KnownComponentType.Tag]); expect(containsMatchingEntityComponent).toBeCalledWith('whatever', undefined); diff --git a/packages/scene-composer/src/store/slices/SceneDocumentSlice.ts b/packages/scene-composer/src/store/slices/SceneDocumentSlice.ts index 18c65eb56..641eba806 100644 --- a/packages/scene-composer/src/store/slices/SceneDocumentSlice.ts +++ b/packages/scene-composer/src/store/slices/SceneDocumentSlice.ts @@ -1,4 +1,5 @@ -import { GetState, SetState, StoreApi } from 'zustand'; +import { GetState, SetState } from 'zustand'; +import { isEmpty } from 'lodash'; import DebugLogger from '../../logger/DebugLogger'; import { RecursivePartial } from '../../utils/typeUtils'; @@ -23,7 +24,8 @@ import { } from '../internalInterfaces'; import interfaceHelpers from '../helpers/interfaceHelpers'; import editorStateHelpers from '../helpers/editorStateHelpers'; -import { ISceneNode, KnownComponentType, KnownSceneProperty } from '../../interfaces'; +import { IOverlaySettings, ISceneNode, KnownComponentType, KnownSceneProperty } from '../../interfaces'; +import { Component } from '../../models/SceneModels'; const LOG = new DebugLogger('stateStore'); @@ -73,11 +75,7 @@ function createEmptyDocumentState(): ISceneDocumentInternal { }; } -export const createSceneDocumentSlice = ( - set: SetState, - get: GetState, - api: StoreApi, -) => +export const createSceneDocumentSlice = (set: SetState, get: GetState) => ({ document: createEmptyDocumentState(), @@ -98,13 +96,21 @@ export const createSceneDocumentSlice = ( draft.document = createEmptyDocumentState(); } + // Initialize view option values based on settings from the scene document + const overlaySettings: IOverlaySettings | undefined = + result.document?.properties?.[KnownSceneProperty.ComponentSettings]?.[KnownComponentType.DataOverlay]; + if (overlaySettings) { + draft.noHistoryStates.componentVisibilities[Component.DataOverlaySubType.OverlayPanel] = + overlaySettings.overlayPanelVisible; + } + draft.sceneLoaded = true; draft.lastOperation = 'loadScene'; }); // We cannot nest set operations in the store impl, so we'll need to delay // setting error messages here. - if (errors) { + if (errors && !isEmpty(errors)) { get().addMessages(errors.map(editorStateHelpers.convertErrorToDisplayMessage)); } }, diff --git a/packages/scene-composer/src/store/slices/ViewOptionStateSlice.spec.ts b/packages/scene-composer/src/store/slices/ViewOptionStateSlice.spec.ts index 9c5e56202..37979a407 100644 --- a/packages/scene-composer/src/store/slices/ViewOptionStateSlice.spec.ts +++ b/packages/scene-composer/src/store/slices/ViewOptionStateSlice.spec.ts @@ -1,33 +1,75 @@ -import { ITagSettings } from '../../interfaces'; +import { ITagSettings, KnownComponentType } from '../../interfaces'; +import { Component } from '../../models/SceneModels'; import { createViewOptionStateSlice } from './ViewOptionStateSlice'; describe('createViewOptionStateSlice', () => { - it('should be able to change motioon indicator visibility', () => { - const draft = { lastOperation: undefined, noHistoryStates: { motionIndicatorVisible: false } }; + it('should be able to change motion indicator visibility', () => { + const draft = { + lastOperation: undefined, + noHistoryStates: { componentVisibilities: { [KnownComponentType.MotionIndicator]: false } }, + }; - const get = jest.fn(); const set = jest.fn((callback) => callback(draft)); - const { toggleMotionIndicatorVisibility } = createViewOptionStateSlice(set, get); - toggleMotionIndicatorVisibility(); + const { toggleComponentVisibility } = createViewOptionStateSlice(set); + toggleComponentVisibility(KnownComponentType.MotionIndicator); - expect(draft.lastOperation!).toEqual('toggleMotionIndicatorVisibility'); - expect(draft.noHistoryStates.motionIndicatorVisible).toBeTruthy(); + expect(draft.lastOperation!).toEqual('toggleComponentVisibility'); + expect(draft.noHistoryStates.componentVisibilities[KnownComponentType.MotionIndicator]).toBeTruthy(); - toggleMotionIndicatorVisibility(); + toggleComponentVisibility(KnownComponentType.MotionIndicator); - expect(draft.lastOperation!).toEqual('toggleMotionIndicatorVisibility'); - expect(draft.noHistoryStates.motionIndicatorVisible).toBeFalsy(); + expect(draft.lastOperation!).toEqual('toggleComponentVisibility'); + expect(draft.noHistoryStates.componentVisibilities[KnownComponentType.MotionIndicator]).toBeFalsy(); + }); + + it('should be able to change overlay panel visibility', () => { + const draft = { + lastOperation: undefined, + noHistoryStates: { componentVisibilities: { [Component.DataOverlaySubType.OverlayPanel]: false } }, + }; + + const set = jest.fn((callback) => callback(draft)); + + const { toggleComponentVisibility } = createViewOptionStateSlice(set); + toggleComponentVisibility(Component.DataOverlaySubType.OverlayPanel); + + expect(draft.lastOperation!).toEqual('toggleComponentVisibility'); + expect(draft.noHistoryStates.componentVisibilities[Component.DataOverlaySubType.OverlayPanel]).toBeTruthy(); + + toggleComponentVisibility(Component.DataOverlaySubType.OverlayPanel); + + expect(draft.lastOperation!).toEqual('toggleComponentVisibility'); + expect(draft.noHistoryStates.componentVisibilities[Component.DataOverlaySubType.OverlayPanel]).toBeFalsy(); + }); + + it('should be able to change text annotation visibility', () => { + const draft = { + lastOperation: undefined, + noHistoryStates: { componentVisibilities: { [Component.DataOverlaySubType.TextAnnotation]: false } }, + }; + + const set = jest.fn((callback) => callback(draft)); + + const { toggleComponentVisibility } = createViewOptionStateSlice(set); + toggleComponentVisibility(Component.DataOverlaySubType.TextAnnotation); + + expect(draft.lastOperation!).toEqual('toggleComponentVisibility'); + expect(draft.noHistoryStates.componentVisibilities[Component.DataOverlaySubType.TextAnnotation]).toBeTruthy(); + + toggleComponentVisibility(Component.DataOverlaySubType.TextAnnotation); + + expect(draft.lastOperation!).toEqual('toggleComponentVisibility'); + expect(draft.noHistoryStates.componentVisibilities[Component.DataOverlaySubType.TextAnnotation]).toBeFalsy(); }); it('should be able to change tag settings', () => { const draft = { lastOperation: undefined, noHistoryStates: { tagSettings: {} } }; - const get = jest.fn(); const set = jest.fn((callback) => callback(draft)); - const { setTagSettings } = createViewOptionStateSlice(set, get); + const { setTagSettings } = createViewOptionStateSlice(set); setTagSettings({ scale: 3.3, autoRescale: true }); expect(draft.lastOperation!).toEqual('setTagSettings'); diff --git a/packages/scene-composer/src/store/slices/ViewOptionStateSlice.ts b/packages/scene-composer/src/store/slices/ViewOptionStateSlice.ts index 5bcf7027d..f57548cbb 100644 --- a/packages/scene-composer/src/store/slices/ViewOptionStateSlice.ts +++ b/packages/scene-composer/src/store/slices/ViewOptionStateSlice.ts @@ -1,27 +1,34 @@ import { SetState } from 'zustand'; -import { ITagSettings } from '../../interfaces'; +import { ITagSettings, KnownComponentType } from '../../interfaces'; +import { Component } from '../../models/SceneModels'; import { RootState } from '../Store'; export interface IViewOptionStateSlice { - motionIndicatorVisible: boolean; + componentVisibilities: Partial<{ + [key in KnownComponentType | Component.DataOverlaySubType]: boolean; + }>; tagSettings?: ITagSettings; enableMatterportViewer?: boolean; - toggleMotionIndicatorVisibility: () => void; + toggleComponentVisibility: (componentType: KnownComponentType | Component.DataOverlaySubType) => void; setTagSettings: (settings: ITagSettings) => void; setMatterportViewerEnabled: (isEnabled: boolean) => void; } export const createViewOptionStateSlice = (set: SetState): IViewOptionStateSlice => ({ - motionIndicatorVisible: true, + componentVisibilities: { + [KnownComponentType.MotionIndicator]: true, + [Component.DataOverlaySubType.TextAnnotation]: true, + }, tagSettings: undefined, enableMatterportViewer: false, - toggleMotionIndicatorVisibility: () => { + toggleComponentVisibility: (componentType) => { set((draft) => { - draft.noHistoryStates.motionIndicatorVisible = !draft.noHistoryStates.motionIndicatorVisible; - draft.lastOperation = 'toggleMotionIndicatorVisibility'; + draft.noHistoryStates.componentVisibilities[componentType] = + !draft.noHistoryStates.componentVisibilities[componentType]; + draft.lastOperation = 'toggleComponentVisibility'; }); }, setTagSettings: (settings) => { diff --git a/packages/scene-composer/src/utils/componentSettingsUtils.spec.ts b/packages/scene-composer/src/utils/componentSettingsUtils.spec.ts index 79a60a9e0..1d4e6d731 100644 --- a/packages/scene-composer/src/utils/componentSettingsUtils.spec.ts +++ b/packages/scene-composer/src/utils/componentSettingsUtils.spec.ts @@ -1,26 +1,36 @@ -import { DEFAULT_TAG_GLOBAL_SETTINGS } from '../common/constants'; +import { DEFAULT_OVERLAY_GLOBAL_SETTINGS, DEFAULT_TAG_GLOBAL_SETTINGS } from '../common/constants'; import { KnownComponentType } from '../interfaces'; +import { useStore } from '../store'; import { componentSettingsSelector } from './componentSettingsUtils'; describe('componentSettingsUtils', () => { describe('componentSettingsSelector', () => { it('should return default tag settings', () => { - const state = { getSceneProperty: jest.fn() } as any; + const state = { ...useStore('default').getState(), getSceneProperty: jest.fn() }; const tagSettings = componentSettingsSelector(state, KnownComponentType.Tag); expect(tagSettings).toEqual(DEFAULT_TAG_GLOBAL_SETTINGS); }); + it('should return default overlay settings', () => { + const state = { ...useStore('default').getState(), getSceneProperty: jest.fn() }; + const settings = componentSettingsSelector(state, KnownComponentType.DataOverlay); + expect(settings).toEqual(DEFAULT_OVERLAY_GLOBAL_SETTINGS); + }); + it('should return tag settings from scene', () => { const expected = { scale: 1.6, autoRescale: true }; - const state = { getSceneProperty: jest.fn().mockReturnValue({ Tag: expected }) } as any; + const state = { + ...useStore('default').getState(), + getSceneProperty: jest.fn().mockReturnValue({ Tag: expected }), + }; const tagSettings = componentSettingsSelector(state, KnownComponentType.Tag); expect(tagSettings).toEqual(expected); }); it('should return empty settings for unknown component type', () => { - const state = { getSceneProperty: jest.fn() } as any; - const settings = componentSettingsSelector(state, 'Random' as any); + const state = { ...useStore('default').getState(), getSceneProperty: jest.fn() }; + const settings = componentSettingsSelector(state, 'Random' as KnownComponentType); expect(settings).toEqual({}); }); }); diff --git a/packages/scene-composer/src/utils/componentSettingsUtils.ts b/packages/scene-composer/src/utils/componentSettingsUtils.ts index cedaa39f5..30a62141f 100644 --- a/packages/scene-composer/src/utils/componentSettingsUtils.ts +++ b/packages/scene-composer/src/utils/componentSettingsUtils.ts @@ -1,6 +1,6 @@ import { RootState } from '../store'; import { KnownSceneProperty, IComponentSettings, KnownComponentType } from '../interfaces'; -import { DEFAULT_TAG_GLOBAL_SETTINGS } from '../common/constants'; +import { DEFAULT_OVERLAY_GLOBAL_SETTINGS, DEFAULT_TAG_GLOBAL_SETTINGS } from '../common/constants'; export const componentSettingsSelector = (state: RootState, componentType: KnownComponentType): IComponentSettings => { const settings = state.getSceneProperty(KnownSceneProperty.ComponentSettings)?.[componentType]; @@ -11,6 +11,8 @@ export const componentSettingsSelector = (state: RootState, componentType: Known switch (componentType) { case KnownComponentType.Tag: return DEFAULT_TAG_GLOBAL_SETTINGS; + case KnownComponentType.DataOverlay: + return DEFAULT_OVERLAY_GLOBAL_SETTINGS; default: return {}; } diff --git a/packages/scene-composer/stories/Developer/SceneComposer.stories.mdx b/packages/scene-composer/stories/Developer/SceneComposer.stories.mdx index 26ab53a1e..019c7a72b 100644 --- a/packages/scene-composer/stories/Developer/SceneComposer.stories.mdx +++ b/packages/scene-composer/stories/Developer/SceneComposer.stories.mdx @@ -8,7 +8,7 @@ import { COMPOSER_FEATURES } from '../../src/interfaces/feature'; export const defaultArgs = { source: 'local', - scene: 'scene1', + scene: 'scene_1', theme: 'dark', mode: 'Editing', density: 'comfortable', @@ -19,6 +19,8 @@ export const defaultArgs = { COMPOSER_FEATURES.ENHANCED_EDITING, COMPOSER_FEATURES.CameraView, COMPOSER_FEATURES.OpacityRule, + COMPOSER_FEATURES.Overlay, + COMPOSER_FEATURES.TagResize, ] } diff --git a/packages/scene-composer/stories/components/scene-composer.tsx b/packages/scene-composer/stories/components/scene-composer.tsx index 2aee50f28..fae273635 100644 --- a/packages/scene-composer/stories/components/scene-composer.tsx +++ b/packages/scene-composer/stories/components/scene-composer.tsx @@ -64,7 +64,7 @@ const SceneComposerWrapper: FC = ({ ...props }: SceneComposerWrapperProps) => { const stagedScene = useRef(undefined); - const scene = sceneId || localScene || 'scene1'; + const scene = sceneId || localScene || 'scene_1'; const loader = useLoader(source, scene, awsCredentials, workspaceId, sceneId); const sceneMetadataModule = useSceneMetadataModule({ source, scene, awsCredentials, workspaceId, sceneId }); const viewport = useRef({ diff --git a/packages/scene-composer/translations/IotAppKitSceneComposer.en_US.json b/packages/scene-composer/translations/IotAppKitSceneComposer.en_US.json index 8a090d296..071e76656 100644 --- a/packages/scene-composer/translations/IotAppKitSceneComposer.en_US.json +++ b/packages/scene-composer/translations/IotAppKitSceneComposer.en_US.json @@ -243,6 +243,10 @@ "note": "Panel Tab title", "text": "Inspector" }, + "FOvFZQ": { + "note": "Sub section label", + "text": "Annotation" + }, "FeC0Eo": { "note": "rule Id label", "text": "Rule Id" @@ -655,6 +659,10 @@ "note": "Form Field label", "text": "Shadow Settings" }, + "jVpmNU": { + "note": "Sub section label", + "text": "Overlay" + }, "jkdxk0": { "note": "Button text for color opacity percentage", "text": "Opacity : {opacityPercentage, number, percent}"