mirror of
https://github.com/nasa/openmct.git
synced 2025-06-27 11:32:13 +00:00
Compare commits
16 Commits
notebook-e
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
06cddb6e16 | |||
ca53852014 | |||
d7d8bedf2a | |||
c24c652f4b | |||
1cb4f5b013 | |||
a7df8bf168 | |||
ff269ac390 | |||
cdee5e8102 | |||
159457a52d | |||
d637420da1 | |||
eb4da293c6 | |||
738fac64b8 | |||
7703ec0a61 | |||
52466999b8 | |||
45373c56f7 | |||
91e909bb4a |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,5 +1,5 @@
|
|||||||
<!--- Note: Please open the PR in draft form until you are ready for active review. -->
|
<!--- Note: Please open the PR in draft form until you are ready for active review. -->
|
||||||
Closes: <!--- Insert Issue Number(s) this PR addresses. Start by typing # will open a dropdown of recent issues. -->
|
Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will open a dropdown of recent issues. Note: this does not work on PRs which target release branches -->
|
||||||
|
|
||||||
### Describe your changes:
|
### Describe your changes:
|
||||||
<!--- Describe your changes and add any comments about your approach either here or inline if code comments aren't added -->
|
<!--- Describe your changes and add any comments about your approach either here or inline if code comments aren't added -->
|
||||||
|
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
@ -10,6 +10,7 @@ updates:
|
|||||||
- "type:maintenance"
|
- "type:maintenance"
|
||||||
- "dependencies"
|
- "dependencies"
|
||||||
- "pr:e2e"
|
- "pr:e2e"
|
||||||
|
- "pr:daveit"
|
||||||
allow:
|
allow:
|
||||||
- dependency-name: "*eslint*"
|
- dependency-name: "*eslint*"
|
||||||
- dependency-name: "*karma*"
|
- dependency-name: "*karma*"
|
||||||
@ -25,4 +26,4 @@ updates:
|
|||||||
labels:
|
labels:
|
||||||
- "type:maintenance"
|
- "type:maintenance"
|
||||||
- "dependencies"
|
- "dependencies"
|
||||||
- "prcop:disable"
|
- "pr:daveit"
|
||||||
|
2
.github/workflows/prcop-config.json
vendored
2
.github/workflows/prcop-config.json
vendored
@ -15,5 +15,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"disableWord": "prcop:disable"
|
"disableWord": "pr:daveit"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "openmct",
|
"name": "openmct",
|
||||||
"version": "1.8.4-SNAPSHOT",
|
"version": "1.8.5-SNAPSHOT",
|
||||||
"description": "The Open MCT core platform",
|
"description": "The Open MCT core platform",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@braintree/sanitize-url": "^5.0.2",
|
"@braintree/sanitize-url": "^5.0.2",
|
||||||
@ -20,7 +20,7 @@
|
|||||||
"d3-scale": "1.0.x",
|
"d3-scale": "1.0.x",
|
||||||
"d3-selection": "1.3.x",
|
"d3-selection": "1.3.x",
|
||||||
"eslint": "7.0.0",
|
"eslint": "7.0.0",
|
||||||
"eslint-plugin-playwright": "0.7.1",
|
"eslint-plugin-playwright": "0.8.0",
|
||||||
"eslint-plugin-vue": "^7.5.0",
|
"eslint-plugin-vue": "^7.5.0",
|
||||||
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
|
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
|
||||||
"eventemitter3": "^1.2.0",
|
"eventemitter3": "^1.2.0",
|
||||||
@ -73,7 +73,7 @@
|
|||||||
"uuid": "^3.3.3",
|
"uuid": "^3.3.3",
|
||||||
"v8-compile-cache": "^1.1.0",
|
"v8-compile-cache": "^1.1.0",
|
||||||
"vue": "2.5.6",
|
"vue": "2.5.6",
|
||||||
"vue-eslint-parser": "8.0.1",
|
"vue-eslint-parser": "8.2.0",
|
||||||
"vue-loader": "15.9.8",
|
"vue-loader": "15.9.8",
|
||||||
"vue-template-compiler": "2.5.6",
|
"vue-template-compiler": "2.5.6",
|
||||||
"webpack": "^5.65.0",
|
"webpack": "^5.65.0",
|
||||||
@ -114,7 +114,7 @@
|
|||||||
"url": "https://github.com/nasa/openmct.git"
|
"url": "https://github.com/nasa/openmct.git"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.1 <15.0.0"
|
"node": ">=12.20.1 <15.0.0"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
@ -21,19 +21,19 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelectorDialogTree :ignore-type-check="true"
|
<mct-tree
|
||||||
:css-class="`form-locator c-form-control--locator`"
|
:is-selector-tree="true"
|
||||||
:parent="model.parent"
|
:initial-selection="model.parent"
|
||||||
@treeItemSelected="handleItemSelection"
|
@tree-item-selection="handleItemSelection"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import SelectorDialogTree from '@/ui/components/SelectorDialogTree.vue';
|
import MctTree from '@/ui/layout/mct-tree.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
SelectorDialogTree
|
MctTree
|
||||||
},
|
},
|
||||||
inject: ['openmct'],
|
inject: ['openmct'],
|
||||||
props: {
|
props: {
|
||||||
@ -43,10 +43,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleItemSelection({ parentObjectPath }) {
|
handleItemSelection(item) {
|
||||||
const data = {
|
const data = {
|
||||||
model: this.model,
|
model: this.model,
|
||||||
value: parentObjectPath
|
value: item.objectPath
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$emit('onChange', data);
|
this.$emit('onChange', data);
|
||||||
|
@ -465,23 +465,6 @@ ObjectAPI.prototype.mutate = function (domainObject, path, value) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a domain object based on its latest persisted state. Note that this will mutate the provided object.
|
|
||||||
* @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store
|
|
||||||
* @returns {Promise} the provided object, updated to reflect the latest persisted state of the object.
|
|
||||||
*/
|
|
||||||
ObjectAPI.prototype.refresh = async function (domainObject) {
|
|
||||||
const refreshedObject = await this.get(domainObject.identifier);
|
|
||||||
|
|
||||||
if (domainObject.isMutable) {
|
|
||||||
domainObject.$refresh(refreshedObject);
|
|
||||||
} else {
|
|
||||||
utils.refresh(domainObject, refreshedObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
return domainObject;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
@ -123,7 +123,10 @@ export default {
|
|||||||
automargin: true,
|
automargin: true,
|
||||||
fixedrange: true
|
fixedrange: true
|
||||||
},
|
},
|
||||||
yaxis: primaryYaxis,
|
yaxis: {
|
||||||
|
...primaryYaxis,
|
||||||
|
type: 'log'
|
||||||
|
},
|
||||||
margin: {
|
margin: {
|
||||||
l: 5,
|
l: 5,
|
||||||
r: 5,
|
r: 5,
|
||||||
|
@ -62,6 +62,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.xValues = [];
|
||||||
|
this.yValues = [];
|
||||||
this.refreshData = this.refreshData.bind(this);
|
this.refreshData = this.refreshData.bind(this);
|
||||||
this.setTimeContext();
|
this.setTimeContext();
|
||||||
|
|
||||||
@ -156,7 +158,7 @@ export default {
|
|||||||
|
|
||||||
const yAxisMetadata = metadata.valuesForHints(['range'])[0];
|
const yAxisMetadata = metadata.valuesForHints(['range'])[0];
|
||||||
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
|
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
|
||||||
const xAxisMetadata = metadata.valuesForHints(['range']);
|
const xAxisMetadata = metadata.valuesForHints(['domain'])[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
xAxisMetadata,
|
xAxisMetadata,
|
||||||
@ -236,29 +238,29 @@ export default {
|
|||||||
let yValues = [];
|
let yValues = [];
|
||||||
|
|
||||||
//populate X and Y values for plotly
|
//populate X and Y values for plotly
|
||||||
axisMetadata.xAxisMetadata.forEach((metadata) => {
|
xValues.push(this.format(key, axisMetadata.xAxisMetadata.key, data));
|
||||||
xValues.push(metadata.name);
|
if (data[axisMetadata.yAxisMetadata.key]) {
|
||||||
if (data[metadata.key]) {
|
const formattedValue = this.format(key, axisMetadata.yAxisMetadata.key, data);
|
||||||
const formattedValue = this.format(key, metadata.key, data);
|
|
||||||
yValues.push(formattedValue);
|
yValues.push(formattedValue);
|
||||||
} else {
|
} else {
|
||||||
yValues.push(null);
|
yValues.push(null);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
this.xValues = this.xValues.concat(xValues);
|
||||||
|
this.yValues = this.yValues.concat(yValues);
|
||||||
|
|
||||||
const trace = {
|
const trace = {
|
||||||
key,
|
key,
|
||||||
name: telemetryObject.name,
|
name: telemetryObject.name,
|
||||||
x: xValues,
|
x: this.xValues,
|
||||||
y: yValues,
|
y: this.yValues,
|
||||||
text: yValues.map(String),
|
text: yValues.map(String),
|
||||||
xAxisMetadata: axisMetadata.xAxisMetadata,
|
xAxisMetadata: axisMetadata.xAxisMetadata,
|
||||||
yAxisMetadata: axisMetadata.yAxisMetadata,
|
yAxisMetadata: axisMetadata.yAxisMetadata,
|
||||||
type: 'bar',
|
type: 'scatter',
|
||||||
marker: {
|
marker: {
|
||||||
color: this.domainObject.configuration.barStyles.series[key].color
|
color: this.domainObject.configuration.barStyles.series[key].color
|
||||||
},
|
}
|
||||||
hoverinfo: 'skip'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.addTrace(trace, key);
|
this.addTrace(trace, key);
|
||||||
|
@ -148,10 +148,8 @@ import FontStyleEditor from '@/ui/inspector/styles/FontStyleEditor.vue';
|
|||||||
import StyleEditor from "./StyleEditor.vue";
|
import StyleEditor from "./StyleEditor.vue";
|
||||||
import PreviewAction from "@/ui/preview/PreviewAction.js";
|
import PreviewAction from "@/ui/preview/PreviewAction.js";
|
||||||
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils";
|
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils";
|
||||||
import SelectorDialogTree from '@/ui/components/SelectorDialogTree.vue';
|
|
||||||
import ConditionError from "@/plugins/condition/components/ConditionError.vue";
|
import ConditionError from "@/plugins/condition/components/ConditionError.vue";
|
||||||
import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue";
|
import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue";
|
||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
const NON_SPECIFIC = '??';
|
const NON_SPECIFIC = '??';
|
||||||
const NON_STYLEABLE_CONTAINER_TYPES = [
|
const NON_STYLEABLE_CONTAINER_TYPES = [
|
||||||
@ -551,53 +549,28 @@ export default {
|
|||||||
return this.conditions ? this.conditions[id] : {};
|
return this.conditions ? this.conditions[id] : {};
|
||||||
},
|
},
|
||||||
addConditionSet() {
|
addConditionSet() {
|
||||||
let conditionSetDomainObject;
|
const conditionWidgetParent = this.openmct.router.path[1];
|
||||||
let self = this;
|
const formStructure = {
|
||||||
function handleItemSelection({ item }) {
|
title: 'Select Condition Set',
|
||||||
if (item) {
|
sections: [{
|
||||||
conditionSetDomainObject = item;
|
name: 'Location',
|
||||||
}
|
cssClass: 'grows',
|
||||||
}
|
rows: [{
|
||||||
|
key: 'location',
|
||||||
function dismissDialog(overlay, initialize) {
|
name: 'Condition Set',
|
||||||
overlay.dismiss();
|
cssClass: 'grows',
|
||||||
|
control: 'locator',
|
||||||
if (initialize && conditionSetDomainObject) {
|
required: true,
|
||||||
self.conditionSetDomainObject = conditionSetDomainObject;
|
parent: conditionWidgetParent,
|
||||||
self.conditionalStyles = [];
|
validate: data => data.value[0].type === 'conditionSet'
|
||||||
self.initializeConditionalStyles();
|
}]
|
||||||
}
|
}]
|
||||||
}
|
|
||||||
|
|
||||||
let vm = new Vue({
|
|
||||||
components: { SelectorDialogTree },
|
|
||||||
provide: {
|
|
||||||
openmct: this.openmct
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
handleItemSelection,
|
|
||||||
title: 'Select Condition Set'
|
|
||||||
};
|
};
|
||||||
},
|
|
||||||
template: '<SelectorDialogTree :title="title" @treeItemSelected="handleItemSelection"></SelectorDialogTree>'
|
|
||||||
}).$mount();
|
|
||||||
|
|
||||||
let overlay = this.openmct.overlays.overlay({
|
this.openmct.forms.showForm(formStructure).then(data => {
|
||||||
element: vm.$el,
|
this.conditionSetDomainObject = data.location[0];
|
||||||
size: 'small',
|
this.conditionalStyles = [];
|
||||||
buttons: [
|
this.initializeConditionalStyles();
|
||||||
{
|
|
||||||
label: 'OK',
|
|
||||||
emphasis: 'true',
|
|
||||||
callback: () => dismissDialog(overlay, true)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Cancel',
|
|
||||||
callback: () => dismissDialog(overlay, false)
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onDestroy: () => vm.$destroy()
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
removeConditionSet() {
|
removeConditionSet() {
|
||||||
|
@ -209,18 +209,7 @@ export default {
|
|||||||
},
|
},
|
||||||
layoutItems: {
|
layoutItems: {
|
||||||
handler(value) {
|
handler(value) {
|
||||||
let wMax = this.$el.clientWidth / this.gridSize[0];
|
this.updateGrid();
|
||||||
let hMax = this.$el.clientHeight / this.gridSize[1];
|
|
||||||
value.forEach(item => {
|
|
||||||
if (item.x + item.width > wMax) {
|
|
||||||
wMax = item.x + item.width + 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.y + item.height > hMax) {
|
|
||||||
hMax = item.y + item.height + 2;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.gridDimensions = [wMax * this.gridSize[0], hMax * this.gridSize[1]];
|
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
@ -233,6 +222,8 @@ export default {
|
|||||||
this.composition.on('remove', this.removeChild);
|
this.composition.on('remove', this.removeChild);
|
||||||
this.composition.load();
|
this.composition.load();
|
||||||
this.gridDimensions = [this.$el.offsetWidth, this.$el.scrollHeight];
|
this.gridDimensions = [this.$el.offsetWidth, this.$el.scrollHeight];
|
||||||
|
|
||||||
|
this.watchDisplayResize();
|
||||||
},
|
},
|
||||||
destroyed: function () {
|
destroyed: function () {
|
||||||
this.openmct.selection.off('change', this.setSelection);
|
this.openmct.selection.off('change', this.setSelection);
|
||||||
@ -240,6 +231,25 @@ export default {
|
|||||||
this.composition.off('remove', this.removeChild);
|
this.composition.off('remove', this.removeChild);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateGrid() {
|
||||||
|
let wMax = this.$el.clientWidth / this.gridSize[0];
|
||||||
|
let hMax = this.$el.clientHeight / this.gridSize[1];
|
||||||
|
this.layoutItems.forEach(item => {
|
||||||
|
if (item.x + item.width > wMax) {
|
||||||
|
wMax = item.x + item.width + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.y + item.height > hMax) {
|
||||||
|
hMax = item.y + item.height + 2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.gridDimensions = [wMax * this.gridSize[0], hMax * this.gridSize[1]];
|
||||||
|
},
|
||||||
|
watchDisplayResize() {
|
||||||
|
const resizeObserver = new ResizeObserver(() => this.updateGrid());
|
||||||
|
|
||||||
|
resizeObserver.observe(this.$el);
|
||||||
|
},
|
||||||
addElement(itemType, element) {
|
addElement(itemType, element) {
|
||||||
this.addItem(itemType + '-view', element);
|
this.addItem(itemType + '-view', element);
|
||||||
},
|
},
|
||||||
@ -404,8 +414,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
containsObject(identifier) {
|
containsObject(identifier) {
|
||||||
return _.get(this.domainObject, 'composition')
|
if ('composition' in this.domainObject) {
|
||||||
|
return this.domainObject.composition
|
||||||
.some(childId => this.openmct.objects.areIdsEqual(childId, identifier));
|
.some(childId => this.openmct.objects.areIdsEqual(childId, identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
handleDragOver($event) {
|
handleDragOver($event) {
|
||||||
if (this.domainObject.locked) {
|
if (this.domainObject.locked) {
|
||||||
@ -494,7 +508,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeFromComposition(keyString) {
|
removeFromComposition(keyString) {
|
||||||
let composition = _.get(this.domainObject, 'composition');
|
let composition = this.domainObject.composition ? this.domainObject.composition : [];
|
||||||
composition = composition.filter(identifier => {
|
composition = composition.filter(identifier => {
|
||||||
return this.openmct.objects.makeKeyString(identifier) !== keyString;
|
return this.openmct.objects.makeKeyString(identifier) !== keyString;
|
||||||
});
|
});
|
||||||
|
@ -322,7 +322,7 @@ export default {
|
|||||||
cleanupDefaultNotebook() {
|
cleanupDefaultNotebook() {
|
||||||
this.defaultPageId = undefined;
|
this.defaultPageId = undefined;
|
||||||
this.defaultSectionId = undefined;
|
this.defaultSectionId = undefined;
|
||||||
this.removeDefaultClass(this.domainObject);
|
this.removeDefaultClass(this.domainObject.identifier);
|
||||||
clearDefaultNotebook();
|
clearDefaultNotebook();
|
||||||
},
|
},
|
||||||
setSectionAndPageFromUrl() {
|
setSectionAndPageFromUrl() {
|
||||||
@ -619,12 +619,8 @@ export default {
|
|||||||
|
|
||||||
this.sectionsChanged({ sections });
|
this.sectionsChanged({ sections });
|
||||||
},
|
},
|
||||||
removeDefaultClass(defaultNotebookIdentifier) {
|
removeDefaultClass(identifier) {
|
||||||
if (!defaultNotebookIdentifier) {
|
this.openmct.status.delete(identifier);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.openmct.status.delete(defaultNotebookIdentifier);
|
|
||||||
},
|
},
|
||||||
resetSearch() {
|
resetSearch() {
|
||||||
this.search = '';
|
this.search = '';
|
||||||
@ -633,27 +629,24 @@ export default {
|
|||||||
toggleNav() {
|
toggleNav() {
|
||||||
this.showNav = !this.showNav;
|
this.showNav = !this.showNav;
|
||||||
},
|
},
|
||||||
updateDefaultNotebook(notebookStorage) {
|
updateDefaultNotebook(updatedNotebookStorageObject) {
|
||||||
const defaultNotebook = getDefaultNotebook();
|
if (!this.isDefaultNotebook()) {
|
||||||
const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.identifier;
|
const persistedNotebookStorageObject = getDefaultNotebook();
|
||||||
const isSameNotebook = defaultNotebookIdentifier
|
if (persistedNotebookStorageObject.identifier !== undefined) {
|
||||||
&& this.openmct.objects.areIdsEqual(defaultNotebookIdentifier, notebookStorage.identifier);
|
this.removeDefaultClass(persistedNotebookStorageObject.identifier);
|
||||||
if (!isSameNotebook) {
|
|
||||||
this.removeDefaultClass(defaultNotebookIdentifier);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!defaultNotebookIdentifier || !isSameNotebook) {
|
setDefaultNotebook(this.openmct, updatedNotebookStorageObject, this.domainObject);
|
||||||
setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.defaultSectionId !== notebookStorage.defaultSectionId) {
|
if (this.defaultSectionId !== updatedNotebookStorageObject.defaultSectionId) {
|
||||||
setDefaultNotebookSectionId(notebookStorage.defaultSectionId);
|
setDefaultNotebookSectionId(updatedNotebookStorageObject.defaultSectionId);
|
||||||
this.defaultSectionId = notebookStorage.defaultSectionId;
|
this.defaultSectionId = updatedNotebookStorageObject.defaultSectionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.defaultPageId !== notebookStorage.defaultPageId) {
|
if (this.defaultPageId !== updatedNotebookStorageObject.defaultPageId) {
|
||||||
setDefaultNotebookPageId(notebookStorage.defaultPageId);
|
setDefaultNotebookPageId(updatedNotebookStorageObject.defaultPageId);
|
||||||
this.defaultPageId = notebookStorage.defaultPageId;
|
this.defaultPageId = updatedNotebookStorageObject.defaultPageId;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateDefaultNotebookSection(sections, id) {
|
updateDefaultNotebookSection(sections, id) {
|
||||||
@ -671,7 +664,7 @@ export default {
|
|||||||
if (defaultNotebookSectionId === id) {
|
if (defaultNotebookSectionId === id) {
|
||||||
const section = sections.find(s => s.id === id);
|
const section = sections.find(s => s.id === id);
|
||||||
if (!section) {
|
if (!section) {
|
||||||
this.removeDefaultClass(this.domainObject);
|
this.removeDefaultClass(this.domainObject.identifier);
|
||||||
clearDefaultNotebook();
|
clearDefaultNotebook();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -8,6 +8,7 @@ export default function (openmct) {
|
|||||||
return apiSave(domainObject);
|
return apiSave(domainObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNewMutable = !domainObject.isMutable;
|
||||||
const localMutable = openmct.objects._toMutable(domainObject);
|
const localMutable = openmct.objects._toMutable(domainObject);
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
@ -20,18 +21,20 @@ export default function (openmct) {
|
|||||||
result = Promise.reject(error);
|
result = Promise.reject(error);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (isNewMutable) {
|
||||||
openmct.objects.destroyMutable(localMutable);
|
openmct.objects.destroyMutable(localMutable);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveConflicts(localMutable, openmct) {
|
function resolveConflicts(localMutable, openmct) {
|
||||||
|
const localEntries = JSON.parse(JSON.stringify(localMutable.configuration.entries));
|
||||||
|
|
||||||
return openmct.objects.getMutable(localMutable.identifier).then((remoteMutable) => {
|
return openmct.objects.getMutable(localMutable.identifier).then((remoteMutable) => {
|
||||||
const localEntries = localMutable.configuration.entries;
|
applyLocalEntries(remoteMutable, localEntries, openmct);
|
||||||
remoteMutable.$refresh(remoteMutable);
|
|
||||||
applyLocalEntries(remoteMutable, localEntries);
|
|
||||||
|
|
||||||
openmct.objects.destroyMutable(remoteMutable);
|
openmct.objects.destroyMutable(remoteMutable);
|
||||||
|
|
||||||
@ -39,7 +42,7 @@ function resolveConflicts(localMutable, openmct) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyLocalEntries(mutable, entries) {
|
function applyLocalEntries(mutable, entries, openmct) {
|
||||||
Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {
|
Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {
|
||||||
Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => {
|
Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => {
|
||||||
const remoteEntries = mutable.configuration.entries[sectionKey][pageKey];
|
const remoteEntries = mutable.configuration.entries[sectionKey][pageKey];
|
||||||
@ -58,14 +61,15 @@ function applyLocalEntries(mutable, entries) {
|
|||||||
|
|
||||||
locallyModifiedEntries.forEach((locallyModifiedEntry) => {
|
locallyModifiedEntries.forEach((locallyModifiedEntry) => {
|
||||||
let mergedEntry = mergedEntries.find(entry => entry.id === locallyModifiedEntry.id);
|
let mergedEntry = mergedEntries.find(entry => entry.id === locallyModifiedEntry.id);
|
||||||
if (mergedEntry !== undefined) {
|
if (mergedEntry !== undefined
|
||||||
|
&& locallyModifiedEntry.text.match(/\S/)) {
|
||||||
mergedEntry.text = locallyModifiedEntry.text;
|
mergedEntry.text = locallyModifiedEntry.text;
|
||||||
shouldMutate = true;
|
shouldMutate = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (shouldMutate) {
|
if (shouldMutate) {
|
||||||
mutable.$set(`configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
|
openmct.objects.mutate(mutable, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -120,7 +120,6 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
|
|||||||
const newEntries = addEntryIntoPage(notebookStorage, entries, entry);
|
const newEntries = addEntryIntoPage(notebookStorage, entries, entry);
|
||||||
|
|
||||||
addDefaultClass(domainObject, openmct);
|
addDefaultClass(domainObject, openmct);
|
||||||
|
|
||||||
mutateObject(openmct, domainObject, 'configuration.entries', newEntries);
|
mutateObject(openmct, domainObject, 'configuration.entries', newEntries);
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
<div v-for="tick in ticks"
|
<div v-for="tick in ticks"
|
||||||
:key="tick.value"
|
:key="tick.value"
|
||||||
class="gl-plot-tick gl-plot-y-tick-label"
|
class="gl-plot-tick gl-plot-y-tick-label"
|
||||||
:style="{ top: (100 * (max - tick.value) / interval) + '%' }"
|
:style="{ top: tick.top }"
|
||||||
:title="tick.fullText || tick.text"
|
:title="tick.fullText || tick.text"
|
||||||
style="margin-top: -0.50em; direction: ltr;"
|
style="margin-top: -0.50em; direction: ltr;"
|
||||||
>
|
>
|
||||||
@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import eventHelpers from "./lib/eventHelpers";
|
import eventHelpers from "./lib/eventHelpers";
|
||||||
import { ticks, getFormattedTicks } from "./tickUtils";
|
import {d3Ticks, ticks, getFormattedTicks, d3TicksLog} from "./tickUtils";
|
||||||
import configStore from "./configuration/ConfigStore";
|
import configStore from "./configuration/ConfigStore";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -172,7 +172,7 @@ export default {
|
|||||||
}, this);
|
}, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ticks(range.min, range.max, number);
|
return d3Ticks(range.min, range.max, number);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTicksForceRegeneration() {
|
updateTicksForceRegeneration() {
|
||||||
@ -210,6 +210,27 @@ export default {
|
|||||||
|
|
||||||
newTicks = getFormattedTicks(newTicks, format);
|
newTicks = getFormattedTicks(newTicks, format);
|
||||||
|
|
||||||
|
if (this.axisType !== 'xAxis') {
|
||||||
|
let min = this.min;
|
||||||
|
let max = this.max;
|
||||||
|
newTicks = newTicks.map((tick) => {
|
||||||
|
let value;
|
||||||
|
if (tick.value !== 0) {
|
||||||
|
value = (Math.log10(max) - Math.log10(tick.value)) / (Math.log10(max) - Math.log10(min)) * (100);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value < 0) {
|
||||||
|
value = -value;
|
||||||
|
}
|
||||||
|
|
||||||
|
tick.top = value + '%';
|
||||||
|
|
||||||
|
return tick;
|
||||||
|
});
|
||||||
|
console.log(this.min, this.max, newTicks.map(tick => `${tick.value} + ${tick.top}`));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
this.ticks = newTicks;
|
this.ticks = newTicks;
|
||||||
this.shouldCheckWidth = true;
|
this.shouldCheckWidth = true;
|
||||||
}
|
}
|
||||||
|
@ -233,7 +233,9 @@ export default class PlotSeries extends Model {
|
|||||||
return this.limitEvaluator.evaluate(datum, valueMetadata);
|
return this.limitEvaluator.evaluate(datum, valueMetadata);
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
const format = this.formats[newKey];
|
const format = this.formats[newKey];
|
||||||
this.getYVal = format.parse.bind(format);
|
this.getYVal = (point) => {
|
||||||
|
return Math.log(format.parse(point));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
formatX(point) {
|
formatX(point) {
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import * as d3Scale from 'd3-scale';
|
||||||
|
import * as d3Axis from 'd3-axis';
|
||||||
|
|
||||||
const e10 = Math.sqrt(50);
|
const e10 = Math.sqrt(50);
|
||||||
const e5 = Math.sqrt(10);
|
const e5 = Math.sqrt(10);
|
||||||
const e2 = Math.sqrt(2);
|
const e2 = Math.sqrt(2);
|
||||||
@ -40,6 +43,33 @@ function getPrecision(step) {
|
|||||||
return precision;
|
return precision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function d3Ticks(start, stop, count) {
|
||||||
|
let scale = d3Scale.scaleLinear();
|
||||||
|
scale.domain([start, stop]);
|
||||||
|
|
||||||
|
const axis = d3Axis.axisLeft(scale);
|
||||||
|
|
||||||
|
return axis.scale().ticks(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function d3TicksLog(start, stop, count) {
|
||||||
|
if (start < 0) {
|
||||||
|
start = 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scale = d3Scale.scaleLog().domain([start, stop]);
|
||||||
|
|
||||||
|
const axis = d3Axis.axisBottom(scale);
|
||||||
|
|
||||||
|
const generatedTicks = axis.scale().ticks(count);
|
||||||
|
if (generatedTicks.length < 1) {
|
||||||
|
// if the difference between start, stop is small, the ticks might not be generated for log
|
||||||
|
return d3Ticks(start, stop, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return generatedTicks;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Linear tick generation from d3-array.
|
* Linear tick generation from d3-array.
|
||||||
*/
|
*/
|
||||||
|
@ -1,240 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2022, 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.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="u-contents">
|
|
||||||
<div v-if="title.length"
|
|
||||||
class="c-overlay__top-bar"
|
|
||||||
>
|
|
||||||
<div class="c-overlay__dialog-title">{{ title }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="c-selector c-tree-and-search"
|
|
||||||
:class="cssClass"
|
|
||||||
>
|
|
||||||
<div class="c-tree-and-search__search">
|
|
||||||
<Search ref="shell-search"
|
|
||||||
class="c-search"
|
|
||||||
:value="searchValue"
|
|
||||||
@input="searchTree"
|
|
||||||
@clear="searchTree"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="isLoading"
|
|
||||||
class="c-tree-and-search__loading loading"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div v-if="shouldDisplayNoResultsText"
|
|
||||||
class="c-tree-and-search__no-results"
|
|
||||||
>
|
|
||||||
No results found
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul v-if="!isLoading"
|
|
||||||
v-show="!searchValue"
|
|
||||||
class="c-tree-and-search__tree c-tree"
|
|
||||||
>
|
|
||||||
<SelectorDialogTreeItem
|
|
||||||
v-for="treeItem in allTreeItems"
|
|
||||||
:key="treeItem.id"
|
|
||||||
:node="treeItem"
|
|
||||||
:selected-item="selectedItem"
|
|
||||||
:handle-item-selected="handleItemSelection"
|
|
||||||
:navigate-to-parent="navigateToParent"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<ul v-if="searchValue && !isLoading"
|
|
||||||
class="c-tree-and-search__tree c-tree"
|
|
||||||
>
|
|
||||||
<SelectorDialogTreeItem
|
|
||||||
v-for="treeItem in filteredTreeItems"
|
|
||||||
:key="treeItem.id"
|
|
||||||
:node="treeItem"
|
|
||||||
:selected-item="selectedItem"
|
|
||||||
:handle-item-selected="handleItemSelection"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import Search from '@/ui/components/search.vue';
|
|
||||||
import SelectorDialogTreeItem from './SelectorDialogTreeItem.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'SelectorDialogTree',
|
|
||||||
components: {
|
|
||||||
Search,
|
|
||||||
SelectorDialogTreeItem
|
|
||||||
},
|
|
||||||
inject: ['openmct'],
|
|
||||||
props: {
|
|
||||||
cssClass: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default() {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default() {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ignoreTypeCheck: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
parent: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default() {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
allTreeItems: [],
|
|
||||||
expanded: false,
|
|
||||||
filteredTreeItems: [],
|
|
||||||
isLoading: false,
|
|
||||||
navigateToParent: undefined,
|
|
||||||
searchValue: '',
|
|
||||||
selectedItem: undefined
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
shouldDisplayNoResultsText() {
|
|
||||||
if (this.isLoading) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.allTreeItems.length === 0
|
|
||||||
|| (this.searchValue && this.filteredTreeItems.length === 0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.getDebouncedFilteredChildren = debounce(this.getFilteredChildren, 400);
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (this.parent) {
|
|
||||||
(async () => {
|
|
||||||
const objectPath = await this.openmct.objects.getOriginalPath(this.parent.identifier);
|
|
||||||
this.navigateToParent = '/browse/'
|
|
||||||
+ objectPath
|
|
||||||
.map(parent => this.openmct.objects.makeKeyString(parent.identifier))
|
|
||||||
.reverse()
|
|
||||||
.join('/');
|
|
||||||
|
|
||||||
this.getAllChildren(this.navigateToParent);
|
|
||||||
})();
|
|
||||||
} else {
|
|
||||||
this.getAllChildren();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async aggregateFilteredChildren(results) {
|
|
||||||
for (const object of results) {
|
|
||||||
const objectPath = await this.openmct.objects.getOriginalPath(object.identifier);
|
|
||||||
|
|
||||||
const navigateToParent = '/browse/'
|
|
||||||
+ objectPath.slice(1)
|
|
||||||
.map(parent => this.openmct.objects.makeKeyString(parent.identifier))
|
|
||||||
.join('/');
|
|
||||||
|
|
||||||
const filteredChild = {
|
|
||||||
id: this.openmct.objects.makeKeyString(object.identifier),
|
|
||||||
object,
|
|
||||||
objectPath,
|
|
||||||
navigateToParent
|
|
||||||
};
|
|
||||||
|
|
||||||
this.filteredTreeItems.push(filteredChild);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getAllChildren(navigateToParent) {
|
|
||||||
this.isLoading = true;
|
|
||||||
this.openmct.objects.get('ROOT')
|
|
||||||
.then(root => {
|
|
||||||
return this.openmct.composition.get(root).load();
|
|
||||||
})
|
|
||||||
.then(children => {
|
|
||||||
this.isLoading = false;
|
|
||||||
this.allTreeItems = children.map(c => {
|
|
||||||
return {
|
|
||||||
id: this.openmct.objects.makeKeyString(c.identifier),
|
|
||||||
object: c,
|
|
||||||
objectPath: [c],
|
|
||||||
navigateToParent: navigateToParent || '/browse'
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getFilteredChildren() {
|
|
||||||
// clear any previous search results
|
|
||||||
this.filteredTreeItems = [];
|
|
||||||
|
|
||||||
const promises = this.openmct.objects.search(this.searchValue)
|
|
||||||
.map(promise => promise
|
|
||||||
.then(results => this.aggregateFilteredChildren(results)));
|
|
||||||
|
|
||||||
Promise.all(promises).then(() => {
|
|
||||||
this.isLoading = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleItemSelection(item, node) {
|
|
||||||
if (item && (this.ignoreTypeCheck || item.type === 'conditionSet')) {
|
|
||||||
const parentId = (node.objectPath && node.objectPath.length > 1) ? node.objectPath[1].identifier : undefined;
|
|
||||||
this.selectedItem = {
|
|
||||||
itemId: item.identifier,
|
|
||||||
parentId
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$emit('treeItemSelected',
|
|
||||||
{
|
|
||||||
item,
|
|
||||||
parentObjectPath: node.objectPath
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
searchTree(value) {
|
|
||||||
this.searchValue = value;
|
|
||||||
this.isLoading = true;
|
|
||||||
|
|
||||||
if (this.searchValue !== '') {
|
|
||||||
this.getDebouncedFilteredChildren();
|
|
||||||
} else {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -1,206 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2022, 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.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<li class="c-tree__item-h">
|
|
||||||
<div
|
|
||||||
class="c-tree__item"
|
|
||||||
:class="{ 'is-alias': isAlias, 'is-navigated-object': navigated }"
|
|
||||||
@click="handleItemSelected(node.object, node)"
|
|
||||||
>
|
|
||||||
<view-control
|
|
||||||
v-model="expanded"
|
|
||||||
class="c-tree__item__view-control"
|
|
||||||
:enabled="hasChildren"
|
|
||||||
/>
|
|
||||||
<div class="c-tree__item__label c-object-label">
|
|
||||||
<div
|
|
||||||
class="c-tree__item__type-icon c-object-label__type-icon"
|
|
||||||
:class="typeClass"
|
|
||||||
></div>
|
|
||||||
<div class="c-tree__item__name c-object-label__name">{{ node.object.name }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
v-if="expanded && !isLoading"
|
|
||||||
class="c-tree"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-if="isLoading && !loaded"
|
|
||||||
class="c-tree__item-h"
|
|
||||||
>
|
|
||||||
<div class="c-tree__item loading">
|
|
||||||
<span class="c-tree__item__label">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<SelectorDialogTreeItem
|
|
||||||
v-for="child in children"
|
|
||||||
:key="child.id"
|
|
||||||
:node="child"
|
|
||||||
:selected-item="selectedItem"
|
|
||||||
:handle-item-selected="handleItemSelected"
|
|
||||||
:navigate-to-parent="navigateToParent"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import viewControl from '@/ui/components/viewControl.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'SelectorDialogTreeItem',
|
|
||||||
components: {
|
|
||||||
viewControl
|
|
||||||
},
|
|
||||||
inject: ['openmct'],
|
|
||||||
props: {
|
|
||||||
node: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
selectedItem: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleItemSelected: {
|
|
||||||
type: Function,
|
|
||||||
default() {
|
|
||||||
return (item) => {};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigateToParent: {
|
|
||||||
type: String,
|
|
||||||
default() {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hasChildren: false,
|
|
||||||
isLoading: false,
|
|
||||||
loaded: false,
|
|
||||||
children: [],
|
|
||||||
expanded: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
navigated() {
|
|
||||||
const itemId = this.selectedItem && this.selectedItem.itemId;
|
|
||||||
const isSelectedObject = itemId && this.openmct.objects.areIdsEqual(this.node.object.identifier, itemId);
|
|
||||||
if (isSelectedObject && this.node.objectPath && this.node.objectPath.length > 1) {
|
|
||||||
const isParent = this.openmct.objects.areIdsEqual(this.node.objectPath[1].identifier, this.selectedItem.parentId);
|
|
||||||
|
|
||||||
return isSelectedObject && isParent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isSelectedObject;
|
|
||||||
},
|
|
||||||
isAlias() {
|
|
||||||
let parent = this.node.objectPath[1];
|
|
||||||
if (!parent) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parentKeyString = this.openmct.objects.makeKeyString(parent.identifier);
|
|
||||||
|
|
||||||
return parentKeyString !== this.node.object.location;
|
|
||||||
},
|
|
||||||
typeClass() {
|
|
||||||
let type = this.openmct.types.get(this.node.object.type);
|
|
||||||
if (!type) {
|
|
||||||
return 'icon-object-unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
return type.definition.cssClass;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
expanded() {
|
|
||||||
if (!this.hasChildren) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.loaded && !this.isLoading) {
|
|
||||||
this.composition = this.openmct.composition.get(this.domainObject);
|
|
||||||
this.composition.on('add', this.addChild);
|
|
||||||
this.composition.on('remove', this.removeChild);
|
|
||||||
this.composition.load().then(this.finishLoading);
|
|
||||||
this.isLoading = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.domainObject = this.node.object;
|
|
||||||
|
|
||||||
if (this.navigateToParent && this.navigateToParent.includes(this.openmct.objects.makeKeyString(this.domainObject.identifier))) {
|
|
||||||
this.expanded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.navigateToParent && this.navigateToParent.endsWith(this.openmct.objects.makeKeyString(this.domainObject.identifier))) {
|
|
||||||
this.handleItemSelected(this.node.object, this.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => {
|
|
||||||
this.domainObject = newObject;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$once('hook:destroyed', removeListener);
|
|
||||||
if (this.openmct.composition.get(this.node.object)) {
|
|
||||||
this.hasChildren = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
this.expanded = false;
|
|
||||||
},
|
|
||||||
destroyed() {
|
|
||||||
if (this.composition) {
|
|
||||||
this.composition.off('add', this.addChild);
|
|
||||||
this.composition.off('remove', this.removeChild);
|
|
||||||
delete this.composition;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
addChild(child) {
|
|
||||||
this.children.push({
|
|
||||||
id: this.openmct.objects.makeKeyString(child.identifier),
|
|
||||||
object: child,
|
|
||||||
objectPath: [child].concat(this.node.objectPath),
|
|
||||||
navigateToParent: this.navigateToPath
|
|
||||||
});
|
|
||||||
},
|
|
||||||
removeChild(identifier) {
|
|
||||||
let removeId = this.openmct.objects.makeKeyString(identifier);
|
|
||||||
this.children = this.children
|
|
||||||
.filter(c => c.id !== removeId);
|
|
||||||
},
|
|
||||||
finishLoading() {
|
|
||||||
this.isLoading = false;
|
|
||||||
this.loaded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -280,38 +280,5 @@
|
|||||||
border: 1px solid $colorFormLines;
|
border: 1px solid $colorFormLines;
|
||||||
border-radius: $controlCr;
|
border-radius: $controlCr;
|
||||||
padding: $interiorMargin;
|
padding: $interiorMargin;
|
||||||
|
|
||||||
> .c-tree {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRANSITIONS
|
|
||||||
.children-enter-active {
|
|
||||||
&.down {
|
|
||||||
animation: animSlideLeft 500ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.up {
|
|
||||||
animation: animSlideRight 500ms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes animSlideLeft {
|
|
||||||
0% {opacity: 0; transform: translateX(100%);}
|
|
||||||
10% {opacity: 1;}
|
|
||||||
100% {transform: translateX(0);}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes animSlideRight {
|
|
||||||
0% {opacity: 0; transform: translateX(-100%);}
|
|
||||||
10% {opacity: 1;}
|
|
||||||
100% {transform: translateX(0);}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes animTemporaryHighlight {
|
|
||||||
from { background: transparent; }
|
|
||||||
30% { background: $colorItemTreeNewNode; }
|
|
||||||
100% { background: transparent; }
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="c-tree-and-search">
|
<div
|
||||||
|
ref="treeContainer"
|
||||||
|
class="c-tree-and-search"
|
||||||
|
:class="{
|
||||||
|
'c-selector': isSelectorTree
|
||||||
|
}"
|
||||||
|
:style="treeHeight"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref="search"
|
ref="search"
|
||||||
class="c-tree-and-search__search"
|
class="c-tree-and-search__search"
|
||||||
@ -72,6 +78,8 @@
|
|||||||
v-for="(treeItem, index) in visibleItems"
|
v-for="(treeItem, index) in visibleItems"
|
||||||
:key="treeItem.navigationPath"
|
:key="treeItem.navigationPath"
|
||||||
:node="treeItem"
|
:node="treeItem"
|
||||||
|
:is-selector-tree="isSelectorTree"
|
||||||
|
:selected-item="selectedItem"
|
||||||
:active-search="activeSearch"
|
:active-search="activeSearch"
|
||||||
:left-offset="!activeSearch ? treeItem.leftOffset : '0px'"
|
:left-offset="!activeSearch ? treeItem.leftOffset : '0px'"
|
||||||
:is-new="treeItem.isNew"
|
:is-new="treeItem.isNew"
|
||||||
@ -82,7 +90,8 @@
|
|||||||
:loading-items="treeItemLoading"
|
:loading-items="treeItemLoading"
|
||||||
@tree-item-mounted="scrollToCheck($event)"
|
@tree-item-mounted="scrollToCheck($event)"
|
||||||
@tree-item-destroyed="removeCompositionListenerFor($event)"
|
@tree-item-destroyed="removeCompositionListenerFor($event)"
|
||||||
@navigation-click="treeItemAction(treeItem, $event)"
|
@tree-item-action="treeItemAction(treeItem, $event)"
|
||||||
|
@tree-item-selection="treeItemSelection(treeItem)"
|
||||||
/>
|
/>
|
||||||
<!-- main loading -->
|
<!-- main loading -->
|
||||||
<div
|
<div
|
||||||
@ -115,6 +124,7 @@ const ITEM_BUFFER = 25;
|
|||||||
const LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded';
|
const LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded';
|
||||||
const SORT_MY_ITEMS_ALPH_ASC = true;
|
const SORT_MY_ITEMS_ALPH_ASC = true;
|
||||||
const TREE_ITEM_INDENT_PX = 18;
|
const TREE_ITEM_INDENT_PX = 18;
|
||||||
|
const LOCATOR_ITEM_COUNT_HEIGHT = 10; // how many tree items to make the locator selection box show
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MctTree',
|
name: 'MctTree',
|
||||||
@ -124,13 +134,27 @@ export default {
|
|||||||
},
|
},
|
||||||
inject: ['openmct'],
|
inject: ['openmct'],
|
||||||
props: {
|
props: {
|
||||||
|
isSelectorTree: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initialSelection: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
syncTreeNavigation: {
|
syncTreeNavigation: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
resetTreeNavigation: {
|
resetTreeNavigation: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -149,7 +173,8 @@ export default {
|
|||||||
itemHeight: 27,
|
itemHeight: 27,
|
||||||
itemOffset: 0,
|
itemOffset: 0,
|
||||||
activeSearch: false,
|
activeSearch: false,
|
||||||
mainTreeTopMargin: undefined
|
mainTreeTopMargin: undefined,
|
||||||
|
selectedItem: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -179,6 +204,13 @@ export default {
|
|||||||
},
|
},
|
||||||
showNoSearchResults() {
|
showNoSearchResults() {
|
||||||
return this.searchValue && this.searchResultItems.length === 0 && !this.searchLoading;
|
return this.searchValue && this.searchResultItems.length === 0 && !this.searchLoading;
|
||||||
|
},
|
||||||
|
treeHeight() {
|
||||||
|
if (!this.isSelectorTree) {
|
||||||
|
return {};
|
||||||
|
} else {
|
||||||
|
return { height: this.itemHeight * LOCATOR_ITEM_COUNT_HEIGHT + 'px' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -223,7 +255,14 @@ export default {
|
|||||||
await this.loadRoot();
|
await this.loadRoot();
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|
||||||
|
if (!this.isSelectorTree) {
|
||||||
await this.syncTreeOpenItems();
|
await this.syncTreeOpenItems();
|
||||||
|
} else {
|
||||||
|
const objectPath = await this.openmct.objects.getOriginalPath(this.initialSelection.identifier);
|
||||||
|
const navigationPath = this.buildNavigationPath(objectPath);
|
||||||
|
|
||||||
|
this.openAndScrollTo(navigationPath);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getSearchResults = _.debounce(this.getSearchResults, 400);
|
this.getSearchResults = _.debounce(this.getSearchResults, 400);
|
||||||
@ -265,6 +304,10 @@ export default {
|
|||||||
this.openTreeItem(parentItem);
|
this.openTreeItem(parentItem);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
treeItemSelection(item) {
|
||||||
|
this.selectedItem = item;
|
||||||
|
this.$emit('tree-item-selection', item);
|
||||||
|
},
|
||||||
async openTreeItem(parentItem) {
|
async openTreeItem(parentItem) {
|
||||||
let parentPath = parentItem.navigationPath;
|
let parentPath = parentItem.navigationPath;
|
||||||
|
|
||||||
@ -358,6 +401,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
openAndScrollTo(navigationPath) {
|
openAndScrollTo(navigationPath) {
|
||||||
|
if (navigationPath.includes('/ROOT')) {
|
||||||
|
navigationPath = navigationPath.split('/ROOT').join('');
|
||||||
|
}
|
||||||
|
|
||||||
let idArray = navigationPath.split('/');
|
let idArray = navigationPath.split('/');
|
||||||
let fullPathArray = [];
|
let fullPathArray = [];
|
||||||
let pathsToOpen;
|
let pathsToOpen;
|
||||||
@ -367,7 +414,6 @@ export default {
|
|||||||
// skip root
|
// skip root
|
||||||
idArray.splice(0, 2);
|
idArray.splice(0, 2);
|
||||||
idArray[0] = 'browse/' + idArray[0];
|
idArray[0] = 'browse/' + idArray[0];
|
||||||
|
|
||||||
idArray.reduce((parentPath, childPath) => {
|
idArray.reduce((parentPath, childPath) => {
|
||||||
let fullPath = [parentPath, childPath].join('/');
|
let fullPath = [parentPath, childPath].join('/');
|
||||||
|
|
||||||
@ -383,7 +429,11 @@ export default {
|
|||||||
|
|
||||||
return this.openTreeItem(this.getTreeItemByPath(childPath));
|
return this.openTreeItem(this.getTreeItemByPath(childPath));
|
||||||
|
|
||||||
}, Promise.resolve());
|
}, Promise.resolve()).then(() => {
|
||||||
|
if (this.isSelectorTree) {
|
||||||
|
this.treeItemSelection(this.getTreeItemByPath(navigationPath));
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
scrollToCheck(navigationPath) {
|
scrollToCheck(navigationPath) {
|
||||||
if (this.scrollToPath && this.scrollToPath === navigationPath) {
|
if (this.scrollToPath && this.scrollToPath === navigationPath) {
|
||||||
@ -721,18 +771,26 @@ export default {
|
|||||||
|
|
||||||
let checkHeights = () => {
|
let checkHeights = () => {
|
||||||
let treeTopMargin = this.getElementStyleValue(this.$refs.mainTree, 'marginTop');
|
let treeTopMargin = this.getElementStyleValue(this.$refs.mainTree, 'marginTop');
|
||||||
|
let paddingOffset = 0;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.$el
|
this.$el
|
||||||
&& this.$refs.search
|
&& this.$refs.search
|
||||||
&& this.$refs.mainTree
|
&& this.$refs.mainTree
|
||||||
|
&& this.$refs.treeContainer
|
||||||
&& this.$refs.dummyItem
|
&& this.$refs.dummyItem
|
||||||
&& this.$el.offsetHeight !== 0
|
&& this.$el.offsetHeight !== 0
|
||||||
&& treeTopMargin > 0
|
&& treeTopMargin > 0
|
||||||
) {
|
) {
|
||||||
|
if (this.isSelectorTree) {
|
||||||
|
paddingOffset = this.getElementStyleValue(this.$refs.treeContainer, 'padding');
|
||||||
|
}
|
||||||
|
|
||||||
this.mainTreeTopMargin = treeTopMargin;
|
this.mainTreeTopMargin = treeTopMargin;
|
||||||
this.mainTreeHeight = this.$el.offsetHeight
|
this.mainTreeHeight = this.$el.offsetHeight
|
||||||
- this.$refs.search.offsetHeight
|
- this.$refs.search.offsetHeight
|
||||||
- this.mainTreeTopMargin;
|
- this.mainTreeTopMargin
|
||||||
|
- (paddingOffset * 2);
|
||||||
this.itemHeight = this.getElementStyleValue(this.$refs.dummyItem, 'height');
|
this.itemHeight = this.getElementStyleValue(this.$refs.dummyItem, 'height');
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
@ -783,10 +841,18 @@ export default {
|
|||||||
return Number(styleString.slice(0, index));
|
return Number(styleString.slice(0, index));
|
||||||
},
|
},
|
||||||
getSavedOpenItems() {
|
getSavedOpenItems() {
|
||||||
|
if (this.isSelectorTree) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let openItems = localStorage.getItem(LOCAL_STORAGE_KEY__TREE_EXPANDED);
|
let openItems = localStorage.getItem(LOCAL_STORAGE_KEY__TREE_EXPANDED);
|
||||||
this.openTreeItems = openItems ? JSON.parse(openItems) : [];
|
this.openTreeItems = openItems ? JSON.parse(openItems) : [];
|
||||||
},
|
},
|
||||||
setSavedOpenItems() {
|
setSavedOpenItems() {
|
||||||
|
if (this.isSelectorTree) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem(LOCAL_STORAGE_KEY__TREE_EXPANDED, JSON.stringify(this.openTreeItems));
|
localStorage.setItem(LOCAL_STORAGE_KEY__TREE_EXPANDED, JSON.stringify(this.openTreeItems));
|
||||||
},
|
},
|
||||||
handleTreeResize() {
|
handleTreeResize() {
|
||||||
|
@ -7,19 +7,19 @@
|
|||||||
class="c-tree__item"
|
class="c-tree__item"
|
||||||
:class="{
|
:class="{
|
||||||
'is-alias': isAlias,
|
'is-alias': isAlias,
|
||||||
'is-navigated-object': navigated,
|
'is-navigated-object': shouldHightlight,
|
||||||
'is-context-clicked': contextClickActive,
|
'is-context-clicked': contextClickActive,
|
||||||
'is-new': isNewItem
|
'is-new': isNewItem
|
||||||
}"
|
}"
|
||||||
@click.capture="handleClick"
|
@click.capture="itemClick"
|
||||||
@contextmenu.capture="handleContextMenu"
|
@contextmenu.capture="handleContextMenu"
|
||||||
>
|
>
|
||||||
<view-control
|
<view-control
|
||||||
ref="navigate"
|
ref="action"
|
||||||
class="c-tree__item__view-control"
|
class="c-tree__item__view-control"
|
||||||
:value="isOpen || isLoading"
|
:value="isOpen || isLoading"
|
||||||
:enabled="!activeSearch && hasComposition"
|
:enabled="!activeSearch && hasComposition"
|
||||||
@input="navigationClick()"
|
@input="itemAction()"
|
||||||
/>
|
/>
|
||||||
<object-label
|
<object-label
|
||||||
ref="objectLabel"
|
ref="objectLabel"
|
||||||
@ -52,6 +52,14 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
isSelectorTree: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selectedItem: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
activeSearch: {
|
activeSearch: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
@ -109,6 +117,9 @@ export default {
|
|||||||
|
|
||||||
return parentKeyString !== this.node.object.location;
|
return parentKeyString !== this.node.object.location;
|
||||||
},
|
},
|
||||||
|
isSelectedItem() {
|
||||||
|
return this.selectedItem.objectPath === this.node.objectPath;
|
||||||
|
},
|
||||||
isNewItem() {
|
isNewItem() {
|
||||||
return this.isNew;
|
return this.isNew;
|
||||||
},
|
},
|
||||||
@ -118,6 +129,13 @@ export default {
|
|||||||
isOpen() {
|
isOpen() {
|
||||||
return this.openItems.includes(this.navigationPath);
|
return this.openItems.includes(this.navigationPath);
|
||||||
},
|
},
|
||||||
|
shouldHightlight() {
|
||||||
|
if (this.isSelectorTree) {
|
||||||
|
return this.isSelectedItem;
|
||||||
|
} else {
|
||||||
|
return this.navigated;
|
||||||
|
}
|
||||||
|
},
|
||||||
treeItemStyles() {
|
treeItemStyles() {
|
||||||
let itemTop = (this.itemOffset + this.itemIndex) * this.itemHeight + 'px';
|
let itemTop = (this.itemOffset + this.itemIndex) * this.itemHeight + 'px';
|
||||||
|
|
||||||
@ -144,20 +162,30 @@ export default {
|
|||||||
this.$emit('tree-item-destoyed', this.navigationPath);
|
this.$emit('tree-item-destoyed', this.navigationPath);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
navigationClick() {
|
itemAction() {
|
||||||
this.$emit('navigation-click', this.isOpen || this.isLoading ? 'close' : 'open');
|
this.$emit('tree-item-action', this.isOpen || this.isLoading ? 'close' : 'open');
|
||||||
},
|
},
|
||||||
handleClick(event) {
|
itemClick(event) {
|
||||||
// skip for navigation, let viewControl handle click
|
// skip for navigation, let viewControl handle click
|
||||||
if (this.$refs.navigate.$el === event.target) {
|
if (this.$refs.action.$el === event.target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (!this.isSelectorTree) {
|
||||||
this.$refs.objectLabel.navigateOrPreview(event);
|
this.$refs.objectLabel.navigateOrPreview(event);
|
||||||
|
} else {
|
||||||
|
this.$emit('tree-item-selection', this.node);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
handleContextMenu(event) {
|
handleContextMenu(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (this.isSelectorTree) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.$refs.objectLabel.showContextMenu(event);
|
this.$refs.objectLabel.showContextMenu(event);
|
||||||
},
|
},
|
||||||
isNavigated() {
|
isNavigated() {
|
||||||
|
Reference in New Issue
Block a user