Inconsistent behavior with multiple annotations in imagery (#7261)

* fix opacity issue

* wip, though selection still weird

* remove debugging

* plots still have issue with last tag

* add some better tests

* Apply suggestions from code review

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>

* remove hardlined classnames

* case sensitivity

* good job tests finding issue

---------

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
This commit is contained in:
Scott Bell 2023-12-05 04:12:24 +01:00 committed by GitHub
parent a3e0a0f694
commit 2d9c0414f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 159 additions and 34 deletions

View File

@ -247,6 +247,14 @@ test.describe('Example Imagery Object', () => {
await page.mouse.click(canvasCenterX - 50, canvasCenterY - 50); await page.mouse.click(canvasCenterX - 50, canvasCenterY - 50);
await expect(page.getByText('Driving')).toBeVisible(); await expect(page.getByText('Driving')).toBeVisible();
await expect(page.getByText('Science')).toBeVisible(); await expect(page.getByText('Science')).toBeVisible();
// add another tag and expect it to appear without changing selection
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Drilling').click();
await expect(page.getByText('Driving')).toBeVisible();
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Drilling')).toBeVisible();
}); });
test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => { test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => {

View File

@ -224,4 +224,22 @@ test.describe('Tagging in Notebooks @addInit', () => {
// Verify the AutoComplete field is hidden // Verify the AutoComplete field is hidden
await expect(page.locator('[placeholder="Type to select tag"]')).toBeHidden(); await expect(page.locator('[placeholder="Type to select tag"]')).toBeHidden();
}); });
test('Can start to add a tag, click away, and add a tag', async ({ page }) => {
await createNotebookEntryAndTags(page);
await page.getByRole('tab', { name: 'Annotations' }).click();
// Click on the body simulating a click outside the autocomplete)
await page.locator('body').click();
await page.locator(`[aria-label="Notebook Entry"]`).click();
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
// Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
// Select the "Driving" tag
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
await expect(page.getByLabel('Notebook Entries').getByText('Drilling')).toBeVisible();
});
}); });

View File

@ -165,6 +165,29 @@ test.describe('Plot Tagging', () => {
await expect(page.getByText('Science')).toBeVisible(); await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden(); await expect(page.getByText('Driving')).toBeHidden();
// click elsewhere
await page.locator('body').click();
//click on tagged plot point again
await canvas.click({
position: {
x: 100,
y: 100
}
});
// Add driving tag again
await page.getByText('Annotations').click();
await page.getByRole('button', { name: /Add Tag/ }).click();
await page.getByPlaceholder('Type to select tag').click();
await page.getByText('Driving').click();
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeVisible();
// Delete Driving again
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
await expect(page.getByText('Science')).toBeVisible();
await expect(page.getByText('Driving')).toBeHidden();
} }
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {

View File

@ -33,8 +33,11 @@
<script> <script>
import Flatbush from 'flatbush'; import Flatbush from 'flatbush';
import isEqual from 'lodash/isEqual';
import { toRaw } from 'vue'; import { toRaw } from 'vue';
import TagEditorClassNames from '../../inspectorViews/annotations/tags/TagEditorClassNames';
const EXISTING_ANNOTATION_STROKE_STYLE = '#D79078'; const EXISTING_ANNOTATION_STROKE_STYLE = '#D79078';
const EXISTING_ANNOTATION_FILL_STYLE = 'rgba(202, 202, 142, 0.2)'; const EXISTING_ANNOTATION_FILL_STYLE = 'rgba(202, 202, 142, 0.2)';
const SELECTED_ANNOTATION_STROKE_COLOR = '#BD8ECC'; const SELECTED_ANNOTATION_STROKE_COLOR = '#BD8ECC';
@ -118,9 +121,22 @@ export default {
document.body.removeEventListener('click', this.cancelSelection); document.body.removeEventListener('click', this.cancelSelection);
}, },
methods: { methods: {
onAnnotationChange(annotations) { onAnnotationChange(updatedAnnotations) {
this.selectedAnnotations = annotations; updatedAnnotations.forEach((updatedAnnotation) => {
this.$emit('annotations-changed', annotations); // Try to find the annotation in the existing selected annotations
const existingIndex = this.selectedAnnotations.findIndex((annotation) =>
this.openmct.objects.areIdsEqual(annotation.identifier, updatedAnnotation.identifier)
);
// If found, update it
if (existingIndex > -1) {
this.selectedAnnotations[existingIndex] = updatedAnnotation;
} else {
// If not found, add it
this.selectedAnnotations.push(updatedAnnotation);
}
});
this.$emit('annotations-changed', this.selectedAnnotations);
}, },
transformAnnotationRectangleToFlatbushRectangle(annotationRectangle) { transformAnnotationRectangleToFlatbushRectangle(annotationRectangle) {
let { x, y, width, height } = annotationRectangle; let { x, y, width, height } = annotationRectangle;
@ -164,7 +180,13 @@ export default {
const targetDetails = []; const targetDetails = [];
annotations.forEach((annotation) => { annotations.forEach((annotation) => {
annotation.targets.forEach((target) => { annotation.targets.forEach((target) => {
targetDetails.push(toRaw(target)); // only add targetDetails if we haven't added it before
const targetAlreadyAdded = targetDetails.some((targetDetail) => {
return isEqual(targetDetail, toRaw(target));
});
if (!targetAlreadyAdded) {
targetDetails.push(toRaw(target));
}
}); });
}); });
this.selectedAnnotations = annotations; this.selectedAnnotations = annotations;
@ -296,9 +318,13 @@ export default {
cancelSelection(event) { cancelSelection(event) {
if (this.$refs.canvas) { if (this.$refs.canvas) {
const clickedInsideCanvas = this.$refs.canvas.contains(event.target); const clickedInsideCanvas = this.$refs.canvas.contains(event.target);
// unfortunate side effect from possibly being detached from the DOM when
// adding/deleting tags, so closest() won't work
const clickedTagEditor = Object.values(TagEditorClassNames).some((className) => {
return event.target.classList.contains(className);
});
const clickedInsideInspector = event.target.closest('.js-inspector') !== null; const clickedInsideInspector = event.target.closest('.js-inspector') !== null;
const clickedOption = event.target.closest('.js-autocomplete-options') !== null; if (!clickedInsideCanvas && !clickedTagEditor && !clickedInsideInspector) {
if (!clickedInsideCanvas && !clickedInsideInspector && !clickedOption) {
this.newAnnotationRectangle = {}; this.newAnnotationRectangle = {};
this.selectedAnnotations = []; this.selectedAnnotations = [];
this.drawAnnotations(); this.drawAnnotations();
@ -345,12 +371,13 @@ export default {
const resultIndicies = this.annotationsIndex.search(x, y, x, y); const resultIndicies = this.annotationsIndex.search(x, y, x, y);
resultIndicies.forEach((resultIndex) => { resultIndicies.forEach((resultIndex) => {
const foundAnnotation = this.indexToAnnotationMap[resultIndex]; const foundAnnotation = this.indexToAnnotationMap[resultIndex];
if (foundAnnotation._deleted) {
return;
}
nearbyAnnotations.push(foundAnnotation); nearbyAnnotations.push(foundAnnotation);
}); });
//show annotations if some were found //if everything has been deleted, don't bother with the selection
const allAnnotationsDeleted = nearbyAnnotations.every((annotation) => annotation._deleted);
if (allAnnotationsDeleted) {
nearbyAnnotations = [];
}
const { targetDomainObjects, targetDetails } = const { targetDomainObjects, targetDetails } =
this.prepareExistingAnnotationSelection(nearbyAnnotations); this.prepareExistingAnnotationSelection(nearbyAnnotations);
this.selectImageAnnotations({ this.selectImageAnnotations({
@ -419,6 +446,7 @@ export default {
}, },
drawAnnotations() { drawAnnotations() {
this.clearCanvas(); this.clearCanvas();
let drawnRectangles = [];
this.imageryAnnotations.forEach((annotation) => { this.imageryAnnotations.forEach((annotation) => {
if (annotation._deleted) { if (annotation._deleted) {
return; return;
@ -426,19 +454,31 @@ export default {
const annotationRectangle = annotation.targets.find( const annotationRectangle = annotation.targets.find(
(target) => target.keyString === this.keyString (target) => target.keyString === this.keyString
)?.rectangle; )?.rectangle;
const rectangleForPixelDensity = this.transformRectangleToPixelDense(annotationRectangle);
if (this.isSelectedAnnotation(annotation)) { // Check if the rectangle has already been drawn
this.drawRectInCanvas( const hasBeenDrawn = drawnRectangles.some(
rectangleForPixelDensity, (drawnRect) =>
SELECTED_ANNOTATION_FILL_STYLE, drawnRect.x === annotationRectangle.x &&
SELECTED_ANNOTATION_STROKE_COLOR drawnRect.y === annotationRectangle.y &&
); drawnRect.width === annotationRectangle.width &&
} else { drawnRect.height === annotationRectangle.height
this.drawRectInCanvas( );
rectangleForPixelDensity, if (!hasBeenDrawn) {
EXISTING_ANNOTATION_FILL_STYLE, const rectangleForPixelDensity = this.transformRectangleToPixelDense(annotationRectangle);
EXISTING_ANNOTATION_STROKE_STYLE if (this.isSelectedAnnotation(annotation)) {
); this.drawRectInCanvas(
rectangleForPixelDensity,
SELECTED_ANNOTATION_FILL_STYLE,
SELECTED_ANNOTATION_STROKE_COLOR
);
} else {
this.drawRectInCanvas(
rectangleForPixelDensity,
EXISTING_ANNOTATION_FILL_STYLE,
EXISTING_ANNOTATION_STROKE_STYLE
);
}
drawnRectangles.push(annotationRectangle);
} }
}); });
} }

View File

@ -65,7 +65,7 @@ export default {
} }
return this.loadedAnnotations.filter((annotation) => { return this.loadedAnnotations.filter((annotation) => {
return !annotation.tags && !annotation._deleted; return !annotation.tags;
}); });
}, },
tagAnnotations() { tagAnnotations() {
@ -74,7 +74,7 @@ export default {
} }
return this.loadedAnnotations.filter((annotation) => { return this.loadedAnnotations.filter((annotation) => {
return !annotation.tags && !annotation._deleted; return !annotation.tags;
}); });
}, },
multiSelection() { multiSelection() {

View File

@ -35,10 +35,13 @@
<button <button
v-show="!userAddingTag && !maxTagsAdded" v-show="!userAddingTag && !maxTagsAdded"
class="c-tag-applier__add-btn c-icon-button c-icon-button--major icon-plus" class="c-tag-applier__add-btn c-icon-button c-icon-button--major icon-plus"
:class="TagEditorClassNames.ADD_TAG_BUTTON"
title="Add new tag" title="Add new tag"
@click="addTag" @click="addTag"
> >
<div class="c-icon-button__label c-tag-btn__label">Add Tag</div> <div class="c-icon-button__label c-tag-btn__label" :class="TagEditorClassNames.ADD_TAG_LABEL">
Add Tag
</div>
</button> </button>
</div> </div>
</template> </template>
@ -46,6 +49,7 @@
<script> <script>
import { toRaw } from 'vue'; import { toRaw } from 'vue';
import TagEditorClassNames from './TagEditorClassNames';
import TagSelection from './TagSelection.vue'; import TagSelection from './TagSelection.vue';
export default { export default {
@ -88,7 +92,8 @@ export default {
data() { data() {
return { return {
addedTags: [], addedTags: [],
userAddingTag: false userAddingTag: false,
TagEditorClassNames: TagEditorClassNames
}; };
}, },
computed: { computed: {

View File

@ -0,0 +1,9 @@
const TagEditorClassNames = Object.freeze({
REMOVE_TAG: 'js-remove-tag',
AUTOCOMPLETE_INPUT: 'js-autocomplete__input',
ADD_TAG_BUTTON: 'js-add-tag-button',
ADD_TAG_LABEL: 'js-add-tag-label',
TAG_OPTION: 'js-tag-option'
});
export default TagEditorClassNames;

View File

@ -29,7 +29,7 @@
:model="availableTagModel" :model="availableTagModel"
:place-holder-text="'Type to select tag'" :place-holder-text="'Type to select tag'"
class="c-tag-selection" class="c-tag-selection"
:item-css-class="'icon-circle'" :item-css-class="`icon-circle ${TagEditorClassNames.TAG_OPTION}`"
@on-change="tagSelected" @on-change="tagSelected"
/> />
</template> </template>
@ -42,6 +42,7 @@
<button <button
v-show="!readOnly" v-show="!readOnly"
class="c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle" class="c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle"
:class="TagEditorClassNames.REMOVE_TAG"
:style="{ textShadow: selectedBackgroundColor + ' 0 0 4px' }" :style="{ textShadow: selectedBackgroundColor + ' 0 0 4px' }"
:aria-label="`Remove tag ${selectedTagLabel}`" :aria-label="`Remove tag ${selectedTagLabel}`"
@click="removeTag" @click="removeTag"
@ -54,6 +55,7 @@
<script> <script>
import AutoCompleteField from '../../../../api/forms/components/controls/AutoCompleteField.vue'; import AutoCompleteField from '../../../../api/forms/components/controls/AutoCompleteField.vue';
import TagEditorClassNames from './TagEditorClassNames';
export default { export default {
components: { components: {
@ -88,7 +90,7 @@ export default {
}, },
emits: ['tag-removed', 'tag-added'], emits: ['tag-removed', 'tag-added'],
data() { data() {
return {}; return { TagEditorClassNames: TagEditorClassNames };
}, },
computed: { computed: {
availableTagModel() { availableTagModel() {
@ -137,7 +139,6 @@ export default {
} }
} }
}, },
mounted() {},
methods: { methods: {
getAvailableTagByID(tagID) { getAvailableTagByID(tagID) {
return this.openmct.annotation.getAvailableTags().find((tag) => { return this.openmct.annotation.getAvailableTags().find((tag) => {

View File

@ -139,6 +139,7 @@
@editing-entry="startTransaction" @editing-entry="startTransaction"
@delete-entry="deleteEntry" @delete-entry="deleteEntry"
@update-entry="updateEntry" @update-entry="updateEntry"
@update-annotations="loadAnnotations"
@entry-selection="entrySelection(entry)" @entry-selection="entrySelection(entry)"
/> />
</div> </div>
@ -298,6 +299,12 @@ export default {
}, },
showTime() { showTime() {
mutateObject(this.openmct, this.domainObject, 'configuration.showTime', this.showTime); mutateObject(this.openmct, this.domainObject, 'configuration.showTime', this.showTime);
},
notebookAnnotations: {
handler() {
this.filterAndSortEntries();
},
deep: true
} }
}, },
beforeMount() { beforeMount() {

View File

@ -274,7 +274,8 @@ export default {
'change-section-page', 'change-section-page',
'update-entry', 'update-entry',
'editing-entry', 'editing-entry',
'entry-selection' 'entry-selection',
'update-annotations'
], ],
data() { data() {
return { return {
@ -638,13 +639,16 @@ export default {
this.entry.text = restoredQuoteBrackets; this.entry.text = restoredQuoteBrackets;
this.timestampAndUpdate(); this.timestampAndUpdate();
}, },
updateAnnotations(newAnnotations) {
this.$emit('update-annotations', newAnnotations);
},
selectAndEmitEntry(event, entry) { selectAndEmitEntry(event, entry) {
selectEntry({ selectEntry({
element: this.$refs.entry, element: this.$refs.entry,
entryId: entry.id, entryId: entry.id,
domainObject: this.domainObject, domainObject: this.domainObject,
openmct: this.openmct, openmct: this.openmct,
onAnnotationChange: this.timestampAndUpdate, onAnnotationChange: this.updateAnnotations,
notebookAnnotations: this.notebookAnnotations notebookAnnotations: this.notebookAnnotations
}); });
event.stopPropagation(); event.stopPropagation();

View File

@ -178,7 +178,9 @@
import Flatbush from 'flatbush'; import Flatbush from 'flatbush';
import _ from 'lodash'; import _ from 'lodash';
import { useEventBus } from 'utils/useEventBus'; import { useEventBus } from 'utils/useEventBus';
import { toRaw } from 'vue';
import TagEditorClassNames from '../inspectorViews/annotations/tags/TagEditorClassNames';
import XAxis from './axis/XAxis.vue'; import XAxis from './axis/XAxis.vue';
import YAxis from './axis/YAxis.vue'; import YAxis from './axis/YAxis.vue';
import MctChart from './chart/MctChart.vue'; import MctChart from './chart/MctChart.vue';
@ -465,9 +467,14 @@ export default {
cancelSelection(event) { cancelSelection(event) {
if (this.$refs?.plot) { if (this.$refs?.plot) {
const clickedInsidePlot = this.$refs.plot.contains(event.target); const clickedInsidePlot = this.$refs.plot.contains(event.target);
// unfortunate side effect from possibly being detached from the DOM when
// adding/deleting tags, so closest() won't work
const clickedTagEditor = Object.values(TagEditorClassNames).some((className) => {
return event.target.classList.contains(className);
});
const clickedInsideInspector = event.target.closest('.js-inspector') !== null; const clickedInsideInspector = event.target.closest('.js-inspector') !== null;
const clickedOption = event.target.closest('.js-autocomplete-options') !== null; const clickedOption = event.target.closest('.js-autocomplete-options') !== null;
if (!clickedInsidePlot && !clickedInsideInspector && !clickedOption) { if (!clickedInsidePlot && !clickedInsideInspector && !clickedOption && !clickedTagEditor) {
this.rectangles = []; this.rectangles = [];
this.annotationSelectionsBySeries = {}; this.annotationSelectionsBySeries = {};
this.selectPlot(); this.selectPlot();
@ -937,7 +944,10 @@ export default {
const targetDetails = []; const targetDetails = [];
const uniqueBoundsAnnotations = []; const uniqueBoundsAnnotations = [];
annotations.forEach((annotation) => { annotations.forEach((annotation) => {
targetDetails.push(annotation.targets); // for each target, push toRaw
annotation.targets.forEach((target) => {
targetDetails.push(toRaw(target));
});
const boundingBoxAlreadyAdded = uniqueBoundsAnnotations.some((existingAnnotation) => { const boundingBoxAlreadyAdded = uniqueBoundsAnnotations.some((existingAnnotation) => {
const existingBoundingBox = Object.values(existingAnnotation.targets)[0]; const existingBoundingBox = Object.values(existingAnnotation.targets)[0];