From 36207609919134c0a511ea51cb9ec39e4bf5a062 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Wed, 5 Jan 2022 13:54:11 -0800 Subject: [PATCH] Condition set output label (#4233) * Show condition set label for condition widgets * CSS changes * Ensure condition set output as labels also works when condition widget is part of a display layout * Adds tests for conditionWidget * Tests for condition label output. Fix breaking tests. * Don't remove event listeners when conditionset changes Co-authored-by: Charles Hacskaylo Co-authored-by: Andrew Henry Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com> --- src/plugins/condition/StyleRuleManager.js | 10 +- .../inspector/ConditionalStylesView.vue | 475 ------------------ .../inspector/MultiSelectStylesView.vue | 280 ----------- .../components/inspector/StylesView.vue | 106 ++-- .../inspector/conditional-styles.scss | 7 +- src/plugins/condition/pluginSpec.js | 166 ++++++ .../components/ConditionWidget.vue | 2 +- src/plugins/conditionWidget/plugin.js | 3 + src/plugins/conditionWidget/pluginSpec.js | 103 ++++ .../mixins/objectStyles-mixin.js | 6 +- src/ui/components/ObjectView.vue | 6 + 11 files changed, 371 insertions(+), 793 deletions(-) delete mode 100644 src/plugins/condition/components/inspector/MultiSelectStylesView.vue create mode 100644 src/plugins/conditionWidget/pluginSpec.js diff --git a/src/plugins/condition/StyleRuleManager.js b/src/plugins/condition/StyleRuleManager.js index a1ab1b6d4c..e1866f9191 100644 --- a/src/plugins/condition/StyleRuleManager.js +++ b/src/plugins/condition/StyleRuleManager.js @@ -109,7 +109,7 @@ export default class StyleRuleManager extends EventEmitter { if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) { this.initialize(styleConfiguration || {}); this.applyStaticStyle(); - this.destroy(); + this.destroy(true); } else { let isNewConditionSet = !this.conditionSetIdentifier || !this.openmct.objects.areIdsEqual(this.conditionSetIdentifier, styleConfiguration.conditionSetIdentifier); @@ -180,15 +180,17 @@ export default class StyleRuleManager extends EventEmitter { this.updateDomainObjectStyle(); } - destroy() { + destroy(skipEventListeners) { if (this.stopProvidingTelemetry) { this.stopProvidingTelemetry(); delete this.stopProvidingTelemetry; } - this.openmct.time.off("bounds", this.refreshData); - this.openmct.editor.off('isEditing', this.toggleSubscription); + if (!skipEventListeners) { + this.openmct.time.off("bounds", this.refreshData); + this.openmct.editor.off('isEditing', this.toggleSubscription); + } this.conditionSetIdentifier = undefined; } diff --git a/src/plugins/condition/components/inspector/ConditionalStylesView.vue b/src/plugins/condition/components/inspector/ConditionalStylesView.vue index 062ac9f35e..e69de29bb2 100644 --- a/src/plugins/condition/components/inspector/ConditionalStylesView.vue +++ b/src/plugins/condition/components/inspector/ConditionalStylesView.vue @@ -1,475 +0,0 @@ -/***************************************************************************** -* Open MCT, Copyright (c) 2014-2021, United States Government -* as represented by the Administrator of the National Aeronautics and Space -* Administration. All rights reserved. -* -* Open MCT is licensed under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* http://www.apache.org/licenses/LICENSE-2.0. -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -* License for the specific language governing permissions and limitations -* under the License. -* -* Open MCT includes source code licensed under additional open source -* licenses. See the Open Source Licenses file (LICENSES.md) included with -* this source code distribution or the Licensing information page available -* at runtime from the About dialog for additional information. -*****************************************************************************/ - - - - diff --git a/src/plugins/condition/components/inspector/MultiSelectStylesView.vue b/src/plugins/condition/components/inspector/MultiSelectStylesView.vue deleted file mode 100644 index d1628fe7d5..0000000000 --- a/src/plugins/condition/components/inspector/MultiSelectStylesView.vue +++ /dev/null @@ -1,280 +0,0 @@ -/***************************************************************************** -* Open MCT, Copyright (c) 2014-2021, United States Government -* as represented by the Administrator of the National Aeronautics and Space -* Administration. All rights reserved. -* -* Open MCT is licensed under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* http://www.apache.org/licenses/LICENSE-2.0. -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -* License for the specific language governing permissions and limitations -* under the License. -* -* Open MCT includes source code licensed under additional open source -* licenses. See the Open Source Licenses file (LICENSES.md) included with -* this source code distribution or the Licensing information page available -* at runtime from the About dialog for additional information. -*****************************************************************************/ - - - - diff --git a/src/plugins/condition/components/inspector/StylesView.vue b/src/plugins/condition/components/inspector/StylesView.vue index b7d1d695ab..5f856c7d10 100644 --- a/src/plugins/condition/components/inspector/StylesView.vue +++ b/src/plugins/condition/components/inspector/StylesView.vue @@ -63,7 +63,7 @@
Conditional Object Styles
-
+ +
+ +
+
+ Condition Set output as label: + Yes No + +
+ object.type === 'conditionWidget'); + + return (hasConditionWidgetObjects || (this.domainObject && this.domainObject.type === 'conditionWidget')); + }, styleableFontItems() { return this.selection.filter(selectionPath => { const item = selectionPath[0].context.item; @@ -205,28 +232,6 @@ export default { return true; }); }, - computedconsolidatedFontStyle() { - let consolidatedFontStyle; - const styles = []; - - this.styleableFontItems.forEach(styleable => { - const fontStyle = this.getFontStyle(styleable[0]); - - styles.push(fontStyle); - }); - - if (styles.length) { - const hasConsolidatedFontSize = styles.length && styles.every((fontStyle, i, arr) => fontStyle.fontSize === arr[0].fontSize); - const hasConsolidatedFont = styles.length && styles.every((fontStyle, i, arr) => fontStyle.font === arr[0].font); - - consolidatedFontStyle = { - fontSize: hasConsolidatedFontSize ? styles[0].fontSize : NON_SPECIFIC, - font: hasConsolidatedFont ? styles[0].font : NON_SPECIFIC - }; - } - - return consolidatedFontStyle; - }, nonSpecificFontProperties() { if (!this.consolidatedFontStyle) { return []; @@ -247,6 +252,8 @@ export default { this.previewAction = new PreviewAction(this.openmct); this.isMultipleSelection = this.selection.length > 1; this.getObjectsAndItemsFromSelection(); + this.useConditionSetOutputAsLabel = this.getConfigurationForLabel(); + if (!this.isMultipleSelection) { let objectStyles = this.getObjectStyles(); this.initializeStaticStyle(objectStyles); @@ -264,6 +271,12 @@ export default { this.stylesManager.on('styleSelected', this.applyStyleToSelection); }, methods: { + getConfigurationForLabel() { + const childObjectUsesLabels = Object.values(this.domainObjectsById || {}).some((object) => object.configuration && object.configuration.useConditionSetOutputAsLabel); + const domainObjectUsesLabels = this.domainObject && this.domainObject.configuration && this.domainObject.configuration.useConditionSetOutputAsLabel; + + return childObjectUsesLabels || domainObjectUsesLabels; + }, getObjectStyles() { let objectStyles; if (this.domainObjectsById) { @@ -487,13 +500,14 @@ export default { this.conditions[conditionConfiguration.id] = conditionConfiguration; let foundStyle = this.findStyleByConditionId(conditionConfiguration.id); + let output = { output: conditionConfiguration.configuration.output }; if (foundStyle) { - foundStyle.style = Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles, foundStyle.style); + foundStyle.style = Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles, foundStyle.style, output); conditionalStyles.push(foundStyle); } else { conditionalStyles.splice(index, 0, { conditionId: conditionConfiguration.id, - style: Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles) + style: Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles, output) }); } }); @@ -715,6 +729,12 @@ export default { } else { objectStyle.styles.forEach((conditionalStyle, index) => { let style = {}; + if (domainObject.configuration.useConditionSetOutputAsLabel) { + style.output = conditionalStyle.style.output; + } else { + style.output = ''; + } + Object.keys(item.applicableStyles).concat(['isStyleInvisible']).forEach(key => { style[key] = conditionalStyle.style[key]; }); @@ -731,10 +751,21 @@ export default { } }); } else { - domainObjectStyles = { - ...domainObjectStyles, - ...objectStyle - }; + if (domainObject.configuration.useConditionSetOutputAsLabel !== true) { + let objectConditionStyle = JSON.parse(JSON.stringify(objectStyle)); + objectConditionStyle.styles.forEach((conditionalStyle) => { + conditionalStyle.style.output = ''; + }); + domainObjectStyles = { + ...domainObjectStyles, + ...objectConditionStyle + }; + } else { + domainObjectStyles = { + ...domainObjectStyles, + ...objectStyle + }; + } } return domainObjectStyles; @@ -743,6 +774,17 @@ export default { this.selectedConditionId = conditionId; this.getAndPersistStyles(); }, + persistLabelConfiguration() { + if (this.domainObjectsById) { + Object.values(this.domainObjectsById).forEach((object) => { + this.openmct.objects.mutate(object, 'configuration.useConditionSetOutputAsLabel', this.useConditionSetOutputAsLabel); + }); + } else { + this.openmct.objects.mutate(this.domainObject, 'configuration.useConditionSetOutputAsLabel', this.useConditionSetOutputAsLabel); + } + + this.getAndPersistStyles(); + }, persist(domainObject, style) { this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style); }, @@ -863,6 +905,10 @@ export default { const layoutItemType = selectionPath[0].context.layoutItem && selectionPath[0].context.layoutItem.type; return layoutItemType && layoutItemType !== 'subobject-view'; + }, + updateConditionSetOutputLabel(event) { + this.useConditionSetOutputAsLabel = event.target.checked === true; + this.persistLabelConfiguration(); } } }; diff --git a/src/plugins/condition/components/inspector/conditional-styles.scss b/src/plugins/condition/components/inspector/conditional-styles.scss index 9679bdddd6..f046a41429 100644 --- a/src/plugins/condition/components/inspector/conditional-styles.scss +++ b/src/plugins/condition/components/inspector/conditional-styles.scss @@ -39,12 +39,15 @@ flex-direction: column; } + &__elem { + border-bottom: 1px solid $colorInteriorBorder; + padding-bottom: $interiorMargin; + } + &__condition-set { align-items: baseline; - border-bottom: 1px solid $colorInteriorBorder; display: flex; flex-direction: row; - padding-bottom: $interiorMargin; .c-object-label { flex: 1 1 auto; diff --git a/src/plugins/condition/pluginSpec.js b/src/plugins/condition/pluginSpec.js index e148872ce2..a6ba1d748e 100644 --- a/src/plugins/condition/pluginSpec.js +++ b/src/plugins/condition/pluginSpec.js @@ -133,6 +133,168 @@ describe('the plugin', function () { }); }); + describe('the condition set usage for condition widgets', () => { + let conditionWidgetItem; + let selection; + let component; + let styleViewComponentObject; + const conditionSetDomainObject = { + "configuration": { + "conditionTestData": [ + { + "telemetry": "", + "metadata": "", + "input": "" + } + ], + "conditionCollection": [ + { + "id": "39584410-cbf9-499e-96dc-76f27e69885d", + "configuration": { + "name": "Unnamed Condition", + "output": "Sine > 0", + "trigger": "all", + "criteria": [ + { + "id": "85fbb2f7-7595-42bd-9767-a932266c5225", + "telemetry": { + "namespace": "", + "key": "be0ba97f-b510-4f40-a18d-4ff121d5ea1a" + }, + "operation": "greaterThan", + "input": [ + "0" + ], + "metadata": "sin" + }, + { + "id": "35400132-63b0-425c-ac30-8197df7d5862", + "telemetry": "any", + "operation": "enumValueIs", + "input": [ + "0" + ], + "metadata": "state" + } + ] + }, + "summary": "Match if all criteria are met: Sine Wave Generator Sine > 0 and any telemetry State is OFF " + }, + { + "isDefault": true, + "id": "2532d90a-e0d6-4935-b546-3123522da2de", + "configuration": { + "name": "Default", + "output": "Default", + "trigger": "all", + "criteria": [ + ] + }, + "summary": "" + } + ] + }, + "composition": [ + { + "namespace": "", + "key": "be0ba97f-b510-4f40-a18d-4ff121d5ea1a" + }, + { + "namespace": "", + "key": "077ffa67-e78f-4e99-80e0-522ac33a3888" + } + ], + "telemetry": { + }, + "name": "Condition Set", + "type": "conditionSet", + "identifier": { + "namespace": "", + "key": "863012c1-f6ca-4ab0-aed7-fd43d5e4cd12" + } + + }; + + beforeEach(() => { + conditionWidgetItem = { + "label": "Condition Widget", + "conditionalLabel": "", + "configuration": { + }, + "name": "Condition Widget", + "type": "conditionWidget", + "identifier": { + "namespace": "", + "key": "c5e636c1-6771-4c9c-b933-8665cab189b3" + } + }; + selection = [ + [{ + context: { + "item": conditionWidgetItem, + "supportsMultiSelect": false + } + }] + ]; + let viewContainer = document.createElement('div'); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + StylesView + }, + provide: { + openmct: openmct, + selection: selection, + stylesManager + }, + template: '' + }); + + return Vue.nextTick().then(() => { + styleViewComponentObject = component.$root.$children[0]; + styleViewComponentObject.setEditState(true); + }); + }); + + afterEach(() => { + component.$destroy(); + }); + + it('does not include the output label when the flag is disabled', () => { + styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject; + styleViewComponentObject.conditionalStyles = []; + styleViewComponentObject.initializeConditionalStyles(); + expect(styleViewComponentObject.conditionalStyles.length).toBe(2); + + return Vue.nextTick().then(() => { + const hasNoOutput = styleViewComponentObject.domainObject.configuration.objectStyles.styles.every((style) => { + return style.style.output === '' || style.style.output === undefined; + }); + + expect(hasNoOutput).toBeTrue(); + }); + }); + + it('includes the output label when the flag is enabled', () => { + styleViewComponentObject.conditionSetDomainObject = conditionSetDomainObject; + styleViewComponentObject.conditionalStyles = []; + styleViewComponentObject.initializeConditionalStyles(); + expect(styleViewComponentObject.conditionalStyles.length).toBe(2); + + styleViewComponentObject.useConditionSetOutputAsLabel = true; + styleViewComponentObject.persistLabelConfiguration(); + + return Vue.nextTick().then(() => { + const outputs = styleViewComponentObject.domainObject.configuration.objectStyles.styles.map((style) => { + return style.style.output; + }); + expect(outputs.join(',')).toEqual('Sine > 0,Default'); + }); + }); + + }); + describe('the condition set usage for multiple display layout items', () => { let displayLayoutItem; let lineLayoutItem; @@ -449,6 +611,10 @@ describe('the plugin', function () { const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item); const applicableStylesKeys = Object.keys(applicableStyles).concat(['isStyleInvisible']); Object.keys(foundStyle.style).forEach((key) => { + if (key === 'output') { + return; + } + expect(applicableStylesKeys.indexOf(key)).toBeGreaterThan(-1); expect(foundStyle.style[key]).toEqual(conditionalStyle.style[key]); }); diff --git a/src/plugins/conditionWidget/components/ConditionWidget.vue b/src/plugins/conditionWidget/components/ConditionWidget.vue index bc37fb736a..ea3fcc9ee7 100644 --- a/src/plugins/conditionWidget/components/ConditionWidget.vue +++ b/src/plugins/conditionWidget/components/ConditionWidget.vue @@ -26,7 +26,7 @@ :href="url" >
- {{ internalDomainObject.label }} + {{ internalDomainObject.conditionalLabel || internalDomainObject.label }}
diff --git a/src/plugins/conditionWidget/plugin.js b/src/plugins/conditionWidget/plugin.js index 9b9440aed5..deb9d9dc70 100644 --- a/src/plugins/conditionWidget/plugin.js +++ b/src/plugins/conditionWidget/plugin.js @@ -27,12 +27,15 @@ export default function plugin() { openmct.objectViews.addProvider(new ConditionWidgetViewProvider(openmct)); openmct.types.addType('conditionWidget', { + key: 'conditionWidget', name: "Condition Widget", description: "A button that can be used on its own, or dynamically styled with a Condition Set.", creatable: true, cssClass: 'icon-condition-widget', initialize(domainObject) { + domainObject.configuration = {}; domainObject.label = 'Condition Widget'; + domainObject.conditionalLabel = ''; }, form: [ { diff --git a/src/plugins/conditionWidget/pluginSpec.js b/src/plugins/conditionWidget/pluginSpec.js new file mode 100644 index 0000000000..228e2a8c2d --- /dev/null +++ b/src/plugins/conditionWidget/pluginSpec.js @@ -0,0 +1,103 @@ +import { createOpenMct, resetApplicationState } from "utils/testing"; +import ConditionWidgetPlugin from "./plugin"; +import Vue from 'vue'; + +describe('the plugin', function () { + let objectDef; + let element; + let child; + let openmct; + let mockObjectPath; + + beforeEach((done) => { + mockObjectPath = [ + { + name: 'mock folder', + type: 'fake-folder', + identifier: { + key: 'mock-folder', + namespace: '' + } + }, + { + name: 'mock parent folder', + type: 'conditionWidget', + identifier: { + key: 'mock-parent-folder', + namespace: '' + } + } + ]; + + const timeSystem = { + timeSystemKey: 'utc', + bounds: { + start: 1597160002854, + end: 1597181232854 + } + }; + + openmct = createOpenMct(timeSystem); + openmct.install(new ConditionWidgetPlugin()); + + objectDef = openmct.types.get('conditionWidget').definition; + + element = document.createElement('div'); + element.style.width = '640px'; + element.style.height = '480px'; + child = document.createElement('div'); + child.style.width = '640px'; + child.style.height = '480px'; + element.appendChild(child); + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + let mockObject = { + name: 'Condition Widget', + key: 'conditionWidget', + creatable: true + }; + + it('defines a conditionWidget object type with the correct key', () => { + expect(objectDef.key).toEqual(mockObject.key); + }); + + describe('the conditionWidget object', () => { + it('is creatable', () => { + expect(objectDef.creatable).toEqual(mockObject.creatable); + }); + }); + + describe('the view', () => { + let conditionWidgetView; + let testViewObject; + + beforeEach(() => { + testViewObject = { + id: "test-object", + identifier: { + key: "test-object", + namespace: '' + }, + type: "conditionWidget" + }; + + const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath); + conditionWidgetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionWidget'); + let view = conditionWidgetView.view(testViewObject, element); + view.show(child, true); + + return Vue.nextTick(); + }); + + it('provides a view', () => { + expect(conditionWidgetView).toBeDefined(); + }); + }); +}); diff --git a/src/plugins/displayLayout/mixins/objectStyles-mixin.js b/src/plugins/displayLayout/mixins/objectStyles-mixin.js index 76323f81b3..08cca1ca92 100644 --- a/src/plugins/displayLayout/mixins/objectStyles-mixin.js +++ b/src/plugins/displayLayout/mixins/objectStyles-mixin.js @@ -38,10 +38,14 @@ export default { this.objectStyle = this.getObjectStyleForItem(this.parentDomainObject.configuration.objectStyles); this.initObjectStyles(); }, - destroyed() { + beforeDestroy() { if (this.stopListeningObjectStyles) { this.stopListeningObjectStyles(); } + + if (this.styleRuleManager) { + this.styleRuleManager.destroy(); + } }, methods: { getObjectStyleForItem(objectStyle) { diff --git a/src/ui/components/ObjectView.vue b/src/ui/components/ObjectView.vue index 50420c2bc1..a7104a5c5c 100644 --- a/src/ui/components/ObjectView.vue +++ b/src/ui/components/ObjectView.vue @@ -191,6 +191,12 @@ export default { } } }); + + if (this.domainObject && this.domainObject.type === 'conditionWidget' && keys.includes('output')) { + this.openmct.objects.mutate(this.domainObject, 'conditionalLabel', styleObj.output); + } else { + this.openmct.objects.mutate(this.domainObject, 'conditionalLabel', ''); + } }, updateView(immediatelySelect) { this.clear();