Merge branch 'master' of https://github.com/nasa/openmct into activity-states-and-compact-view

This commit is contained in:
Shefali 2024-01-18 09:23:16 -08:00
commit 50de0f6f27
19 changed files with 249 additions and 197 deletions

View File

@ -6,15 +6,15 @@ This is the OpenMCT common webpack file. It is imported by the other three webpa
There are separate npm scripts to use these configurations, though simply running `npm install` There are separate npm scripts to use these configurations, though simply running `npm install`
will use the default production configuration. will use the default production configuration.
*/ */
import path from 'node:path';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import webpack from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import fs from 'node:fs';
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { VueLoaderPlugin } from 'vue-loader'; import { VueLoaderPlugin } from 'vue-loader';
import webpack from 'webpack';
let gitRevision = 'error-retrieving-revision'; let gitRevision = 'error-retrieving-revision';
let gitBranch = 'error-retrieving-branch'; let gitBranch = 'error-retrieving-branch';
@ -22,9 +22,7 @@ const packageDefinition = JSON.parse(fs.readFileSync(new URL('../package.json',
try { try {
gitRevision = execSync('git rev-parse HEAD').toString().trim(); gitRevision = execSync('git rev-parse HEAD').toString().trim();
gitBranch = execSync('git rev-parse --abbrev-ref HEAD') gitBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
.toString()
.trim();
} catch (err) { } catch (err) {
console.warn(err); console.warn(err);
} }
@ -67,7 +65,6 @@ const config = {
alias: { alias: {
'@': path.join(projectRootDir, 'src'), '@': path.join(projectRootDir, 'src'),
legacyRegistry: path.join(projectRootDir, 'src/legacyRegistry'), legacyRegistry: path.join(projectRootDir, 'src/legacyRegistry'),
saveAs: 'file-saver/src/FileSaver.js',
csv: 'comma-separated-values', csv: 'comma-separated-values',
EventEmitter: 'eventemitter3', EventEmitter: 'eventemitter3',
bourbon: 'bourbon.scss', bourbon: 'bourbon.scss',

View File

@ -43,7 +43,7 @@ test.describe('Clear Data Action', () => {
await expect(page.locator(backgroundImageSelector)).toBeVisible(); await expect(page.locator(backgroundImageSelector)).toBeVisible();
}); });
test('works as expected with Example Imagery', async ({ page }) => { test('works as expected with Example Imagery', async ({ page }) => {
await expect(await page.locator('.c-thumb__image').count()).toBeGreaterThan(0); expect(await page.locator('.c-thumb__image').count()).toBeGreaterThan(0);
// Click the "Clear Data" menu action // Click the "Clear Data" menu action
await page.getByTitle('More actions').click(); await page.getByTitle('More actions').click();
await expect( await expect(
@ -59,6 +59,6 @@ test.describe('Clear Data Action', () => {
// Verify that the background image is no longer visible // Verify that the background image is no longer visible
await expect(page.locator(backgroundImageSelector)).toBeHidden(); await expect(page.locator(backgroundImageSelector)).toBeHidden();
await expect(await page.locator('.c-thumb__image').count()).toBe(0); expect(await page.locator('.c-thumb__image').count()).toBe(0);
}); });
}); });

View File

@ -95,18 +95,15 @@ async function makeOverlayPlot(page, myItemsFolderName) {
await page.locator('button.c-create-button').click(); await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click(); await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
// Click OK button and wait for Navigate event
await Promise.all([ await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle' }), page.waitForLoadState(),
page.locator('button:has-text("OK")').click(), await page.getByRole('button', { name: 'Save' }).click(),
// Wait for Save Banner to appear // Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
// save the overlay plot // save the overlay plot
await saveOverlayPlot(page); await saveOverlayPlot(page);
// create a sinewave generator // create a sinewave generator
@ -114,34 +111,24 @@ async function makeOverlayPlot(page, myItemsFolderName) {
await page.locator('button.c-create-button').click(); await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
// set amplitude to 6, offset 4, period 2 // set amplitude to 6, offset 4, data rate 2 hz
await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').click(); await page.getByLabel('Amplitude').fill('6');
await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').fill('6'); await page.getByLabel('Offset').fill('4');
await page.getByLabel('Data Rate (hz)').fill('2');
await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').click();
await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').fill('4');
await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').click();
await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').fill('2');
// Click OK to make generator
// Click OK button and wait for Navigate event
await Promise.all([ await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle' }), page.waitForLoadState(),
page.locator('button:has-text("OK")').click(), await page.getByRole('button', { name: 'Save' }).click(),
// Wait for Save Banner to appear // Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
// click on overlay plot // click on overlay plot
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForLoadState(),
page.locator('text=Unnamed Overlay Plot').first().click() page.locator('text=Unnamed Overlay Plot').first().click()
]); ]);
} }

View File

@ -21,24 +21,24 @@
*****************************************************************************/ *****************************************************************************/
const DEFAULT_IMAGE_SAMPLES = [ const DEFAULT_IMAGE_SAMPLES = [
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18732.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18733.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18734.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18734.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18735.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18735.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18736.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18736.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18737.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18737.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18738.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18738.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18739.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18739.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18740.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18740.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18741.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18741.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18742.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18742.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18743.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18743.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18744.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18744.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18745.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18745.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18746.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18746.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg', 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18747.jpg',
'https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg' 'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18748.jpg'
]; ];
const DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS = 20000; const DEFAULT_IMAGE_LOAD_DELAY_IN_MILLISECONDS = 20000;
const MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS = 5000; const MIN_IMAGE_LOAD_DELAY_IN_MILLISECONDS = 5000;

View File

@ -78,9 +78,7 @@ class MenuAPI {
if (isActionGroup) { if (isActionGroup) {
action = this.actionsToMenuItems(action, objectPath, view); action = this.actionsToMenuItems(action, objectPath, view);
} else { } else {
action.onItemClicked = () => { action.onItemClicked = () => action.invoke(objectPath, view);
action.invoke(objectPath, view);
};
} }
return action; return action;

View File

@ -32,7 +32,7 @@ class Overlay extends EventEmitter {
const { destroy } = mount( const { destroy } = mount(
{ {
components: { components: {
OverlayComponent: OverlayComponent OverlayComponent
}, },
provide: { provide: {
dismiss: this.notifyAndDismiss.bind(this), dismiss: this.notifyAndDismiss.bind(this),
@ -60,7 +60,6 @@ class Overlay extends EventEmitter {
dismiss() { dismiss() {
this.emit('destroy'); this.emit('destroy');
document.body.removeChild(this.container);
this.destroy(); this.destroy();
} }

View File

@ -82,14 +82,17 @@ class OverlayAPI {
} }
/** /**
* A description of option properties that can be passed into the overlay * Creates and displays an overlay with the specified options.
* @typedef options *
* @property {object} element DOMElement that is to be inserted/shown on the overlay * @typedef {Object} OverlayOptions
* @property {string} size preferred size of the overlay (large, small, fit) * @property {HTMLElement} element The DOM Element to be inserted or shown in the overlay.
* @property {array} buttons optional button objects with label and callback properties * @property {'large'|'small'|'fit'} size The preferred size of the overlay.
* @property {function} onDestroy callback to be called when overlay is destroyed * @property {Array<{label: string, callback: Function}>} [buttons] Optional array of button objects, each with 'label' and 'callback' properties.
* @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away * @property {Function} onDestroy Callback to be called when the overlay is destroyed.
* from overlay. Unless set to false, all overlays will be dismissable by default. * @property {boolean} [dismissable=true] Whether the overlay can be dismissed by pressing 'esc' or clicking outside of it. Defaults to true.
*
* @param {OverlayOptions} options - The configuration options for the overlay.
* @returns {Overlay} An instance of the Overlay class.
*/ */
overlay(options) { overlay(options) {
let overlay = new Overlay(options); let overlay = new Overlay(options);

View File

@ -17,7 +17,7 @@ class ProgressDialog extends Overlay {
}) { }) {
const { vNode, destroy } = mount({ const { vNode, destroy } = mount({
components: { components: {
ProgressDialogComponent: ProgressDialogComponent ProgressDialogComponent
}, },
provide: { provide: {
iconClass, iconClass,
@ -28,16 +28,15 @@ class ProgressDialog extends Overlay {
}, },
data() { data() {
return { return {
model: { progressPerc,
progressPerc: progressPerc || 0,
progressText progressText
}
}; };
}, },
template: '<progress-dialog-component :model="model"></progress-dialog-component>' template:
'<progress-dialog-component :progress-perc="progressPerc" :progress-text="progressText"></progress-dialog-component>'
}); });
component = vNode.componentInstance;
component = vNode.componentInstance;
super({ super({
element: vNode.el, element: vNode.el,
size: 'fit', size: 'fit',
@ -51,8 +50,8 @@ class ProgressDialog extends Overlay {
} }
updateProgress(progressPerc, progressText) { updateProgress(progressPerc, progressText) {
component.model.progressPerc = progressPerc; component.$data.progressPerc = progressPerc;
component.model.progressText = progressText; component.$data.progressText = progressText;
} }
} }

View File

@ -20,9 +20,9 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<dialog-component> <DialogComponent>
<progress-component :model="model" /> <ProgressComponent :progress-perc="progressPerc" :progress-text="progressText" />
</dialog-component> </DialogComponent>
</template> </template>
<script> <script>
@ -31,14 +31,18 @@ import DialogComponent from './DialogComponent.vue';
export default { export default {
components: { components: {
DialogComponent: DialogComponent, DialogComponent,
ProgressComponent: ProgressComponent ProgressComponent
}, },
inject: ['iconClass', 'title', 'hint', 'timestamp', 'message'], inject: ['iconClass', 'title', 'hint', 'timestamp', 'message'],
props: { props: {
model: { progressPerc: {
type: Object, type: Number,
required: true default: 0
},
progressText: {
type: String,
default: ''
} }
} }
}; };

View File

@ -21,7 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import CSV from 'comma-separated-values'; import CSV from 'comma-separated-values';
import { saveAs } from 'saveAs'; import { saveAs } from 'file-saver';
class CSVExporter { class CSVExporter {
export(rows, options) { export(rows, options) {

View File

@ -31,8 +31,8 @@ function replaceDotsWithUnderscores(filename) {
return filename.replace(regex, '_'); return filename.replace(regex, '_');
} }
import { saveAs } from 'file-saver';
import html2canvas from 'html2canvas'; import html2canvas from 'html2canvas';
import { saveAs } from 'saveAs';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
class ImageExporter { class ImageExporter {

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { saveAs } from 'saveAs'; import { saveAs } from 'file-saver';
class JSONExporter { class JSONExporter {
export(obj, options) { export(obj, options) {

View File

@ -24,8 +24,19 @@ import { v4 as uuid } from 'uuid';
import JSONExporter from '/src/exporters/JSONExporter.js'; import JSONExporter from '/src/exporters/JSONExporter.js';
export default class ExportAsJSONAction { export default class ExportAsJSONAction {
#openmct;
/**
* @param {import('../../../openmct').OpenMCT} openmct The Open MCT API
*/
constructor(openmct) { constructor(openmct) {
this.openmct = openmct; this.#openmct = openmct;
// Bind public methods
this.invoke = this.invoke.bind(this);
this.appliesTo = this.appliesTo.bind(this);
// FIXME: This should be private but is used in tests
this.saveAs = this.saveAs.bind(this);
this.name = 'Export as JSON'; this.name = 'Export as JSON';
this.key = 'export.JSON'; this.key = 'export.JSON';
@ -37,6 +48,10 @@ export default class ExportAsJSONAction {
this.tree = null; this.tree = null;
this.calls = null; this.calls = null;
this.idMap = null; this.idMap = null;
this.dialog = null;
this.progressPerc = 0;
this.exportedCount = 0;
this.totalToExport = 0;
this.JSONExportService = new JSONExporter(); this.JSONExportService = new JSONExporter();
} }
@ -50,87 +65,127 @@ export default class ExportAsJSONAction {
appliesTo(objectPath) { appliesTo(objectPath) {
let domainObject = objectPath[0]; let domainObject = objectPath[0];
return this._isCreatableAndPersistable(domainObject); return this.#isCreatableAndPersistable(domainObject);
} }
/** /**
* *
* @param {object} objectpath * @param {import('../../api/objects/ObjectAPI').DomainObject[]} objectPath
*/ */
invoke(objectpath) { invoke(objectPath) {
this.tree = {}; this.tree = {};
this.calls = 0; this.calls = 0;
this.idMap = {}; this.idMap = {};
const root = objectpath[0]; const root = objectPath[0];
this.root = this._copy(root); this.root = this.#copy(root);
const rootId = this._getKeystring(this.root); const rootId = this.#getKeystring(this.root);
this.tree[rootId] = this.root; this.tree[rootId] = this.root;
this._write(this.root); this.dialog = this.#openmct.overlays.progressDialog({
message:
'Do not navigate away from this page or close this browser tab while this message is displayed.',
iconClass: 'info',
title: 'Exporting'
});
this.dialog.show();
this.#write(this.root)
.then(() => {
this.exportedCount++;
this.#updateProgress();
})
.catch((error) => {
this.dialog.dismiss();
this.dialog = null;
this.#resetCounts();
this.#openmct.notifications.error({
title: 'Export as JSON failed',
message: error.message
});
});
} }
/** /**
* @private * @private
* @param {object} parent * @param {import('../../api/objects/ObjectAPI').DomainObject} parent
*/ */
async _write(parent) { async #write(parent) {
this.totalToExport++;
this.calls++; this.calls++;
//conditional object styles are not saved on the composition, so we need to check for them //conditional object styles are not saved on the composition, so we need to check for them
const conditionSetIdentifier = this._getConditionSetIdentifier(parent); const conditionSetIdentifier = this.#getConditionSetIdentifier(parent);
const hasItemConditionSetIdentifiers = this._hasItemConditionSetIdentifiers(parent); const hasItemConditionSetIdentifiers = this.#hasItemConditionSetIdentifiers(parent);
const composition = this.openmct.composition.get(parent); const composition = this.#openmct.composition.get(parent);
if (composition) { if (composition) {
const children = await composition.load(); const children = await composition.load();
const exportPromises = children.map((child) => this.#exportObject(child, parent));
children.forEach((child) => { await Promise.all(exportPromises);
this._exportObject(child, parent);
});
} }
if (!conditionSetIdentifier && !hasItemConditionSetIdentifiers) { if (!conditionSetIdentifier && !hasItemConditionSetIdentifiers) {
this._decrementCallsAndSave(); this.#decrementCallsAndSave();
} else { } else {
const conditionSetObjects = []; let conditionSetObjects = [];
// conditionSetIdentifiers directly in objectStyles object // conditionSetIdentifiers directly in objectStyles object
if (conditionSetIdentifier) { if (conditionSetIdentifier) {
conditionSetObjects.push(await this.openmct.objects.get(conditionSetIdentifier)); const conditionSetObject = this.#openmct.objects.get(conditionSetIdentifier);
conditionSetObjects.push(conditionSetObject);
} }
// conditionSetIdentifiers stored on item ids in the objectStyles object // conditionSetIdentifiers stored on item ids in the objectStyles object
if (hasItemConditionSetIdentifiers) { if (hasItemConditionSetIdentifiers) {
const itemConditionSetIdentifiers = this._getItemConditionSetIdentifiers(parent); const itemConditionSetIdentifiers = this.#getItemConditionSetIdentifiers(parent);
const itemConditionSetObjects = itemConditionSetIdentifiers.map((id) =>
this.#openmct.objects.get(id)
);
conditionSetObjects = conditionSetObjects.concat(itemConditionSetObjects);
for (const itemConditionSetIdentifier of itemConditionSetIdentifiers) { for (const itemConditionSetIdentifier of itemConditionSetIdentifiers) {
conditionSetObjects.push(await this.openmct.objects.get(itemConditionSetIdentifier)); conditionSetObjects.push(this.#openmct.objects.get(itemConditionSetIdentifier));
} }
} }
for (const conditionSetObject of conditionSetObjects) { if (conditionSetObjects.length > 0) {
this._exportObject(conditionSetObject, parent); const resolvedConditionSetObjects = await Promise.all(conditionSetObjects);
const exportConditionSetPromises = resolvedConditionSetObjects.map((obj) =>
this.#exportObject(obj, parent)
);
await Promise.all(exportConditionSetPromises);
} }
this._decrementCallsAndSave(); this.#decrementCallsAndSave();
} }
} }
_exportObject(child, parent) { #updateProgress() {
const originalKeyString = this._getKeystring(child); this.progressPerc = Math.ceil((100 * this.exportedCount) / this.totalToExport);
const createable = this._isCreatableAndPersistable(child); this.dialog?.updateProgress(
this.progressPerc,
`Exporting ${this.exportedCount} / ${this.totalToExport} objects.`
);
}
#exportObject(child, parent) {
const originalKeyString = this.#getKeystring(child);
const createable = this.#isCreatableAndPersistable(child);
const isNotInfinite = !Object.prototype.hasOwnProperty.call(this.tree, originalKeyString); const isNotInfinite = !Object.prototype.hasOwnProperty.call(this.tree, originalKeyString);
if (createable && isNotInfinite) { if (createable && isNotInfinite) {
// for external or linked objects we generate new keys, if they don't exist already // for external or linked objects we generate new keys, if they don't exist already
if (this._isLinkedObject(child, parent)) { if (this.#isLinkedObject(child, parent)) {
child = this._rewriteLink(child, parent); child = this.#rewriteLink(child, parent);
} else { } else {
this.tree[originalKeyString] = child; this.tree[originalKeyString] = child;
} }
this._write(child); this.#write(child).then(() => {
this.exportedCount++;
this.#updateProgress();
});
} }
} }
@ -140,23 +195,23 @@ export default class ExportAsJSONAction {
* @param {object} parent * @param {object} parent
* @returns {object} * @returns {object}
*/ */
_rewriteLink(child, parent) { #rewriteLink(child, parent) {
const originalKeyString = this._getKeystring(child); const originalKeyString = this.#getKeystring(child);
const parentKeyString = this._getKeystring(parent); const parentKeyString = this.#getKeystring(parent);
const conditionSetIdentifier = this._getConditionSetIdentifier(parent); const conditionSetIdentifier = this.#getConditionSetIdentifier(parent);
const hasItemConditionSetIdentifiers = this._hasItemConditionSetIdentifiers(parent); const hasItemConditionSetIdentifiers = this.#hasItemConditionSetIdentifiers(parent);
const existingMappedKeyString = this.idMap[originalKeyString]; const existingMappedKeyString = this.idMap[originalKeyString];
let copy; let copy;
if (!existingMappedKeyString) { if (!existingMappedKeyString) {
copy = this._copy(child); copy = this.#copy(child);
copy.identifier.key = uuid(); copy.identifier.key = uuid();
if (!conditionSetIdentifier && !hasItemConditionSetIdentifiers) { if (!conditionSetIdentifier && !hasItemConditionSetIdentifiers) {
copy.location = parentKeyString; copy.location = parentKeyString;
} }
let newKeyString = this._getKeystring(copy); let newKeyString = this.#getKeystring(copy);
this.idMap[originalKeyString] = newKeyString; this.idMap[originalKeyString] = newKeyString;
this.tree[newKeyString] = copy; this.tree[newKeyString] = copy;
} else { } else {
@ -166,7 +221,7 @@ export default class ExportAsJSONAction {
if (conditionSetIdentifier || hasItemConditionSetIdentifiers) { if (conditionSetIdentifier || hasItemConditionSetIdentifiers) {
// update objectStyle object // update objectStyle object
if (conditionSetIdentifier) { if (conditionSetIdentifier) {
const directObjectStylesIdentifier = this.openmct.objects.areIdsEqual( const directObjectStylesIdentifier = this.#openmct.objects.areIdsEqual(
parent.configuration.objectStyles.conditionSetIdentifier, parent.configuration.objectStyles.conditionSetIdentifier,
child.identifier child.identifier
); );
@ -187,7 +242,7 @@ export default class ExportAsJSONAction {
if ( if (
itemConditionSetIdentifier && itemConditionSetIdentifier &&
this.openmct.objects.areIdsEqual(itemConditionSetIdentifier, child.identifier) this.#openmct.objects.areIdsEqual(itemConditionSetIdentifier, child.identifier)
) { ) {
parent.configuration.objectStyles[itemId].conditionSetIdentifier = copy.identifier; parent.configuration.objectStyles[itemId].conditionSetIdentifier = copy.identifier;
this.tree[parentKeyString].configuration.objectStyles[itemId].conditionSetIdentifier = this.tree[parentKeyString].configuration.objectStyles[itemId].conditionSetIdentifier =
@ -199,7 +254,7 @@ export default class ExportAsJSONAction {
} else { } else {
// just update parent // just update parent
const index = parent.composition.findIndex((identifier) => { const index = parent.composition.findIndex((identifier) => {
return this.openmct.objects.areIdsEqual(child.identifier, identifier); return this.#openmct.objects.areIdsEqual(child.identifier, identifier);
}); });
parent.composition[index] = copy.identifier; parent.composition[index] = copy.identifier;
@ -214,8 +269,8 @@ export default class ExportAsJSONAction {
* @param {object} domainObject * @param {object} domainObject
* @returns {string} A string representation of the given identifier, including namespace and key * @returns {string} A string representation of the given identifier, including namespace and key
*/ */
_getKeystring(domainObject) { #getKeystring(domainObject) {
return this.openmct.objects.makeKeyString(domainObject.identifier); return this.#openmct.objects.makeKeyString(domainObject.identifier);
} }
/** /**
@ -223,9 +278,9 @@ export default class ExportAsJSONAction {
* @param {object} domainObject * @param {object} domainObject
* @returns {boolean} * @returns {boolean}
*/ */
_isCreatableAndPersistable(domainObject) { #isCreatableAndPersistable(domainObject) {
const type = this.openmct.types.get(domainObject.type); const type = this.#openmct.types.get(domainObject.type);
const isPersistable = this.openmct.objects.isPersistable(domainObject.identifier); const isPersistable = this.#openmct.objects.isPersistable(domainObject.identifier);
return type && type.definition.creatable && isPersistable; return type && type.definition.creatable && isPersistable;
} }
@ -236,10 +291,10 @@ export default class ExportAsJSONAction {
* @param {object} parent * @param {object} parent
* @returns {boolean} * @returns {boolean}
*/ */
_isLinkedObject(child, parent) { #isLinkedObject(child, parent) {
const rootKeyString = this._getKeystring(this.root); const rootKeyString = this.#getKeystring(this.root);
const childKeyString = this._getKeystring(child); const childKeyString = this.#getKeystring(child);
const parentKeyString = this._getKeystring(parent); const parentKeyString = this.#getKeystring(parent);
return ( return (
(child.location !== parentKeyString && (child.location !== parentKeyString &&
@ -249,11 +304,11 @@ export default class ExportAsJSONAction {
); );
} }
_getConditionSetIdentifier(object) { #getConditionSetIdentifier(object) {
return object.configuration?.objectStyles?.conditionSetIdentifier; return object.configuration?.objectStyles?.conditionSetIdentifier;
} }
_hasItemConditionSetIdentifiers(parent) { #hasItemConditionSetIdentifiers(parent) {
const objectStyles = parent.configuration?.objectStyles; const objectStyles = parent.configuration?.objectStyles;
for (const itemId in objectStyles) { for (const itemId in objectStyles) {
@ -265,7 +320,7 @@ export default class ExportAsJSONAction {
return false; return false;
} }
_getItemConditionSetIdentifiers(parent) { #getItemConditionSetIdentifiers(parent) {
const objectStyles = parent.configuration?.objectStyles; const objectStyles = parent.configuration?.objectStyles;
let identifiers = new Set(); let identifiers = new Set();
@ -280,10 +335,15 @@ export default class ExportAsJSONAction {
return Array.from(identifiers); return Array.from(identifiers);
} }
#resetCounts() {
this.totalToExport = 0;
this.exportedCount = 0;
}
/** /**
* @private * @private
*/ */
_rewriteReferences() { #rewriteReferences() {
const oldKeyStrings = Object.keys(this.idMap); const oldKeyStrings = Object.keys(this.idMap);
let treeString = JSON.stringify(this.tree); let treeString = JSON.stringify(this.tree);
@ -291,8 +351,8 @@ export default class ExportAsJSONAction {
// this will cover keyStrings, identifiers and identifiers created // this will cover keyStrings, identifiers and identifiers created
// by hand that may be structured differently from those created with 'makeKeyString' // by hand that may be structured differently from those created with 'makeKeyString'
const newKeyString = this.idMap[oldKeyString]; const newKeyString = this.idMap[oldKeyString];
const newIdentifier = JSON.stringify(this.openmct.objects.parseKeyString(newKeyString)); const newIdentifier = JSON.stringify(this.#openmct.objects.parseKeyString(newKeyString));
const oldIdentifier = this.openmct.objects.parseKeyString(oldKeyString); const oldIdentifier = this.#openmct.objects.parseKeyString(oldKeyString);
const oldIdentifierNamespaceFirst = JSON.stringify(oldIdentifier); const oldIdentifierNamespaceFirst = JSON.stringify(oldIdentifier);
const oldIdentifierKeyFirst = JSON.stringify({ const oldIdentifierKeyFirst = JSON.stringify({
key: oldIdentifier.key, key: oldIdentifier.key,
@ -318,29 +378,35 @@ export default class ExportAsJSONAction {
* @private * @private
* @param {object} completedTree * @param {object} completedTree
*/ */
_saveAs(completedTree) { saveAs(completedTree) {
this.JSONExportService.export(completedTree, { filename: this.root.name + '.json' }); this.JSONExportService.export(completedTree, { filename: this.root.name + '.json' });
} }
/** /**
* @private * @private
* @returns {object} * @returns {object}
*/ */
_wrapTree() { #wrapTree() {
return { return {
openmct: this.tree, openmct: this.tree,
rootId: this._getKeystring(this.root) rootId: this.#getKeystring(this.root)
}; };
} }
_decrementCallsAndSave() { #decrementCallsAndSave() {
this.calls--; this.calls--;
this.#updateProgress();
if (this.calls === 0) { if (this.calls === 0) {
this._rewriteReferences(); this.#rewriteReferences();
this._saveAs(this._wrapTree()); this.dialog.dismiss();
this.#resetCounts();
this.saveAs(this.#wrapTree());
this.dialog = null;
} }
} }
_copy(object) { #copy(object) {
return JSON.parse(JSON.stringify(object)); return JSON.parse(JSON.stringify(object));
} }
} }

View File

@ -138,7 +138,7 @@ describe('Export as JSON plugin', () => {
}; };
}); });
spyOn(exportAsJSONAction, '_saveAs').and.callFake((completedTree) => { spyOn(exportAsJSONAction, 'saveAs').and.callFake((completedTree) => {
expect(Object.keys(completedTree).length).toBe(2); expect(Object.keys(completedTree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy(); expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy(); expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
@ -195,7 +195,7 @@ describe('Export as JSON plugin', () => {
}; };
}); });
spyOn(exportAsJSONAction, '_saveAs').and.callFake((completedTree) => { spyOn(exportAsJSONAction, 'saveAs').and.callFake((completedTree) => {
expect(Object.keys(completedTree).length).toBe(2); expect(Object.keys(completedTree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy(); expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy(); expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
@ -257,7 +257,7 @@ describe('Export as JSON plugin', () => {
}; };
}); });
spyOn(exportAsJSONAction, '_saveAs').and.callFake((completedTree) => { spyOn(exportAsJSONAction, 'saveAs').and.callFake((completedTree) => {
expect(Object.keys(completedTree).length).toBe(2); expect(Object.keys(completedTree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy(); expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy(); expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
@ -318,7 +318,7 @@ describe('Export as JSON plugin', () => {
}; };
}); });
spyOn(exportAsJSONAction, '_saveAs').and.callFake((completedTree) => { spyOn(exportAsJSONAction, 'saveAs').and.callFake((completedTree) => {
expect(Object.keys(completedTree).length).toBe(2); expect(Object.keys(completedTree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy(); expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy(); expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
@ -373,7 +373,7 @@ describe('Export as JSON plugin', () => {
return Promise.resolve(child); return Promise.resolve(child);
}); });
spyOn(exportAsJSONAction, '_saveAs').and.callFake((completedTree) => { spyOn(exportAsJSONAction, 'saveAs').and.callFake((completedTree) => {
expect(Object.keys(completedTree).length).toBe(2); expect(Object.keys(completedTree).length).toBe(2);
const conditionSetId = Object.keys(completedTree.openmct)[1]; const conditionSetId = Object.keys(completedTree.openmct)[1];
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy(); expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();

View File

@ -71,14 +71,14 @@ export default class LocalStorageObjectProvider {
* @private * @private
*/ */
persistSpace(space) { persistSpace(space) {
this.localStorage[this.spaceKey] = JSON.stringify(space); this.localStorage.setItem(this.spaceKey, JSON.stringify(space));
} }
/** /**
* @private * @private
*/ */
getSpace() { getSpace() {
return this.localStorage[this.spaceKey]; return this.localStorage.getItem(this.spaceKey);
} }
/** /**
@ -93,7 +93,7 @@ export default class LocalStorageObjectProvider {
*/ */
initializeSpace() { initializeSpace() {
if (this.isEmpty()) { if (this.isEmpty()) {
this.localStorage[this.spaceKey] = JSON.stringify({}); this.localStorage.setItem(this.spaceKey, JSON.stringify({}));
} }
} }
@ -101,6 +101,6 @@ export default class LocalStorageObjectProvider {
* @private * @private
*/ */
isEmpty() { isEmpty() {
return this.getSpace() === undefined; return this.getSpace() === null;
} }
} }

View File

@ -1,5 +1,5 @@
import { saveAs } from 'file-saver';
import Moment from 'moment'; import Moment from 'moment';
import { saveAs } from 'saveAs';
import { NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-constants.js'; import { NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-constants.js';
const UNKNOWN_USER = 'Unknown'; const UNKNOWN_USER = 'Unknown';

View File

@ -23,14 +23,12 @@
<div class="c-progress-bar"> <div class="c-progress-bar">
<div <div
class="c-progress-bar__bar" class="c-progress-bar__bar"
:class="{ '--indeterminate': !model.progressPerc }" :class="{ '--indeterminate': !progressPerc }"
:style="styleBarWidth" :style="styleBarWidth"
></div> ></div>
<div v-if="model.progressText !== undefined" class="c-progress-bar__text"> <div v-if="progressText !== ''" class="c-progress-bar__text">
<span v-if="model.progressPerc && model.progressPerc > 0" <span v-if="progressPerc > 0">{{ progressPerc }}% complete.</span>
>{{ model.progressPerc }}% complete.</span {{ progressText }}
>
{{ model.progressText }}
</div> </div>
</div> </div>
</template> </template>
@ -38,14 +36,18 @@
<script> <script>
export default { export default {
props: { props: {
model: { progressPerc: {
type: Object, type: Number,
required: true default: 0
},
progressText: {
type: String,
default: ''
} }
}, },
computed: { computed: {
styleBarWidth() { styleBarWidth() {
return this.model.progressPerc ? `width: ${this.model.progressPerc}%;` : ''; return this.progressPerc ? `width: ${this.progressPerc}%;` : '';
} }
} }
}; };

View File

@ -23,6 +23,7 @@
import mount from 'utils/mount'; import mount from 'utils/mount';
import { createOpenMct, resetApplicationState } from 'utils/testing'; import { createOpenMct, resetApplicationState } from 'utils/testing';
import { mockLocalStorage } from 'utils/testing/mockLocalStorage'; import { mockLocalStorage } from 'utils/testing/mockLocalStorage';
import { nextTick } from 'vue';
import StylesView from '@/plugins/condition/components/inspector/StylesView.vue'; import StylesView from '@/plugins/condition/components/inspector/StylesView.vue';
@ -67,7 +68,7 @@ describe('the inspector', () => {
expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(1); expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(1);
}); });
it('should display all saved styles', () => { it('should display all saved styles', async () => {
selection = mockTelemetryTableSelection; selection = mockTelemetryTableSelection;
stylesViewComponent = createViewComponent(StylesView, selection, openmct); stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct); savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
@ -75,10 +76,9 @@ describe('the inspector', () => {
expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(0); expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(0);
stylesViewComponent.$refs.root.saveStyle(mockStyle); stylesViewComponent.$refs.root.saveStyle(mockStyle);
return stylesViewComponent.$nextTick().then(() => { await nextTick();
expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(1); expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(1);
}); });
});
xit('should allow a saved style to be applied', () => { xit('should allow a saved style to be applied', () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true); spyOn(openmct.editor, 'isEditing').and.returnValue(true);
@ -139,14 +139,14 @@ describe('the inspector', () => {
expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(20); expect(savedStylesViewComponent.$refs.root.savedStyles.length).toBe(20);
}); });
it('should allow styles from multi-selections to be saved', () => { it('should allow styles from multi-selections to be saved', async () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true); spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockMultiSelectionSameStyles; selection = mockMultiSelectionSameStyles;
stylesViewComponent = createViewComponent(StylesView, selection, openmct); stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct); savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
return stylesViewComponent.$nextTick().then(() => { await nextTick();
const styleEditorComponent = stylesViewComponent.$refs.root.$refs.styleEditor; const styleEditorComponent = stylesViewComponent.$refs.root.$refs.styleEditor;
const saveStyleButton = styleEditorComponent.$refs.saveStyleButton; const saveStyleButton = styleEditorComponent.$refs.saveStyleButton;
@ -156,39 +156,36 @@ describe('the inspector', () => {
expect(savedStylesViewComponent.$refs.root.$data.savedStyles.length).toBe(1); expect(savedStylesViewComponent.$refs.root.$data.savedStyles.length).toBe(1);
}); });
});
it('should prevent mixed styles from being saved', () => { it('should prevent mixed styles from being saved', async () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true); spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockMultiSelectionMixedStyles; selection = mockMultiSelectionMixedStyles;
stylesViewComponent = createViewComponent(StylesView, selection, openmct); stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct); savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
return stylesViewComponent.$nextTick().then(() => { await nextTick();
const styleEditorComponent = stylesViewComponent.$refs.root.$refs.styleEditor; const styleEditorComponent = stylesViewComponent.$refs.root.$refs.styleEditor;
const saveStyleButton = styleEditorComponent.$refs.saveStyleButton; const saveStyleButton = styleEditorComponent.$refs.saveStyleButton;
// Saving should not be enabled, thus the button ref should be undefined // Saving should not be enabled, thus the button ref should be undefined
expect(saveStyleButton).toBe(undefined); expect(saveStyleButton).toBe(undefined);
}); });
});
it('should prevent non-specific styles from being saved', () => { it('should prevent non-specific styles from being saved', async () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true); spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockMultiSelectionNonSpecificStyles; selection = mockMultiSelectionNonSpecificStyles;
stylesViewComponent = createViewComponent(StylesView, selection, openmct); stylesViewComponent = createViewComponent(StylesView, selection, openmct);
savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct); savedStylesViewComponent = createViewComponent(SavedStylesView, selection, openmct);
return stylesViewComponent.$nextTick().then(() => { await nextTick();
const styleEditorComponent = stylesViewComponent.$refs.root.$refs.styleEditor; const styleEditorComponent = stylesViewComponent.$refs.root.$refs.styleEditor;
const saveStyleButton = styleEditorComponent.$refs.saveStyleButton; const saveStyleButton = styleEditorComponent.$refs.saveStyleButton;
// Saving should not be enabled, thus the button ref should be undefined // Saving should not be enabled, thus the button ref should be undefined
expect(saveStyleButton).toBe(undefined); expect(saveStyleButton).toBe(undefined);
}); });
});
function createViewComponent(component) { function createViewComponent(component) {
const element = document.createElement('div'); const element = document.createElement('div');

View File

@ -10,7 +10,7 @@ export function mockLocalStorage() {
store = {}; store = {};
function getItem(key) { function getItem(key) {
return store[key]; return store[key] || null;
} }
function setItem(key, value) { function setItem(key, value) {