Files
openmct/src/plugins/notebook/components/NotebookEntry.vue
Scott Bell ce463babff 5734 synchronization for new tags on notebook entries (#5763)
* trying this again

* wip

* wip

* wip

* one annotation per tag

* fixed too many events firing

* syncing works mostly

* syncing properly across existing annotations

* search with multiple tags

* resolve conflicts between different tag editors

* resolve conflicts

* fix annotation tests

* combine search results

* modify tests

* prevent infinite loop creating annotation

* add modified and deleted

* revert index checkin

* change to standard couch deleted flag

* revert throwing of error

* resolve conflict issues

* work in progress, but load annotations once from notebook

* works to add

* attempt 1

* wip

* last changes

* listening works, though still getting conflicts

* rename to annotationLastCreated

* use local mutable again

* works with new tags syncing

* listeners wont fire if modification is null

* clean up code

* fixed local search

* cleaned up log messages

* remove on more log

* add e2e test for network traffic

* lint

* change to use good old for each

* add some local variables for clarity

* Update src/api/objects/ObjectAPI.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* Update src/api/objects/ObjectAPI.js

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* Update src/plugins/notebook/components/Notebook.vue

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>

* press enter for last entry

* add test explanation of numbers

* fix spread typo

* add some nice jsdoc

* throw some errors

* use really small integer instead

* remove unneeded binding

* make method public and jsdoc it

* use mutables

* clean up tests

* clean up tests

* use aria labels for tests

* add some proper tsdoc to annotation api

* add undelete test

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-09-30 10:32:11 -07:00

364 lines
11 KiB
Vue

/*****************************************************************************
* 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="c-notebook__entry c-ne has-local-controls has-tag-applier"
aria-label="Notebook Entry"
:class="{ 'locked': isLocked }"
@dragover="changeCursor"
@drop.capture="cancelEditMode"
@drop.prevent="dropOnEntry"
>
<div class="c-ne__time-and-content">
<div class="c-ne__time-and-creator">
<span class="c-ne__created-date">{{ createdOnDate }}</span>
<span class="c-ne__created-time">{{ createdOnTime }}</span>
<span
v-if="entry.createdBy"
class="c-ne__creator"
>
<span class="icon-person"></span> {{ entry.createdBy }}
</span>
</div>
<div class="c-ne__content">
<template v-if="readOnly && result">
<div
:id="entry.id"
class="c-ne__text highlight"
tabindex="0"
>
<TextHighlight
:text="entryText"
:highlight="highlightText"
:highlight-class="'search-highlight'"
/>
</div>
</template>
<template v-else-if="!isLocked">
<div
:id="entry.id"
class="c-ne__text c-ne__input"
aria-label="Notebook Entry Input"
tabindex="0"
contenteditable="true"
@focus="editingEntry()"
@blur="updateEntryValue($event)"
@keydown.enter.exact.prevent
@keyup.enter.exact.prevent="forceBlur($event)"
v-text="entry.text"
>
</div>
</template>
<template v-else>
<div
:id="entry.id"
class="c-ne__text"
contenteditable="false"
tabindex="0"
v-text="entry.text"
>
</div>
</template>
<TagEditor
:domain-object="domainObject"
:annotations="notebookAnnotations"
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
:target-specific-details="{entryId: entry.id}"
@tags-updated="timestampAndUpdate"
/>
<div class="c-snapshots c-ne__embeds">
<NotebookEmbed
v-for="embed in entry.embeds"
:key="embed.id"
:embed="embed"
:is-locked="isLocked"
@removeEmbed="removeEmbed"
@updateEmbed="updateEmbed"
/>
</div>
</div>
</div>
<div
v-if="!readOnly && !isLocked"
class="c-ne__local-controls--hidden"
>
<button
class="c-icon-button c-icon-button--major icon-trash"
title="Delete this entry"
tabindex="-1"
@click="deleteEntry"
>
</button>
</div>
<div
v-if="readOnly"
class="c-ne__section-and-page"
>
<a
class="c-click-link"
:class="{ 'search-highlight': result.metadata.sectionHit }"
@click="navigateToSection()"
>
{{ result.section.name }}
</a>
<span class="icon-arrow-right"></span>
<a
class="c-click-link"
:class="{ 'search-highlight': result.metadata.pageHit }"
@click="navigateToPage()"
>
{{ result.page.name }}
</a>
</div>
</div>
</template>
<script>
import NotebookEmbed from './NotebookEmbed.vue';
import TagEditor from '../../../ui/components/tags/TagEditor.vue';
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
import { createNewEmbed } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import Moment from 'moment';
const UNKNOWN_USER = 'Unknown';
export default {
components: {
NotebookEmbed,
TextHighlight,
TagEditor
},
inject: ['openmct', 'snapshotContainer'],
props: {
domainObject: {
type: Object,
default() {
return {};
}
},
notebookAnnotations: {
type: Array,
default() {
return [];
}
},
entry: {
type: Object,
default() {
return {};
}
},
result: {
type: Object,
default() {
return {};
}
},
selectedPage: {
type: Object,
default() {
return {};
}
},
selectedSection: {
type: Object,
default() {
return {};
}
},
readOnly: {
type: Boolean,
default() {
return true;
}
},
isLocked: {
type: Boolean,
default() {
return false;
}
}
},
computed: {
createdOnDate() {
return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');
},
createdOnTime() {
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
},
entryText() {
let text = this.entry.text;
if (!this.result.metadata.entryHit) {
text = `[ no result for '${this.result.metadata.originalSearchText}' in entry ]`;
}
return text;
},
highlightText() {
let text = '';
if (this.result.metadata.entryHit) {
text = this.result.metadata.originalSearchText;
}
return text;
}
},
mounted() {
this.dropOnEntry = this.dropOnEntry.bind(this);
},
methods: {
async addNewEmbed(objectPath) {
const bounds = this.openmct.time.bounds();
const snapshotMeta = {
bounds,
link: null,
objectPath,
openmct: this.openmct
};
const newEmbed = await createNewEmbed(snapshotMeta);
this.entry.embeds.push(newEmbed);
},
cancelEditMode(event) {
const isEditing = this.openmct.editor.isEditing();
if (isEditing) {
this.openmct.editor.cancel();
}
},
changeCursor(event) {
event.preventDefault();
if (!this.isLocked) {
event.dataTransfer.dropEffect = 'copy';
} else {
event.dataTransfer.dropEffect = 'none';
event.dataTransfer.effectAllowed = 'none';
}
},
deleteEntry() {
this.$emit('deleteEntry', this.entry.id);
},
async dropOnEntry($event) {
$event.stopImmediatePropagation();
const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id');
if (snapshotId.length) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.entry.embeds.push(snapshot.embedObject);
this.snapshotContainer.removeSnapshot(snapshotId);
const namespace = this.domainObject.identifier.namespace;
const notebookImageDomainObject = updateNamespaceOfDomainObject(snapshot.notebookImageDomainObject, namespace);
saveNotebookImageDomainObject(this.openmct, notebookImageDomainObject);
} else {
const data = $event.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);
await this.addNewEmbed(objectPath);
}
this.timestampAndUpdate();
},
findPositionInArray(array, id) {
let position = -1;
array.some((item, index) => {
const found = item.id === id;
if (found) {
position = index;
}
return found;
});
return position;
},
forceBlur(event) {
event.target.blur();
},
formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat);
},
navigateToPage() {
this.$emit('changeSectionPage', {
sectionId: this.result.section.id,
pageId: this.result.page.id
});
},
navigateToSection() {
this.$emit('changeSectionPage', {
sectionId: this.result.section.id,
pageId: null
});
},
removeEmbed(id) {
const embedPosition = this.findPositionInArray(this.entry.embeds, id);
// TODO: remove notebook snapshot object using object remove API
this.entry.embeds.splice(embedPosition, 1);
this.timestampAndUpdate();
},
updateEmbed(newEmbed) {
this.entry.embeds.some(e => {
const found = (e.id === newEmbed.id);
if (found) {
e = newEmbed;
}
return found;
});
this.timestampAndUpdate();
},
async timestampAndUpdate() {
const user = await this.openmct.user.getCurrentUser();
if (user === undefined) {
this.entry.modifiedBy = UNKNOWN_USER;
}
this.entry.modified = Date.now();
this.$emit('updateEntry', this.entry);
},
editingEntry() {
this.$emit('editingEntry');
},
updateEntryValue($event) {
const value = $event.target.innerText;
if (value !== this.entry.text && value.match(/\S/)) {
this.entry.text = value;
this.timestampAndUpdate();
} else {
this.$emit('cancelEdit');
}
}
}
};
</script>