Compare commits

...

5 Commits

Author SHA1 Message Date
Jesse Mazzella
45cbf514f2 docs: update release docs 2024-02-28 13:25:37 -08:00
Jesse Mazzella
8090e71110 docs(WIP): update release documentation 2024-02-22 11:37:46 -08:00
Shefali Joshi
dc9bd8bcb1 Remove all logic around auto centering and scrolling the timelist (#7474) 2024-02-22 17:56:54 +00:00
Jesse Mazzella
29d83e9c6d fix(#7456): allow children of Overlay Plots to be removed (#7516)
* fix(#7456): check if object is an identifier

* refactor: use v-show, remove unnecessary deep copy, set yAxes array to empty in a way that preserves reactivity

* refactor: use ESM exports

* refactor: use ESM imports

* test(e2e): add test for element item removal from overlay plot

* a11y: add accessible name for object labels

* refactor: move overlayPlot creation to beforeAll, use getByLabel
2024-02-21 16:07:48 -08:00
Rukmini Bose (Ruki)
6393a77c19 [Mobile] Add Global Search to Mobile (#7477)
* Add status area back to mobile

* Make search results responsive to width

* Make clear search button always visible, regardless of hover

* Make clear search button visible, and fix weird margin in top left corner

* Fix input width, add logic to make close button work properly, fix margin on results so there is room for close button, fix various landscape mode issues

* update mobile testing

* Fix dropdown sizes, remove shadows and corners to make it look less like a popup and more full screen

* Add animation and persist search bar in mobile

* Fix linting issues

* Fix padding in Desktop

* fix padding in status area

* fix bad merge

* lint fixes

* fix bad merge... again

* again

* fixes to mobile

* update tests

* lint fix

* test fixes

---------

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2024-02-21 15:29:38 -08:00
43 changed files with 635 additions and 333 deletions

View File

@@ -160,6 +160,31 @@ jobs:
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
e2e-mobile:
executor: pw-focal-development
steps:
- build_and_install:
node-version: lts/hydrogen
- run: npm run test:e2e:mobile
- when:
condition:
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_e2e_code_cov_report:
suite: full
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
- store_artifacts:
path: coverage
- store_artifacts:
path: html-test-results
- when:
condition:
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
e2e-couchdb:
executor: ubuntu
steps:
@@ -260,8 +285,9 @@ workflows:
- e2e-test:
name: e2e-stable
suite: stable
- e2e-mobile
- visual-a11y-tests:
name: visual-test-ci
name: visual-a11y-test-ci
suite: ci
the-nightly: #These jobs do not run on PRs, but against master at night
@@ -277,10 +303,11 @@ workflows:
- e2e-test:
name: e2e-full-nightly
suite: full
- mem-test
- e2e-mobile
- perf-test
- mem-test
- visual-a11y-tests:
name: visual-test-nightly
name: visual-a11y-test-nightly
suite: full
- e2e-couchdb
triggers:

View File

@@ -3,28 +3,124 @@
This document outlines the process and key considerations for releasing a new version of the NASA Open MCT project as an NPM (Node Package Manager) package.
## FAQ
1. When do we publish a new version of Open MCT?
- At the end of a working sprint (typically) after all blocking issues have been resolved.
2. Where do we publish?
- [NPM](https://www.npmjs.com/package/openmct)
- [Github Releases](https://github.com/nasa/openmct/releases)
2. What do we publish?
- What constitutes a "stable" release?
- TODO
- What constitutes a "latest" release?
- The most recently published release.
- What constitutes a "nightly" release?
- TODO
4. What necessitates a patch release?
-
## 1. Pre-requisites
Before releasing a new version of the NASA Open MCT NPM package, ensure all dependencies are updated, and comprehensive tests are performed. This ensures compatibility and performance of the Open MCT within the Node.js ecosystem.
Before releasing a new version of Open MCT, ensure that all dependencies are updated, and
comprehensive testing is performed.
## 2. Versioning
Versioning is a critical step for package release. The Open MCT team follows [Semantic Versioning (SemVer)](https://semver.org) that consists of three major components: MAJOR.MINOR.PATCH. These ensure a structured process for updating, bug fixes, backward compatibility, and software progress.
Open MCT follows [Semantic Versioning 2.0.0 (SemVer)](https://semver.org) that consists of three
major components: `MAJOR.MINOR.PATCH` (i.e. `1.2.3`).
Major releases are necessitated by fundamental framework changes that are expected to be incompatible
with previous releases.
Minor releases are necessitated by non-backwards-compatible application, API changes, or new
features or enhancements.
Patch releases are created for backporting fixes to blocking bugs that were discovered _after_
the release of a major or minor version. They are not to introduce new features, enhancements, or
dependency changes.
## 3. Changelog Maintenance
A comprehensive changelog file, `CHANGELOG.md`, documents any changes, adding a high level of transparencies for anyone desiring to look into the status of new and past progress. It includes the summation of any major new enhancements, changes, bug fixes, and the credits to the users responsible for each unique progress.
Changelogs can be found in the GitHub releases section of the repository and are auto-generated
using [GitHub's feature](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes).
## 4. Notable Changes Labels on GitHub PRs
## 4. Pull Request Labeling
For the Open MCT package, we leverage GitHub's Pull Request (PR) mechanisms extensively, with three important PR labels dedicated to signifying 'notable_changes':
Generation of release notes is automated by the use of labels on pull requests. The following
labels are used to categorize pull requests:
- **Breaking Change** Highlights the integration of changes that are suspected to break, or without a doubt will break, backward compatibility. These should signal to users the upgrade might be seamless only if dependency and integration factors are properly managed, if not, one should expect to manage atypical technical snags.
- **API change** Signifies when a contribution makes any complete or under layer changes to the communication or its supporting access processes. This label flags required see-through insight on how the web-based control panel sees and manipulates any value and or network logs.
- **Default Behavior Change:** In the incident an update either adjusts a form to or integrates a not previously kept setting or plugin. i.e. autoscale is enabled by default when working with plots.
### `type:bug`
## 6. Community & Contributions
Pull requests are to be labeled with `type:bug` if they contain changes that intend to fix a bug.
A flat community and the rounded center are kept in continuous celebration, with the given station open for two open-specifying dialogues, research, and all-for development probing. State the ownership for a handed looped, a welcome for even structure-core and architectural draft and impend.
### `type:enhancement`
Thank you for your collaboration and commitment to moving the project onto a text big club.
Pull requests are to be labeled with `type:enhancement` if they contain changes that intend to
enhance existing functionality of Open MCT.
### `type:feature`
Pull requests are to be labeled with `type:feature` if they contain changes that intend to introduce
new functionality to Open MCT.
### `type:maintenance`
Pull requests are to be labeled with `type:maintenance` if they contain changes that introduce
new tests, documentation, or other maintenance-related changes.
### `performance`
Pull requests are to be labeled with `performance` if they contain changes that are intended to
improve the performance of Open MCT.
### `notable_change`
Pull requests are to be labeled with `notable_change` if they contain changes that fit any of the
following criteria:
- **Breaking Change**
- Highlights the integration of changes that are suspected to break, or without a doubt will
break, backwards compatibility. These should signal to users the upgrade might be seamless only
if dependency and integration factors are properly managed, if not, one should expect to manage
atypical technical snags.
- **API Change**
- Signifies any change to the Open MCT API such as the addition of new methods, or the
modification or deprecation of existing methods. API changes may or may not constitute a
breaking change.
- **Default Behavior Change**
- Any change to the default behavior of Open MCT, such as the default configuration of a plugin,
or the default behavior of a user interface component or feature (i.e.: autoscale being enabled
by default on plots).
## 5. Community & Contributions
Open MCT is an open-source project and contributions are welcome. As such, it is important to
acknowledge the contributions of the community and contributors. Pull requests by contributors
will be labeled with `source:community` to signify that the contribution was made by a member of
the community.
## 6. Release Process
Currently, the release process is manual and requires the following steps:
1. Clone a fresh copy of the repository.
- `git clone git@github.com:nasa/openmct.git`
2. Check out the appropriate release branch.
- `git checkout release/1.2.3`
3. Ensure that the `package.json` file is updated with the correct version number and does not
contain the `-next` suffix (which implies a pre-release).
4. Create a tag for the release if it does not already exist.
- `git tag v1.2.3`
5. Push the tag to the repository.
- `git push origin v1.2.3`
6. Run `npm install` to install dependencies.
7. Publish the release to NPM (You will need to be logged in to an NPM account with the appropriate permissions).
- `npm publish`
8. Create a release on GitHub.
- Navigate to the Releases page on the Open MCT repository.
- Click [draft a new release.](https://github.com/nasa/openmct/releases/new)
- Choose the tag that was just created for the release.
- For "Previous tag", choose the tag that was most recently released.
- Click "Generate release notes" to auto-generate release notes.
- Click "Publish release" to publish the release.

View File

@@ -229,7 +229,7 @@ Current list of test tags:
|Test Tag|Description|
|:-:|-|
|`@ipad` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|`@mobile` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|`@a11y` | Test case or test suite to execute playwright-axe accessibility checks and generate a11y reports.|
|`@gds` | Denotes a GDS Test Case used in the VIPER Mission.|
|`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.|
@@ -329,9 +329,15 @@ In terms of operating system testing, we're only limited by what the CI provider
#### **Mobile**
We have the Mission-need to support iPad. To run our iPad suite, please see our `playwright-*.config.js` with the 'iPad' project.
We have a Mission-need to support iPad and mobile devices. To run our test suites with mobile devices, please see our `playwright-mobile.config.js` projects.
In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button) and so this will likely turn into a separate suite.
In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button). To bypass the object creation, we leverage the `storageState` properties for starting the mobile tests with localstorage.
For now, the mobile tests will exist in the /tests/mobile/ suites and be executed with the
```sh
npm run test:e2e:mobile
```
command.
#### **Skipping or executing tests based on browser, os, and/os browser version:**

View File

@@ -10,6 +10,7 @@ const NUM_WORKERS = 2;
const config = {
retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
testDir: 'tests',
grepInvert: /@mobile/, //Ignore mobile tests
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000,
webServer: {

View File

@@ -1,13 +1,11 @@
// playwright.config.js
// @ts-check
// eslint-disable-next-line no-unused-vars
import { devices } from '@playwright/test';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0,
testDir: 'tests',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
testIgnore: '**/*.perf.spec.js',
timeout: 30 * 1000,
webServer: {
@@ -35,7 +33,6 @@ const config = {
},
{
name: 'MMOC',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
@@ -47,8 +44,6 @@ const config = {
},
{
name: 'safari',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340
grepInvert: /@snapshot/,
use: {
browserName: 'webkit'
@@ -56,7 +51,6 @@ const config = {
},
{
name: 'firefox',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'firefox'
@@ -64,7 +58,6 @@ const config = {
},
{
name: 'canary',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
@@ -73,22 +66,11 @@ const config = {
},
{
name: 'chrome-beta',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
channel: 'chrome-beta'
}
},
{
name: 'ipad',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grep: /@ipad/,
grepInvert: /@snapshot/,
use: {
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}
],
reporter: [

View File

@@ -0,0 +1,69 @@
// playwright.config.js
// @ts-check
import { devices } from '@playwright/test';
const MAX_FAILURES = 5;
const NUM_WORKERS = 2;
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 1, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
testDir: 'tests',
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 30 * 1000,
webServer: {
command: 'npm run start:coverage',
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
},
maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent
use: {
baseURL: 'http://localhost:8080/',
headless: true,
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
trace: 'on-first-retry',
video: 'off'
},
projects: [
{
name: 'ipad',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
},
{
name: 'iphone',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}
],
reporter: [
['list'],
[
'html',
{
open: 'never',
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
}
],
['junit', { outputFile: '../test-results/results.xml' }]
]
};
export default config;

View File

@@ -1,6 +1,9 @@
// playwright.config.js
// @ts-check
import { devices } from '@playwright/test';
import { fileURLToPath } from 'url';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0, //Retries are not needed with watch mode
@@ -28,6 +31,28 @@ const config = {
use: {
browserName: 'chromium'
}
},
{
name: 'ipad',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
},
{
name: 'iphone',
grep: /@mobile/,
use: {
storageState: fileURLToPath(
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
),
browserName: 'webkit',
...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}
],
reporter: [

View File

@@ -363,7 +363,7 @@ test.describe('Example Imagery in Display Layout', () => {
await page.locator('li[title="View Large"]').click();
await expect(pausePlayButton).toHaveClass(/is-paused/);
await page.locator('[aria-label="Close"]').click();
await page.getByRole('button', { name: 'Close' }).click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
});
@@ -386,7 +386,7 @@ test.describe('Example Imagery in Display Layout', () => {
await page.locator('li[title="View Large"]').click();
await expect(pausePlayButton).toHaveClass(/is-paused/);
await page.locator('[aria-label="Close"]').click();
await page.getByRole('button', { name: 'Close' }).click();
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
});
@@ -509,7 +509,7 @@ test.describe('Example Imagery in Flexible layout', () => {
await page.getByRole('button', { name: 'Background Image', state: 'visible' });
// Close the large view
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
});
test.beforeEach(async ({ page }) => {

View File

@@ -68,7 +68,7 @@ test.describe('Snapshot image tests', () => {
// expect large image to be displayed
await expect(page.getByRole('dialog').getByText('favicon-96x96.png')).toBeVisible();
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
// drop another image onto the entry
await page.dispatchEvent('.c-snapshots', 'drop', { dataTransfer: dropTransfer });

View File

@@ -33,15 +33,15 @@ import {
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Overlay Plot', () => {
let overlayPlot;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
});
test('Plot legend color is in sync with plot series color', async ({ page }) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
@@ -68,9 +68,6 @@ test.describe('Overlay Plot', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7403'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
@@ -130,10 +127,6 @@ test.describe('Overlay Plot', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6338'
});
// Create an Overlay Plot with a default SWG
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
@@ -199,10 +192,6 @@ test.describe('Overlay Plot', () => {
test('The elements pool supports dragging series into multiple y-axis buckets', async ({
page
}) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
@@ -292,10 +281,6 @@ test.describe('Overlay Plot', () => {
description: 'https://github.com/nasa/openmct/issues/7421'
});
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
@@ -309,12 +294,32 @@ test.describe('Overlay Plot', () => {
await page.getByRole('tab', { name: 'Elements' }).click();
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
const plotPixelSize = plotPixels.length;
expect(plotPixelSize).toBeGreaterThan(0);
}
);
test('Can remove an item via the elements pool action menu', async ({ page }) => {
const swgA = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
});
await page.goto(overlayPlot.url);
// Wait for plot series data to load and be drawn
await waitForPlotsToRender(page);
await page.getByLabel('Edit Object').click();
await page.getByRole('tab', { name: 'Elements' }).click();
const swgAElementsPoolItem = page.getByLabel(`Preview ${swgA.name}`);
await expect(swgAElementsPoolItem).toBeVisible();
await swgAElementsPoolItem.click({ button: 'right' });
await page.getByRole('menuitem', { name: 'Remove' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(swgAElementsPoolItem).toBeHidden();
});
});
/**

View File

@@ -57,7 +57,7 @@ test.describe('Plots work in Previews', () => {
await page.getByLabel('Sine', { exact: true }).click({ button: 'right' });
await page.getByLabel('View Historical Data').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
await page.getByLabel('Expand Test Display Layout layout').click();
// change to a plot and ensure embiggen works
@@ -73,7 +73,7 @@ test.describe('Plots work in Previews', () => {
await expect(page.getByLabel('Preview Container')).toBeHidden();
await page.getByLabel('Large View').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
// get last sinewave tree item (in the display layout)
await page
@@ -83,6 +83,6 @@ test.describe('Plots work in Previews', () => {
.click({ button: 'right' });
await page.getByLabel('View', { exact: true }).click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
});
});

View File

@@ -53,7 +53,7 @@ test.describe('Preview mode', () => {
await expect(page.getByLabel('Export Table Data')).toBeVisible();
await expect(page.getByLabel('Export Marked Rows')).toBeVisible();
await page.getByRole('menuitem', { name: 'Pause' }).click();
await page.getByLabel('Close').click();
await page.getByRole('button', { name: 'Close' }).click();
await expandEntireTree(page);

View File

@@ -48,7 +48,7 @@ test('Verify that the create button appears and that the Folder Domain Object is
await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled();
});
test('Verify that My Items Tree appears @ipad', async ({ page, openmctConfig }) => {
test('Verify that My Items Tree appears', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
//Go to baseURL
await page.goto('./');

View File

@@ -0,0 +1,59 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
/*
This test suite is dedicated to tests which can quickly verify that any openmct installation is
operable and that any type of testing can proceed.
Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them
more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly
as they cover a very "thin surface" of functionality.
When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel
comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects.
Make no assumptions about the order that elements appear in the DOM.
*/
import { expect, test } from '../../pluginFixtures.js';
test('Verify that My Items Tree appears @mobile', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
//Go to baseURL
await page.goto('./');
//My Items to be visible
await expect(page.getByRole('treeitem', { name: `${myItemsFolderName}` })).toBeVisible();
});
test('Verify that user can search @mobile', async ({ page }) => {
//For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json'
await page.goto('./');
await page.getByRole('searchbox', { name: 'Search Input' }).click();
await page.getByRole('searchbox', { name: 'Search Input' }).fill('Parent Display Layout');
//Search Results appear in search modal
await expect(page.getByLabel('Object Results').getByText('Parent Display Layout')).toBeVisible();
//Clicking on the search result takes you to the object
await page.getByLabel('Object Results').getByText('Parent Display Layout').click();
await page.getByTitle('Collapse Browse Pane').click();
await expect(page.getByRole('main').getByText('Parent Display Layout')).toBeVisible();
});

View File

@@ -107,6 +107,7 @@
"test:debug": "KARMA_DEBUG=true karma start karma.conf.cjs",
"test:e2e": "npx playwright test",
"test:e2e:a11y": "npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep @a11y",
"test:e2e:mobile": "npx playwright test --config=e2e/playwright-mobile.config.js",
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb --workers=1",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb|@generatedata\"",
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import _ from 'lodash';
import objectUtils from '../objects/object-utils.js';
import { makeKeyString, parseKeyString } from '../objects/object-utils.js';
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
@@ -223,18 +223,18 @@ export default class CompositionProvider {
* @param {DomainObject} oldDomainObject
*/
#onMutation(newDomainObject, oldDomainObject) {
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
const id = makeKeyString(oldDomainObject.identifier);
const listeners = this.#listeningTo[id];
if (!listeners) {
return;
}
const oldComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
const newComposition = newDomainObject.composition.map(objectUtils.makeKeyString);
const oldComposition = oldDomainObject.composition.map(makeKeyString);
const newComposition = newDomainObject.composition.map(makeKeyString);
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
const added = _.difference(newComposition, oldComposition).map(parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(parseKeyString);
function notify(value) {
return function (listener) {

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import { toRaw } from 'vue';
import objectUtils from '../objects/object-utils.js';
import { makeKeyString } from '../objects/object-utils.js';
import CompositionProvider from './CompositionProvider.js';
/**
@@ -91,7 +91,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
this.establishTopicListener();
/** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const keyString = makeKeyString(domainObject.identifier);
let objectListeners = this.listeningTo[keyString];
if (!objectListeners) {
@@ -120,7 +120,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
*/
off(domainObject, event, callback, context) {
/** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const keyString = makeKeyString(domainObject.identifier);
const objectListeners = this.listeningTo[keyString];
const index = objectListeners[event].findIndex((l) => {
@@ -228,7 +228,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);
/** @type {string} */
let id = objectUtils.makeKeyString(domainObject.identifier);
let id = makeKeyString(domainObject.identifier);
const listeners = this.listeningTo[id];
if (!listeners) {

View File

@@ -22,7 +22,7 @@
import EventEmitter from 'EventEmitter';
import _ from 'lodash';
import utils from './object-utils.js';
import { makeKeyString, refresh } from './object-utils.js';
const ANY_OBJECT_EVENT = 'mutation';
@@ -152,7 +152,7 @@ class MutableDomainObject {
mutable.$observe('$_synchronize_model', (updatedObject) => {
let clone = JSON.parse(JSON.stringify(updatedObject));
utils.refresh(mutable, clone);
refresh(mutable, clone);
});
return mutable;
@@ -168,7 +168,7 @@ class MutableDomainObject {
}
function qualifiedEventName(object, eventName) {
let keystring = utils.makeKeyString(object.identifier);
let keystring = makeKeyString(object.identifier);
return [keystring, eventName].join(':');
}

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import utils from 'objectUtils';
import { identifierEquals, makeKeyString, parseKeyString, refresh } from 'objectUtils';
import ConflictError from './ConflictError.js';
import InMemorySearchProvider from './InMemorySearchProvider.js';
@@ -82,8 +82,19 @@ import Transaction from './Transaction.js';
* @memberof module:openmct
*/
export default class ObjectAPI {
#makeKeyString;
#parseKeyString;
#identifierEquals;
#refresh;
#openmct;
constructor(typeRegistry, openmct) {
this.openmct = openmct;
this.#makeKeyString = makeKeyString;
this.#parseKeyString = parseKeyString;
this.#identifierEquals = identifierEquals;
this.#refresh = refresh;
this.#openmct = openmct;
this.typeRegistry = typeRegistry;
this.SEARCH_TYPES = Object.freeze({
OBJECTS: 'OBJECTS',
@@ -206,14 +217,14 @@ export default class ObjectAPI {
* has been saved, or be rejected if it cannot be saved
*/
get(identifier, abortSignal, forceRemote = false) {
let keystring = this.makeKeyString(identifier);
let keystring = this.#makeKeyString(identifier);
if (!forceRemote) {
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
identifier = utils.parseKeyString(identifier);
identifier = parseKeyString(identifier);
if (this.isTransactionActive()) {
let dirtyObject = this.transaction.getDirtyObject(identifier);
@@ -227,7 +238,7 @@ export default class ObjectAPI {
const provider = this.getProvider(identifier);
if (!provider) {
throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}"`);
throw new Error(`No Provider Matched for keyString "${this.#makeKeyString(identifier)}"`);
}
if (!provider.get) {
@@ -325,7 +336,7 @@ export default class ObjectAPI {
*/
getMutable(identifier) {
if (!this.supportsMutation(identifier)) {
throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`);
throw new Error(`Object "${this.#makeKeyString(identifier)}" does not support mutation.`);
}
return this.get(identifier).then((object) => {
@@ -352,7 +363,7 @@ export default class ObjectAPI {
}
isPersistable(idOrKeyString) {
let identifier = utils.parseKeyString(idOrKeyString);
let identifier = parseKeyString(idOrKeyString);
let provider = this.getProvider(identifier);
if (provider?.isReadOnly) {
return !provider.isReadOnly();
@@ -362,7 +373,7 @@ export default class ObjectAPI {
}
isMissing(domainObject) {
let identifier = utils.makeKeyString(domainObject.identifier);
let identifier = makeKeyString(domainObject.identifier);
let missingName = 'Missing: ' + identifier;
return domainObject.name === missingName;
@@ -442,21 +453,21 @@ export default class ObjectAPI {
if (error instanceof this.errors.Conflict) {
// Synchronized objects will resolve their own conflicts
if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
this.openmct.notifications.info(
`Conflict detected while saving "${this.makeKeyString(
this.#openmct.notifications.info(
`Conflict detected while saving "${this.#makeKeyString(
domainObject.name
)}", attempting to resolve`
);
} else {
this.openmct.notifications.error(
`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`
this.#openmct.notifications.error(
`Conflict detected while saving ${this.#makeKeyString(domainObject.identifier)}`
);
if (this.isTransactionActive()) {
this.endTransaction();
}
await this.refresh(domainObject);
await this.#refresh(domainObject);
}
}
@@ -465,7 +476,7 @@ export default class ObjectAPI {
}
async #getCurrentUsername() {
const user = await this.openmct.user.getCurrentUser();
const user = await this.#openmct.user.getCurrentUser();
let username;
if (user !== undefined) {
@@ -554,7 +565,7 @@ export default class ObjectAPI {
*/
getRelativePath(objectPath) {
return objectPath
.map((p) => this.makeKeyString(p.identifier))
.map((p) => this.#makeKeyString(p.identifier))
.reverse()
.join('/');
}
@@ -574,13 +585,13 @@ export default class ObjectAPI {
}
let sourceTelemetry = null;
if (telemetryIdentifier && utils.identifierEquals(identifier, telemetryIdentifier)) {
if (telemetryIdentifier && this.#identifierEquals(identifier, telemetryIdentifier)) {
sourceTelemetry = identifier;
} else if (objectDetails.composition) {
sourceTelemetry = objectDetails.composition[0];
if (telemetryIdentifier) {
sourceTelemetry = objectDetails.composition.find((telemetrySource) =>
utils.identifierEquals(telemetrySource, telemetryIdentifier)
this.#identifierEquals(telemetrySource, telemetryIdentifier)
);
}
}
@@ -666,7 +677,7 @@ export default class ObjectAPI {
mutableObject = MutableDomainObject.createMutable(domainObject, this.eventEmitter);
// Check if provider supports realtime updates
let identifier = utils.parseKeyString(mutableObject.identifier);
let identifier = parseKeyString(mutableObject.identifier);
let provider = this.getProvider(identifier);
if (
@@ -706,7 +717,7 @@ export default class ObjectAPI {
if (domainObject.isMutable) {
domainObject.$refresh(refreshedObject);
} else {
utils.refresh(domainObject, refreshedObject);
refresh(domainObject, refreshedObject);
}
return domainObject;
@@ -745,7 +756,7 @@ export default class ObjectAPI {
* @returns {string} A string representation of the given identifier, including namespace and key
*/
makeKeyString(identifier) {
return utils.makeKeyString(identifier);
return makeKeyString(identifier);
}
/**
@@ -753,7 +764,7 @@ export default class ObjectAPI {
* @returns {module:openmct.ObjectAPI~Identifier} An identifier object
*/
parseKeyString(keyString) {
return utils.parseKeyString(keyString);
return parseKeyString(keyString);
}
/**
@@ -761,9 +772,9 @@ export default class ObjectAPI {
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
*/
areIdsEqual(...identifiers) {
const firstIdentifier = utils.parseKeyString(identifiers[0]);
const firstIdentifier = this.#parseKeyString(identifiers[0]);
return identifiers.map(utils.parseKeyString).every((identifier) => {
return identifiers.map(this.#parseKeyString).every((identifier) => {
return (
identifier === firstIdentifier ||
(identifier.namespace === firstIdentifier.namespace &&
@@ -791,7 +802,7 @@ export default class ObjectAPI {
}
return path.some((pathElement) => {
const identifierToCheck = utils.parseKeyString(keyStringToCheck);
const identifierToCheck = this.#parseKeyString(keyStringToCheck);
return this.areIdsEqual(identifierToCheck, pathElement.identifier);
});
@@ -814,7 +825,7 @@ export default class ObjectAPI {
if (location && !this.#pathContainsDomainObject(location, path)) {
// if we have a location, and we don't already have this in our constructed path,
// then keep walking up the path
return this.getOriginalPath(utils.parseKeyString(location), path, abortSignal);
return this.getOriginalPath(this.#parseKeyString(location), path, abortSignal);
} else {
return path;
}
@@ -853,8 +864,8 @@ export default class ObjectAPI {
await Promise.all(
keyStrings.map((keyString) =>
this.supportsMutation(keyString)
? this.getMutable(utils.parseKeyString(keyString))
: this.get(utils.parseKeyString(keyString))
? this.getMutable(this.#parseKeyString(keyString))
: this.get(this.#parseKeyString(keyString))
)
)
).reverse();
@@ -866,7 +877,7 @@ export default class ObjectAPI {
return (
objectPath !== undefined &&
objectPath.length > 1 &&
domainObject.location !== this.makeKeyString(objectPath[1].identifier)
domainObject.location !== this.#makeKeyString(objectPath[1].identifier)
);
}

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import utils from './object-utils.js';
import { isIdentifier } from './object-utils.js';
export default class RootRegistry {
constructor(openmct) {
@@ -47,12 +47,12 @@ export default class RootRegistry {
}
_isValid(rootItem) {
if (utils.isIdentifier(rootItem) || typeof rootItem === 'function') {
if (isIdentifier(rootItem) || typeof rootItem === 'function') {
return true;
}
if (Array.isArray(rootItem)) {
return rootItem.every(utils.isIdentifier);
return rootItem.every(isIdentifier);
}
return false;

View File

@@ -1,4 +1,4 @@
import utils from 'objectUtils';
import { makeKeyString, parseKeyString } from 'objectUtils';
import Transaction from './Transaction.js';
@@ -9,7 +9,7 @@ let transaction;
describe('Transaction Class', () => {
beforeEach(() => {
objectAPI = {
makeKeyString: (identifier) => utils.makeKeyString(identifier),
makeKeyString: (identifier) => makeKeyString(identifier),
save: () => Promise.resolve(true),
mutate: (object, prop, value) => {
object[prop] = value;
@@ -18,7 +18,7 @@ describe('Transaction Class', () => {
},
refresh: (object) => Promise.resolve(object),
areIdsEqual: (...identifiers) => {
return identifiers.map(utils.parseKeyString).every((identifier) => {
return identifiers.map(parseKeyString).every((identifier) => {
return (
identifier === identifiers[0] ||
(identifier.namespace === identifiers[0].namespace &&

View File

@@ -24,7 +24,7 @@
* Utility for checking if a thing is an Open MCT Identifier.
* @private
*/
function isIdentifier(thing) {
export function isIdentifier(thing) {
return (
typeof thing === 'object' &&
Object.prototype.hasOwnProperty.call(thing, 'key') &&
@@ -36,7 +36,7 @@ function isIdentifier(thing) {
* Utility for checking if a thing is a key string. Not perfect.
* @private
*/
function isKeyString(thing) {
export function isKeyString(thing) {
return typeof thing === 'string';
}
@@ -49,7 +49,7 @@ function isKeyString(thing) {
* @param keyString
* @returns identifier
*/
function parseKeyString(keyString) {
export function parseKeyString(keyString) {
if (isIdentifier(keyString)) {
return keyString;
}
@@ -86,7 +86,7 @@ function parseKeyString(keyString) {
* @param identifier
* @returns keyString
*/
function makeKeyString(identifier) {
export function makeKeyString(identifier) {
if (!identifier) {
throw new Error('Cannot make key string from null identifier');
}
@@ -112,7 +112,7 @@ function makeKeyString(identifier) {
* @param domainObject
* @returns oldFormatModel
*/
function toOldFormat(model) {
export function toOldFormat(model) {
model = JSON.parse(JSON.stringify(model));
delete model.identifier;
if (model.composition) {
@@ -131,7 +131,7 @@ function toOldFormat(model) {
* @param keyString
* @returns domainObject
*/
function toNewFormat(model, keyString) {
export function toNewFormat(model, keyString) {
model = JSON.parse(JSON.stringify(model));
model.identifier = parseKeyString(keyString);
if (model.composition) {
@@ -148,7 +148,7 @@ function toNewFormat(model, keyString) {
* @param otherIdentifier
* @returns Boolean true if identifiers are equal.
*/
function identifierEquals(a, b) {
export function identifierEquals(a, b) {
return a.key === b.key && a.namespace === b.namespace;
}
@@ -160,23 +160,12 @@ function identifierEquals(a, b) {
* @param otherDomainOBject
* @returns Boolean true if objects are equal.
*/
function objectEquals(a, b) {
export function objectEquals(a, b) {
return identifierEquals(a.identifier, b.identifier);
}
function refresh(oldObject, newObject) {
export function refresh(oldObject, newObject) {
let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));
deleted.forEach((propertyName) => delete oldObject[propertyName]);
Object.assign(oldObject, newObject);
}
export default {
isIdentifier: isIdentifier,
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,
parseKeyString: parseKeyString,
equals: objectEquals,
identifierEquals: identifierEquals,
refresh: refresh
};

View File

@@ -1,4 +1,4 @@
import objectUtils from 'objectUtils';
import { makeKeyString, parseKeyString, toNewFormat, toOldFormat } from 'objectUtils';
describe('objectUtils', function () {
describe('keyString util', function () {
@@ -31,27 +31,27 @@ describe('objectUtils', function () {
Object.keys(EXPECTATIONS).forEach(function (keyString) {
it('parses "' + keyString + '".', function () {
expect(objectUtils.parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);
expect(parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);
});
it('parses and re-encodes "' + keyString + '"', function () {
const identifier = objectUtils.parseKeyString(keyString);
expect(objectUtils.makeKeyString(identifier)).toEqual(keyString);
const identifier = parseKeyString(keyString);
expect(makeKeyString(identifier)).toEqual(keyString);
});
it('is idempotent for "' + keyString + '".', function () {
const identifier = objectUtils.parseKeyString(keyString);
let again = objectUtils.parseKeyString(identifier);
const identifier = parseKeyString(keyString);
let again = parseKeyString(identifier);
expect(identifier).toEqual(again);
again = objectUtils.parseKeyString(again);
again = objectUtils.parseKeyString(again);
again = parseKeyString(again);
again = parseKeyString(again);
expect(identifier).toEqual(again);
let againKeyString = objectUtils.makeKeyString(again);
let againKeyString = makeKeyString(again);
expect(againKeyString).toEqual(keyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = makeKeyString(againKeyString);
againKeyString = makeKeyString(againKeyString);
againKeyString = makeKeyString(againKeyString);
expect(againKeyString).toEqual(keyString);
});
});
@@ -60,7 +60,7 @@ describe('objectUtils', function () {
describe('old object conversions', function () {
it('translate ids', function () {
expect(
objectUtils.toNewFormat(
toNewFormat(
{
prop: 'someValue'
},
@@ -77,7 +77,7 @@ describe('objectUtils', function () {
it('translates composition', function () {
expect(
objectUtils.toNewFormat(
toNewFormat(
{
prop: 'someValue',
composition: ['anotherObjectId', 'scratch:anotherObjectId']
@@ -107,7 +107,7 @@ describe('objectUtils', function () {
describe('new object conversions', function () {
it('removes ids', function () {
expect(
objectUtils.toOldFormat({
toOldFormat({
prop: 'someValue',
identifier: {
namespace: '',
@@ -121,7 +121,7 @@ describe('objectUtils', function () {
it('translates composition', function () {
expect(
objectUtils.toOldFormat({
toOldFormat({
prop: 'someValue',
composition: [
{

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectUtils from 'objectUtils';
import { makeKeyString } from 'objectUtils';
import CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter.js';
import BatchingWebSocket from './BatchingWebSocket.js';
@@ -429,7 +429,7 @@ export default class TelemetryAPI {
this.#subscribeCache = {};
}
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const keyString = makeKeyString(domainObject.identifier);
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;
// Override the requested strategy with the strategy supported by the provider
const optionsWithSupportedStrategy = {
@@ -541,7 +541,7 @@ export default class TelemetryAPI {
this.stalenessSubscriberCache = {};
}
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const keyString = makeKeyString(domainObject.identifier);
let stalenessSubscriber = this.stalenessSubscriberCache[keyString];
if (!stalenessSubscriber) {
@@ -600,7 +600,7 @@ export default class TelemetryAPI {
this.limitsSubscribeCache = {};
}
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const keyString = makeKeyString(domainObject.identifier);
let subscriber = this.limitsSubscribeCache[keyString];
if (!subscriber) {

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectUtils from 'objectUtils';
import { parseKeyString } from 'objectUtils';
import { filter__proto__ } from 'utils/sanitization';
import { v4 as uuid } from 'uuid';
@@ -158,7 +158,7 @@ export default class ImportAsJSONAction {
key: uuid()
};
const oldId = objectUtils.parseKeyString(domainObjectId);
const oldId = parseKeyString(domainObjectId);
tree = this._rewriteId(oldId, newId, tree);
}, this);

View File

@@ -23,6 +23,8 @@
<template>
<li
draggable="true"
:aria-label="`${elementObject.name} Element Item`"
:aria-grabbed="hover"
@dragstart="emitDragStartEvent"
@dragenter="onDragenter"
@dragover.prevent
@@ -76,16 +78,19 @@ export default {
},
emits: ['drop-custom', 'dragstart-custom'],
data() {
const isAlias =
this.elementObject.location !==
this.openmct.objects.makeKeyString(this.domainObject.identifier);
return {
contextClickActive: false,
hover: false,
isAlias
hover: false
};
},
computed: {
isAlias() {
return (
this.elementObject.location !==
this.openmct.objects.makeKeyString(this.domainObject.identifier)
);
}
},
methods: {
emitDropEvent(event) {
this.$emit('drop-custom', event);

View File

@@ -30,7 +30,7 @@
/>
<div class="c-elements-pool__elements">
<ul
v-if="hasElements"
v-show="hasElements"
id="inspector-elements-tree"
class="c-tree c-elements-pool__tree js-elements-pool__tree"
>
@@ -63,7 +63,7 @@
></li>
</element-item-group>
</ul>
<div v-if="!hasElements">No contained elements</div>
<div v-show="!hasElements">No contained elements</div>
</div>
</div>
</template>
@@ -169,7 +169,8 @@ export default {
setYAxisIds() {
const configId = this.openmct.objects.makeKeyString(this.parentObject.identifier);
this.config = configStore.get(configId);
this.yAxes = [];
// Clear the yAxes array and repopulate it with the current YAxis elements
this.yAxes.splice(0);
this.yAxes.push({
id: this.config.yAxis.id,
elements: this.parentObject.configuration.series.filter(
@@ -207,7 +208,7 @@ export default {
}
// Store the element in the cache and set its yAxisId
this.elementsCache[keyString] = JSON.parse(JSON.stringify(element));
this.elementsCache[keyString] = element;
if (this.elementsCache[keyString].yAxisId !== yAxisId) {
// Mutate the YAxisId on the domainObject itself
this.updateCacheAndMutate(element, yAxisId);
@@ -276,7 +277,7 @@ export default {
yAxisId
});
this.composition.add(domainObject);
this.elementsCache[keyString] = JSON.parse(JSON.stringify(domainObject));
this.elementsCache[keyString] = domainObject;
}
this.elementsCache[keyString].yAxisId = yAxisId;

View File

@@ -1,4 +1,4 @@
import objectUtils from 'objectUtils';
import { makeKeyString } from 'objectUtils';
const NOTEBOOK_LOCAL_STORAGE = 'notebook-storage';
let currentNotebookObjectIdentifier = null;
@@ -22,8 +22,7 @@ function defaultNotebookObjectChanged(newDomainObject) {
function observeDefaultNotebookObject(openmct, notebookStorage, domainObject) {
if (
currentNotebookObjectIdentifier &&
objectUtils.makeKeyString(currentNotebookObjectIdentifier) ===
objectUtils.makeKeyString(notebookStorage.identifier)
makeKeyString(currentNotebookObjectIdentifier) === makeKeyString(notebookStorage.identifier)
) {
return;
}

View File

@@ -26,7 +26,7 @@
* rootIdentifier, and rewrites all child object identifiers so that they
* exist in the same namespace as the rootIdentifier.
*/
import objectUtils from 'objectUtils';
import { makeKeyString, parseKeyString, toNewFormat } from 'objectUtils';
class StaticModelProvider {
constructor(importData, rootIdentifier) {
@@ -38,7 +38,7 @@ class StaticModelProvider {
* Standard "Get".
*/
get(identifier) {
const keyString = objectUtils.makeKeyString(identifier);
const keyString = makeKeyString(identifier);
if (this.objectMap[keyString]) {
return this.objectMap[keyString];
}
@@ -49,7 +49,7 @@ class StaticModelProvider {
parseObjectLeaf(objectLeaf, idMap, newRootNamespace, oldRootNamespace) {
Object.keys(objectLeaf).forEach((nodeKey) => {
if (idMap.get(nodeKey)) {
const newIdentifier = objectUtils.makeKeyString({
const newIdentifier = makeKeyString({
namespace: newRootNamespace,
key: idMap.get(nodeKey)
});
@@ -104,7 +104,7 @@ class StaticModelProvider {
let mappedLeafValue;
if (oldRootNamespace) {
mappedLeafValue = idMap.get(
objectUtils.makeKeyString({
makeKeyString({
namespace: oldRootNamespace,
key: leafValue
})
@@ -125,7 +125,7 @@ class StaticModelProvider {
return null;
}
const newLocationIdentifier = objectUtils.makeKeyString({
const newLocationIdentifier = makeKeyString({
namespace: newRootNamespace,
key: mappedLeafValue
});
@@ -134,7 +134,7 @@ class StaticModelProvider {
} else {
const mappedLeafValue = idMap.get(leafValue);
if (mappedLeafValue) {
const newIdentifier = objectUtils.makeKeyString({
const newIdentifier = makeKeyString({
namespace: newRootNamespace,
key: mappedLeafValue
});
@@ -147,7 +147,7 @@ class StaticModelProvider {
}
rewriteObjectIdentifiers(importData, rootIdentifier) {
const { namespace: oldRootNamespace } = objectUtils.parseKeyString(importData.rootId);
const { namespace: oldRootNamespace } = parseKeyString(importData.rootId);
const { namespace: newRootNamespace } = rootIdentifier;
const idMap = new Map();
const objectTree = importData.openmct;
@@ -172,7 +172,7 @@ class StaticModelProvider {
*/
convertToNewObjects(oldObjectMap) {
return Object.keys(oldObjectMap).reduce(function (newObjectMap, key) {
newObjectMap[key] = objectUtils.toNewFormat(oldObjectMap[key], key);
newObjectMap[key] = toNewFormat(oldObjectMap[key], key);
return newObjectMap;
}, {});
@@ -180,7 +180,7 @@ class StaticModelProvider {
/* Set the root location correctly for a top-level object */
setRootLocation(objectMap, rootIdentifier) {
objectMap[objectUtils.makeKeyString(rootIdentifier)].location = 'ROOT';
objectMap[makeKeyString(rootIdentifier)].location = 'ROOT';
return objectMap;
}

View File

@@ -1,6 +1,6 @@
import EventEmitter from 'EventEmitter';
import _ from 'lodash';
import objectUtils from 'objectUtils';
import { makeKeyString } from 'objectUtils';
import ConditionEvaluator from './ConditionEvaluator.js';
@@ -119,7 +119,7 @@ ConditionManager.prototype.addGlobalPropertyType = function (key, type) {
* has completed and types have been parsed
*/
ConditionManager.prototype.parsePropertyTypes = function (object) {
const objectId = objectUtils.makeKeyString(object.identifier);
const objectId = makeKeyString(object.identifier);
this.telemetryTypesById[objectId] = {};
Object.values(this.telemetryMetadataById[objectId]).forEach(function (valueMetadata) {
@@ -182,7 +182,7 @@ ConditionManager.prototype.createNormalizedDatum = function (objId, telemetryDat
ConditionManager.prototype.onCompositionAdd = function (obj) {
let compositionKeys;
const telemetryAPI = this.openmct.telemetry;
const objId = objectUtils.makeKeyString(obj.identifier);
const objId = makeKeyString(obj.identifier);
let telemetryMetadata;
const self = this;
@@ -191,7 +191,7 @@ ConditionManager.prototype.onCompositionAdd = function (obj) {
self.telemetryMetadataById[objId] = {};
// FIXME: this should just update based on listener.
compositionKeys = self.domainObject.composition.map(objectUtils.makeKeyString);
compositionKeys = self.domainObject.composition.map(makeKeyString);
if (!compositionKeys.includes(objId)) {
self.domainObject.composition.push(obj.identifier);
}
@@ -245,7 +245,7 @@ ConditionManager.prototype.onCompositionAdd = function (obj) {
* @private
*/
ConditionManager.prototype.onCompositionRemove = function (identifier) {
const objectId = objectUtils.makeKeyString(identifier);
const objectId = makeKeyString(identifier);
// FIXME: this should just update by listener.
_.remove(this.domainObject.composition, function (id) {
return id.key === identifier.key && id.namespace === identifier.namespace;

View File

@@ -1,4 +1,4 @@
import objectUtils from 'objectUtils';
import { makeKeyString } from 'objectUtils';
import Select from './Select.js';
@@ -39,7 +39,7 @@ export default function ObjectSelect(config, manager, baseOptions) {
* @private
*/
function onCompositionAdd(obj) {
self.select.addOption(objectUtils.makeKeyString(obj.identifier), obj.name);
self.select.addOption(makeKeyString(obj.identifier), obj.name);
}
/**
@@ -77,7 +77,7 @@ export default function ObjectSelect(config, manager, baseOptions) {
*/
ObjectSelect.prototype.generateOptions = function () {
const items = Object.values(this.compositionObjs).map(function (obj) {
return [objectUtils.makeKeyString(obj.identifier), obj.name];
return [makeKeyString(obj.identifier), obj.name];
});
this.baseOptions.forEach(function (option, index) {
items.splice(index, 0, option);

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectUtils from 'objectUtils';
import { makeKeyString } from 'objectUtils';
import SummaryWidgetEvaluator from './SummaryWidgetEvaluator.js';
@@ -31,7 +31,7 @@ export default function EvaluatorPool(openmct) {
}
EvaluatorPool.prototype.get = function (domainObject) {
const objectId = objectUtils.makeKeyString(domainObject.identifier);
const objectId = makeKeyString(domainObject.identifier);
let poolEntry = this.byObjectId[objectId];
if (!poolEntry) {
poolEntry = {

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import _ from 'lodash';
import objectUtils from 'objectUtils';
import { makeKeyString } from 'objectUtils';
import eventHelpers from '../eventHelpers.js';
import SummaryWidgetRule from './SummaryWidgetRule.js';
@@ -113,7 +113,7 @@ SummaryWidgetEvaluator.prototype.updateRules = function (domainObject) {
};
SummaryWidgetEvaluator.prototype.addChild = function (childObject) {
const childId = objectUtils.makeKeyString(childObject.identifier);
const childId = makeKeyString(childObject.identifier);
const metadata = this.openmct.telemetry.getMetadata(childObject);
const formats = this.openmct.telemetry.getFormatMap(metadata);
@@ -126,7 +126,7 @@ SummaryWidgetEvaluator.prototype.addChild = function (childObject) {
};
SummaryWidgetEvaluator.prototype.removeChild = function (childObject) {
const childId = objectUtils.makeKeyString(childObject.identifier);
const childId = makeKeyString(childObject.identifier);
delete this.baseState[childId];
};

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectUtils from 'objectUtils';
import { parseKeyString } from 'objectUtils';
import TelemetryAverager from './TelemetryAverager.js';
@@ -43,7 +43,7 @@ MeanTelemetryProvider.prototype.supportsRequest =
MeanTelemetryProvider.prototype.subscribe = function (domainObject, callback) {
let wrappedUnsubscribe;
let unsubscribeCalled = false;
const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint);
const objectId = parseKeyString(domainObject.telemetryPoint);
const samples = domainObject.samples;
this.objectAPI
@@ -79,7 +79,7 @@ MeanTelemetryProvider.prototype.subscribeToAverage = function (domainObject, sam
};
MeanTelemetryProvider.prototype.request = function (domainObject, request) {
const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint);
const objectId = parseKeyString(domainObject.telemetryPoint);
const samples = domainObject.samples;
return this.objectAPI.get(objectId).then(
@@ -119,7 +119,7 @@ MeanTelemetryProvider.prototype.requestAverageTelemetry = function (
* @private
*/
MeanTelemetryProvider.prototype.getLinkedObject = function (domainObject) {
const objectId = objectUtils.parseKeyString(domainObject.telemetryPoint);
const objectId = parseKeyString(domainObject.telemetryPoint);
return this.objectAPI.get(objectId);
};

View File

@@ -85,8 +85,6 @@ import { getFilteredValues, getValidatedData, getValidatedGroups } from '../plan
import { SORT_ORDER_OPTIONS } from './constants.js';
import ExpandedViewItem from './ExpandedViewItem.vue';
const SCROLL_TIMEOUT = 10000;
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
const SAME_DAY_PRECISION_SECONDS = 'HH:mm:ss';
@@ -209,7 +207,6 @@ export default {
},
created() {
this.updateTimestamp = _.throttle(this.updateTimestamp, 1000);
this.deferAutoScroll = _.debounce(this.deferAutoScroll, 500);
this.setTimeContext();
this.timestamp = this.timeContext.now();
@@ -238,8 +235,6 @@ export default {
this.openmct.editor.on('isEditing', this.setEditState);
this.$el.parentElement.addEventListener('scroll', this.deferAutoScroll, true);
if (this.composition) {
this.composition.on('add', this.addToComposition);
this.composition.on('remove', this.removeItem);
@@ -272,11 +267,6 @@ export default {
this.openmct.editor.off('isEditing', this.setEditState);
this.stopFollowingTimeContext();
this.$el.parentElement?.removeEventListener('scroll', this.deferAutoScroll, true);
if (this.clearAutoScrollDisabledTimer) {
clearTimeout(this.clearAutoScrollDisabledTimer);
}
if (this.composition) {
this.composition.off('add', this.addToComposition);
this.composition.off('remove', this.removeItem);
@@ -429,8 +419,6 @@ export default {
const filteredItems = this.planActivities.filter(this.filterActivities);
const sortedItems = this.sortItems(filteredItems);
this.sortedItems = this.applyStyles(sortedItems);
//We need to wait for the next tick since we need the height of the row from the DOM
this.$nextTick(this.setScrollTop);
},
updateTimeStampAndListActivities(time) {
this.timestamp = time;
@@ -509,24 +497,13 @@ export default {
});
},
// Add activity classes, increase activity counts by type,
// set indices of the first occurrences of current and future activities - used for scrolling
styleActivity(activity, index) {
if (this.timestamp >= activity.start && this.timestamp <= activity.end) {
activity.cssClass = CURRENT_CSS_SUFFIX;
if (this.firstCurrentActivityIndex < 0) {
this.firstCurrentActivityIndex = index;
}
this.currentActivitiesCount = this.currentActivitiesCount + 1;
} else if (this.timestamp < activity.start) {
activity.cssClass = FUTURE_CSS_SUFFIX;
//the index of the first activity that's greater than the current timestamp
if (this.firstFutureActivityIndex < 0) {
this.firstFutureActivityIndex = index;
}
this.futureActivitiesCount = this.futureActivitiesCount + 1;
} else {
activity.cssClass = PAST_CSS_SUFFIX;
this.pastActivitiesCount = this.pastActivitiesCount + 1;
}
if (!activity.key) {
@@ -545,111 +522,7 @@ export default {
return activity;
},
applyStyles(activities) {
this.firstCurrentOrFutureActivityIndex = -1;
this.firstCurrentActivityIndex = -1;
this.firstFutureActivityIndex = -1;
this.currentActivitiesCount = 0;
this.pastActivitiesCount = 0;
this.futureActivitiesCount = 0;
const styledActivities = activities.map(this.styleActivity);
if (this.firstCurrentActivityIndex > -1) {
this.firstCurrentOrFutureActivityIndex = this.firstCurrentActivityIndex;
} else if (this.firstFutureActivityIndex > -1) {
this.firstCurrentOrFutureActivityIndex = this.firstFutureActivityIndex;
}
return styledActivities;
},
canAutoScroll() {
//this distinguishes between programmatic vs user-triggered scroll events
this.autoScrolled = this.dontAutoScroll !== true;
return this.autoScrolled;
},
resetScroll() {
if (this.canAutoScroll() === false) {
return;
}
this.firstCurrentOrFutureActivityIndex = -1;
this.pastActivitiesCount = 0;
this.currentActivitiesCount = 0;
this.futureActivitiesCount = 0;
this.$el.parentElement?.scrollTo({ top: 0 });
this.autoScrolled = false;
},
setScrollTop() {
//The view isn't ready yet
if (!this.$el.parentElement || this.isExpanded) {
return;
}
if (this.canAutoScroll() === false) {
return;
}
// See #7167 for scrolling algorithm
const scrollTop = this.calculateScrollOffset();
if (scrollTop === undefined) {
this.resetScroll();
} else {
this.$el.parentElement?.scrollTo({
top: scrollTop,
behavior: 'smooth'
});
this.autoScrolled = false;
}
},
calculateScrollOffset() {
let scrollTop;
//No scrolling necessary if no past events are present
if (this.pastActivitiesCount > 0) {
const row = this.$el.querySelector('.js-list-item');
const ROW_HEIGHT = row.getBoundingClientRect().height;
const maxViewableActivities =
Math.floor(this.$el.parentElement.getBoundingClientRect().height / ROW_HEIGHT) - 1;
const currentAndFutureActivities = this.currentActivitiesCount + this.futureActivitiesCount;
//If there is more viewable area than all current and future activities combined, then show some past events
const numberOfPastEventsToShow = maxViewableActivities - currentAndFutureActivities;
if (numberOfPastEventsToShow > 0) {
//some past events can be shown - get that scroll index
if (this.pastActivitiesCount > numberOfPastEventsToShow) {
scrollTop =
ROW_HEIGHT * (this.firstCurrentOrFutureActivityIndex + numberOfPastEventsToShow);
}
} else {
// only show current and future events
scrollTop = ROW_HEIGHT * this.firstCurrentOrFutureActivityIndex;
}
}
return scrollTop;
},
deferAutoScroll() {
//if this is not a user-triggered event, don't defer auto scrolling
if (this.autoScrolled) {
this.autoScrolled = false;
return;
}
this.dontAutoScroll = true;
const self = this;
if (this.clearAutoScrollDisabledTimer) {
clearTimeout(this.clearAutoScrollDisabledTimer);
}
this.clearAutoScrollDisabledTimer = setTimeout(() => {
self.dontAutoScroll = false;
self.setScrollTop();
}, SCROLL_TIMEOUT);
return activities.map(this.styleActivity);
},
setSort() {
const sortOrder = SORT_ORDER_OPTIONS[this.domainObject.configuration.sortOrderIndex];

View File

@@ -25,6 +25,7 @@
class="c-tree__item__label c-object-label"
:class="[statusClass]"
draggable="true"
:aria-label="ariaLabel"
@dragstart="dragStart"
@click="navigateOrPreview"
>
@@ -47,7 +48,10 @@
</template>
<script>
import { inject } from 'vue';
import tooltipHelpers from '../../api/tooltips/tooltipMixins.js';
import { useIsEditing } from '../../ui/composables/edit.js';
import ContextMenuGesture from '../mixins/context-menu-gesture.js';
import ObjectLink from '../mixins/object-link.js';
import PreviewAction from '../preview/PreviewAction.js';
@@ -76,6 +80,13 @@ export default {
}
}
},
setup() {
const openmct = inject('openmct');
const { isEditing } = useIsEditing(openmct);
return {
isEditing
};
},
data() {
return {
status: ''
@@ -92,6 +103,9 @@ export default {
},
statusClass() {
return this.status ? `is-status--${this.status}` : '';
},
ariaLabel() {
return `${this.isEditing ? 'Preview' : 'Navigate to'} ${this.domainObject.name} ${this.domainObject.type} Object`;
}
},
mounted() {

View File

@@ -33,7 +33,7 @@ import StyleRuleManager from '@/plugins/condition/StyleRuleManager';
import { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import objectUtils from '../../api/objects/object-utils.js';
import { objectEquals } from '../../api/objects/object-utils.js';
import VisibilityObserver from '../../utils/visibility/VisibilityObserver.js';
export default {
@@ -224,7 +224,7 @@ export default {
this.updateView(true);
},
reload(domainObjectToReload) {
if (objectUtils.equals(domainObjectToReload, this.domainObject)) {
if (objectEquals(domainObjectToReload, this.domainObject)) {
this.updateView(true);
this.initObjectStyles();
this.triggerStalenessSubscribe(this.domainObject);

View File

@@ -12,6 +12,9 @@
&:before {
// Mag glass icon
content: $glyph-icon-magnify;
body.mobile & { // Make search icon stand out in mobile
opacity: 1;
}
}
&__use-regex {
@@ -47,15 +50,26 @@
display: none;
order: 99;
padding: 1px 0;
body.mobile & {
display: block;
}
}
&.is-active {
body.mobile & { // In mobile, persist the expanded search bar instead of collapsing upon clicking away
background-color: rgba($colorHeadFg, 0.2) !important;
width: 50vw !important;
}
.c-search__use-regex {
margin-left: 0;
}
&:before {
width: 0;
body.mobile & {
width: auto;
}
}
input[type='text'],

View File

@@ -0,0 +1,43 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
import { ref } from 'vue';
import { useEventEmitter } from './event.js';
/**
* Provides a reactive `isEditing` property that reflects the current editing state of the
* application.
* @param {OpenMCT} openmct the Open MCT API
* @returns {{
* isEditing: import('vue').Ref<boolean>
* }}
*/
export function useIsEditing(openmct) {
const isEditing = ref(openmct.editor.isEditing());
useEventEmitter(openmct.editor, 'isEditing', (_isEditing) => {
isEditing.value = _isEditing;
});
return { isEditing };
}

View File

@@ -171,7 +171,7 @@
}
}
&__head,
//&__head,
&__pane-inspector {
body.mobile & {
display: none;
@@ -189,6 +189,18 @@
margin-bottom: $interiorMargin; // Needs some additional visual separation
}
&__head {
body.mobile & {
.c-create-button,
.c-create-menu,
.c-indicator,
.c-icon-button {
// Making status area visible, but hiding some widgets for mobile.
display: none;
}
}
}
body.mobile & .l-shell__main-view-browse-bar {
margin-left: $mobileMenuIconD; // Make room for the hamburger!
.c-button[class*='__actions__edit'] {
@@ -241,7 +253,20 @@
border-left: $brdr;
align-items: start;
$p: $interiorMarginSm;
padding-left: $p; padding-right: $p;
padding-left: $p;
padding-right: $p;
}
&__create-button,
&__app-logo {
flex: 0 0 auto;
}
&__create-button {
body.mobile & {
// Fixes weird gap in mobile status area
margin-right: 0px;
}
}
&__indicators {
@@ -254,7 +279,6 @@
height: 24px;
overflow: hidden;
}
}
/******************************* MAIN AREA */
@@ -355,14 +379,18 @@
.is-editing {
.l-shell__main-container {
$m: 3px;
box-shadow: $colorBodyBg 0 0 0 1px, $editUIAreaShdw;
box-shadow:
$colorBodyBg 0 0 0 1px,
$editUIAreaShdw;
margin-left: $m;
margin-right: $m;
top: $shellToolBarH + $shellMainBrowseBarH + $interiorMarginLg !important;
&[s-selected] {
// Provide a clearer selection context articulation for the main edit area
box-shadow: $colorBodyBg 0 0 0 1px, $editUIAreaShdwSelected;
box-shadow:
$colorBodyBg 0 0 0 1px,
$editUIAreaShdwSelected;
}
}
}

View File

@@ -27,6 +27,11 @@
aria-label="Search Results Dropdown"
class="c-gsearch__dropdown"
>
<button
aria-label="Close"
class="c-gsearch__results-close c-click-icon c-overlay__close-button icon-x"
@click="selectedResult"
></button>
<div class="c-gsearch__results" :class="{ 'search-finished': !searchLoading }">
<div
v-if="objectResults?.length"

View File

@@ -28,6 +28,19 @@
background: rgba($colorHeadFg, 0.2);
box-shadow: none;
flex: 1 1 auto;
body.mobile & {
@include phonePortrait() {
// This logic of expanding the search input upon click only happens in mobile portrait mode
background: $colorHeadBg;
width: 15%;
&:hover {
// When clicked, expand the search bar
background-color: rgba($colorHeadFg, 0.2);
width: 50vw;
transition: width 120ms;
}
}
}
}
}
@@ -40,6 +53,17 @@
max-height: 500px;
top: $formInputH;
z-index: 60;
body.mobile & {
// Makes search in mobile look less like an overlay, and more fullscreen.
position: absolute;
top: 25px;
left: -12px;
height: calc(100vh - 22px);
min-width: 100vw;
max-height: none;
border-radius: 0px;
box-shadow: none;
}
}
&__results,
@@ -55,6 +79,10 @@
> * + * {
margin-top: $interiorMarginLg;
}
body.mobile & {
// Add a margin to results so we have room for the close button
width: 93%;
}
}
&__results-section {
@@ -65,6 +93,7 @@
&__results-section-title {
@include propertiesHeader();
width: 95%;
}
&__result-pane-msg {
@@ -72,6 +101,22 @@
margin-top: $interiorMargin;
}
}
&__results-close {
// Close button that appears for mobile only
display: none;
body.mobile & {
display: block;
}
}
body.mobile & {
width: 50vw;
@include phonePortrait() {
// This logic only appears for a mobile portrait mode
width: 100%;
}
}
}
.c-gsearch-result {
@@ -113,9 +158,9 @@
}
.c-location {
//color: $colorBodyFg;
//font-size: 0.9em;
//opacity: 0.8;
color: $colorBodyFg;
font-size: 0.9em;
opacity: 0.8;
}
}

View File

@@ -22,6 +22,7 @@
import { isProxy, toRaw } from 'vue';
import { isIdentifier } from '@/api/objects/object-utils';
import StalenessUtils from '@/utils/staleness';
export default {
@@ -40,7 +41,9 @@ export default {
},
methods: {
getSubscriptionId(domainObject) {
return this.openmct?.objects.makeKeyString(domainObject.identifier);
// Only extract the identifier if it is not already an identifier
const identifier = isIdentifier(domainObject) ? domainObject : domainObject.identifier;
return this.openmct?.objects.makeKeyString(identifier);
},
setupClockChangedEvent(callback) {
this.setupClockChanged = true;
@@ -97,6 +100,7 @@ export default {
if (isProxy(domainObject)) {
domainObject = toRaw(domainObject);
}
const id = this.getSubscriptionId(domainObject);
if (!this.stalenessSubscription[id]) {
return;