From 9fbb695379cc695d2a76c23ea3f2566bcc54aa7c Mon Sep 17 00:00:00 2001 From: Jamie V Date: Sat, 4 Jun 2022 09:06:07 -0700 Subject: [PATCH] [Restricted Notebook] Creating new Restricted Notebook type (#5173) * added/removed status for locked, will not work with current one status per domain object setup * setting restricted right away based on nb type * added confirmation dialog for locking a page * Styling for restricted Notebook - Markup, CSS and content changes for lock button and locked message. - Removed "Note book Type" property from NotebookType.js. * have a version of entry template that has no listeners for locked items * cleaning up page and section components * making sure basic notebook stuff is installed at least once * updating data transfer values for locked page entries, fixing page and section selection from edits * adding locked flag to search result entries * fixing uneditable section/page names * cleaning up updateName function for page/section names * removing install of restricted notebook * updating confirmation dialog * updating tests for new export structur - New symbols glyph and SVG for the Shift Log. IMPORTANT: OVERRIDE ANY MERGE CONFLICTS WITH THIS COMMIT! * made create button items dynamic each time the button is clicked, this will pick up any new types added after the create menu is created * removing dynamic create menu list * found a way to add the plugin before openmct.start is called * making create items dynamic to include types added after openmct is started * more e2e tests for restricted notebook * updates from PR reviews, also fixed error in mct-tree thrown by not checking for an element * plain notebook tests * More testcase definition * actually removing notebook object to test * removing dupes * checking if agent exists before relying on it... it was breaking tests with errors * updating for new browser agent code * fixing linting errors Co-authored-by: Charles Hacskaylo Co-authored-by: unlikelyzero Co-authored-by: John Hill Co-authored-by: Andrew Henry Co-authored-by: Shefali Joshi --- .../notebook/addInitRestrictedNotebook.js | 30 ++ .../plugins/notebook/notebook.e2e.spec.js | 198 ++++++++++++ .../notebook/restrictedNotebook.e2e.spec.js | 264 ++++++++++++++++ e2e/tests/recycled_storage.json | 4 +- e2e/tests/visual/default.visual.spec.js | 1 - src/plugins/notebook/NotebookType.js | 88 ++++++ src/plugins/notebook/NotebookViewProvider.js | 72 +++++ src/plugins/notebook/components/Notebook.vue | 72 ++++- .../notebook/components/NotebookEmbed.vue | 24 +- .../notebook/components/NotebookEntry.vue | 39 ++- .../notebook/components/PageCollection.vue | 2 +- .../notebook/components/PageComponent.vue | 68 ++-- .../notebook/components/SearchResults.vue | 1 + .../notebook/components/SectionCollection.vue | 2 +- .../notebook/components/SectionComponent.vue | 49 ++- src/plugins/notebook/components/Sidebar.vue | 37 +-- src/plugins/notebook/components/sidebar.scss | 44 +-- .../monkeyPatchObjectAPIForNotebooks.js | 4 +- src/plugins/notebook/notebook-constants.js | 13 + src/plugins/notebook/plugin.js | 297 ++++++++---------- src/plugins/notebook/pluginSpec.js | 4 +- .../persistence/couch/CouchObjectProvider.js | 4 +- src/plugins/plugins.js | 3 +- src/styles/_constants-espresso.scss | 2 +- src/styles/fonts/Open-MCT-Symbols-16px.ttf | Bin 26020 -> 26020 bytes src/styles/fonts/Open-MCT-Symbols-16px.woff | Bin 26096 -> 26096 bytes src/styles/notebook.scss | 116 +++++-- src/ui/layout/CreateButton.vue | 42 +-- src/ui/layout/mct-tree.scss | 24 +- src/ui/layout/mct-tree.vue | 6 +- 30 files changed, 1175 insertions(+), 335 deletions(-) create mode 100644 e2e/tests/plugins/notebook/addInitRestrictedNotebook.js create mode 100644 e2e/tests/plugins/notebook/notebook.e2e.spec.js create mode 100644 e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js create mode 100644 src/plugins/notebook/NotebookType.js create mode 100644 src/plugins/notebook/NotebookViewProvider.js diff --git a/e2e/tests/plugins/notebook/addInitRestrictedNotebook.js b/e2e/tests/plugins/notebook/addInitRestrictedNotebook.js new file mode 100644 index 0000000000..dd303fb521 --- /dev/null +++ b/e2e/tests/plugins/notebook/addInitRestrictedNotebook.js @@ -0,0 +1,30 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +// this will be called from the test suite with +// await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') }); +// it will install the RestrictedNotebook since it is not installed by default + +document.addEventListener('DOMContentLoaded', () => { + const openmct = window.openmct; + openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME')); +}); diff --git a/e2e/tests/plugins/notebook/notebook.e2e.spec.js b/e2e/tests/plugins/notebook/notebook.e2e.spec.js new file mode 100644 index 0000000000..021946c875 --- /dev/null +++ b/e2e/tests/plugins/notebook/notebook.e2e.spec.js @@ -0,0 +1,198 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +This test suite is dedicated to tests which verify the basic operations surrounding Notebooks. +*/ + +const { test } = require('../../../fixtures'); + +test.describe('Notebook CRUD Operations', () => { + test.fixme('Can create a Notebook Object', async ({ page }) => { + //Create domain object + //Newly created notebook should have one Section and one page, 'Unnamed Section'/'Unnamed Page' + }); + test.fixme('Can update a Notebook Object', async ({ page }) => {}); + test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {}); + test.fixme('Can Delete a Notebook Object', async ({ page }) => { + // Other than non-persistible objects + }); +}); + +test.describe('Default Notebook', () => { + // General Default Notebook statements + // ## Useful commands: + // 1. - To check default notebook: + // `JSON.parse(localStorage.getItem('notebook-storage'));` + // 1. - Clear default notebook: + // `localStorage.setItem('notebook-storage', null);` + test.fixme('A newly created Notebook is automatically set as the default notebook if no other notebooks exist', async ({ page }) => { + //Create new notebook + //Verify Default Notebook Characteristics + }); + test.fixme('A newly created Notebook is automatically set as the default notebook if at least one other notebook exists', async ({ page }) => { + //Create new notebook A + //Create second notebook B + //Verify Non-Default Notebook A Characteristics + //Verify Default Notebook B Characteristics + }); + test.fixme('If a default notebook is deleted, the second most recent notebook becomes the default', async ({ page }) => { + //Create new notebook A + //Create second notebook B + //Delete Notebook B + //Verify Default Notebook A Characteristics + }); +}); + +test.describe('Notebook section tests', () => { + //The following test cases are associated with Notebook Sections + test.fixme('New sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => { + //Create new notebook A + //Add section + //Verify new section and new page details + }); + test.fixme('Section selection operations and associated behavior', async ({ page }) => { + //Create new notebook A + //Add Sections until 6 total with no default section/page + //Select 3rd section + //Delete 4th section + //3rd section is still selected + //Delete 3rd section + //1st section is selected + //Set 3rd section as default + //Delete 2nd section + //3rd section is still default + //Delete 3rd section + //1st is selected and there is no default notebook + }); +}); + +test.describe('Notebook page tests', () => { + //The following test cases are associated with Notebook Pages + test.fixme('Page selection operations and associated behavior', async ({ page }) => { + //Create new notebook A + //Delete existing Page + //New 'Unnamed Page' automatically created + //Create 6 total Pages without a default page + //Select 3rd + //Delete 3rd + //First is now selected + //Set 3rd as default + //Select 2nd page + //Delete 2nd page + //3rd (default) is now selected + //Set 3rd as default page + //Select 3rd (default) page + //Delete 3rd page + //First is now selected and there is no default notebook + }); +}); + +test.describe('Notebook search tests', () => { + test.fixme('Can search for a single result', async ({ page }) => {}); + test.fixme('Can search for many results', async ({ page }) => {}); + test.fixme('Can search for new and recently modified entries', async ({ page }) => {}); + test.fixme('Can search for section text', async ({ page }) => {}); + test.fixme('Can search for page text', async ({ page }) => {}); + test.fixme('Can search for entry text', async ({ page }) => {}); +}); + +test.describe('Notebook entry tests', () => { + test.fixme('When a new entry is created, it should be focused', async ({ page }) => {}); + test.fixme('When a telemetry object is dropped into a notebook, a new entry is created and it should be focused', async ({ page }) => { + // Drag and drop any telmetry object on 'drop object' + // new entry gets created with telemtry object + }); + test.fixme('When a telemetry object is dropped into a notebooks existing entry, it should be focused', async ({ page }) => { + // Drag and drop any telemetry object onto existing entry + // Entry updated with object and snapshot + }); + test.fixme('new entries persist through navigation events without save', async ({ page }) => {}); + test.fixme('previous and new entries can be deleted', async ({ page }) => {}); +}); + +test.describe('Snapshot Menu tests', () => { + test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => { + // There should be no default notebook + // Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);` + // refresh page + // Click on 'Notebook Snaphot Menu' + // 'save to Notebook Snapshots' should be only option there + }); + test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => { + // Create 2a notebooks + // Set Notebook A as Default + // Open Snapshot Menu and note that Notebook A is listed + // Close Snapshot Menu + // Set Default Notebook to Notebook B + // Open Snapshot Notebook and note that Notebook B is listed + // Select Default Notebook Option and verify that Snapshot is added to Notebook B + }); + test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => { + //Note this should be a visual test, too + // Create Telemetry object + // Create A notebook with many pages and sections. + // Set page and section defaults to be between first and last of many. i.e. 3 of 5 + // Navigate to Telemetry object + // Select Default Notebook Option and verify that Snapshot is added to Notebook A + // Verify Snapshot Details appear correctly + }); + test.fixme('Snapshots adjust time conductor', async ({ page }) => { + // Create Telemetry object + // Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded + // Embed Telemetry object into notebook + // Set Time Conductor to Local clock + // Click into embedded telemetry object and verify object appears with same fixed time from record + }); +}); + +test.describe('Snapshot Container tests', () => { + test.fixme('5 Snapshots can be added to a container', async ({ page }) => {}); + test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {}); + test.fixme('A snapshot can be Deleted from Container', async ({ page }) => {}); + test.fixme('A snapshot can be Previewed from Container', async ({ page }) => {}); + test.fixme('A snapshot Container can be open and closed', async ({ page }) => {}); + test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => { + //Create Notebook + //Create Telemetry Object + //From Telemetry Object, use 'save to Notebook Snapshots' + //Snapshots indicator should blink, click on it to view snapshots + //Navigate to Notebook + //Drag and Drop onto droppable area for new entry + //New Entry created with given snapshot added + //Snapshot removed from container? + }); + test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => { + //Create Notebook + //Create Telemetry Object + //From Telemetry Object, use 'save to Notebook Snapshots' + //Snapshots indicator should blink, click on it to view snapshots + //Navigate to Notebook + //Drag and Drop into exiting entry + //Existing Entry updated with given snapshot + //Snapshot removed from container? + }); + test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => { + //Add snapshot to container + //Verify PNG, JPG, and Annotate buttons work correctly + }); +}); diff --git a/e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js b/e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js new file mode 100644 index 0000000000..12e1cc3220 --- /dev/null +++ b/e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js @@ -0,0 +1,264 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +const { test } = require('../../../fixtures'); +const { expect } = require('@playwright/test'); +const path = require('path'); + +const TEST_TEXT = 'Testing text for entries.'; +const TEST_TEXT_NAME = 'Test Page'; +const CUSTOM_NAME = 'CUSTOM_NAME'; +const COMMIT_BUTTON_TEXT = 'button:has-text("Commit Entries")'; +const SINE_WAVE_GENERATOR = 'text=Unnamed Sine Wave Generator'; +const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area'; + +/** + * @param {import('@playwright/test').Page} page + */ +async function startAndAddNotebookObject(page) { + // eslint-disable-next-line no-undef + await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') }); + //Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + //Click the Create button + await page.click('button:has-text("Create")'); + // Click text=CUSTOME_NAME + await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also + // Click text=OK + await Promise.all([ + page.waitForNavigation({waitUntil: 'networkidle'}), + page.click('text=OK') + ]); + + return; +} + +/** + * @param {import('@playwright/test').Page} page + */ +async function enterTextEntry(page) { + // Click .c-notebook__drag-area + await page.locator(NOTEBOOK_DROP_AREA).click(); + + // enter text + await page.locator('div.c-ne__text').click(); + await page.locator('div.c-ne__text').fill(TEST_TEXT); + await page.locator('div.c-ne__text').press('Enter'); + + return; +} + +/** + * @param {import('@playwright/test').Page} page + */ +async function dragAndDropEmbed(page) { + // Click button:has-text("Create") + await page.locator('button:has-text("Create")').click(); + // Click li:has-text("Sine Wave Generator") + await page.locator('li:has-text("Sine Wave Generator")').click(); + // Click form[name="mctForm"] >> text=My Items + await page.locator('form[name="mctForm"] >> text=My Items').click(); + // Click text=OK + await page.locator('text=OK').click(); + // Click text=Open MCT My Items >> span >> nth=3 + await page.locator('text=Open MCT My Items >> span').nth(3).click(); + // Click text=Unnamed CUSTOM_NAME + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Unnamed CUSTOM_NAME').click() + ]); + + await page.dragAndDrop(SINE_WAVE_GENERATOR, NOTEBOOK_DROP_AREA); + + return; +} + +/** + * @param {import('@playwright/test').Page} page + */ +async function lockPage(page) { + const commitButton = page.locator(COMMIT_BUTTON_TEXT); + await commitButton.click(); + + // confirmation dialog click + await page.locator('text=Lock Page').click(); + + // waiting for mutation of locked page + await new Promise((resolve, reject) => { + setTimeout(resolve, 1000); + }); + + return; +} + +/** + * @param {import('@playwright/test').Page} page + */ +async function openContextMenuRestrictedNotebook(page) { + // Click text=Open MCT My Items (This expands the My Items folder to show it's chilren in the tree) + await page.locator('text=Open MCT My Items >> span').nth(3).click(); + + // Click a:has-text("Unnamed CUSTOM_NAME") + await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({ + button: 'right' + }); + + return; +} + +test.describe('Restricted Notebook', () => { + + test.beforeEach(async ({ page }) => { + await startAndAddNotebookObject(page); + }); + + test('Can be renamed', async ({ page }) => { + await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`); + }); + + test('Can be deleted if there are no locked pages', async ({ page }) => { + await openContextMenuRestrictedNotebook(page); + + const menuOptions = page.locator('.c-menu ul'); + await expect.soft(menuOptions).toContainText('Remove'); + + const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`); + + // notbook tree object exists + expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1); + + // Click text=Remove + await page.locator('text=Remove').click(); + // Click text=OK + await Promise.all([ + page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine?tc.mode=fixed&tc.startBound=1653671067340&tc.endBound=1653672867340&tc.timeSystem=utc&view=grid' }*/), + page.locator('text=OK').click() + ]); + + // has been deleted + expect.soft(await restrictedNotebookTreeObject.count()).toEqual(0); + }); + + test('Can be locked if at least one page has one entry', async ({ page }) => { + + await enterTextEntry(page); + + const commitButton = page.locator(COMMIT_BUTTON_TEXT); + expect.soft(await commitButton.count()).toEqual(1); + }); + +}); + +test.describe('Restricted Notebook with at least one entry and with the page locked', () => { + + test.beforeEach(async ({ page }) => { + await startAndAddNotebookObject(page); + await enterTextEntry(page); + await lockPage(page); + + // open sidebar + await page.locator('button.c-notebook__toggle-nav-button').click(); + }); + + test('Locked page should now be in a locked state', async ({ page }) => { + // main lock message on page + const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed'); + expect.soft(await lockMessage.count()).toEqual(1); + + // lock icon on page in sidebar + const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock'); + expect.soft(await pageLockIcon.count()).toEqual(1); + + // no way to remove a restricted notebook with a locked page + await openContextMenuRestrictedNotebook(page); + + const menuOptions = page.locator('.c-menu ul'); + + await expect.soft(menuOptions).not.toContainText('Remove'); + + }); + + test('Can still: add page, rename, add entry, delete unlocked pages', async ({ page }) => { + // Click text=Page Add >> button + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Page Add >> button').click() + ]); + // Click text=Unnamed Page >> nth=1 + await page.locator('text=Unnamed Page').nth(1).click(); + // Press a with modifiers + await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME); + + // expect to be able to rename unlocked pages + const newPageElement = page.locator(`text=${TEST_TEXT_NAME}`); + const newPageCount = await newPageElement.count(); + await newPageElement.press('Enter'); // exit contenteditable state + expect.soft(newPageCount).toEqual(1); + + // enter test text + await enterTextEntry(page); + + // expect new page to be lockable + const commitButton = page.locator(COMMIT_BUTTON_TEXT); + expect.soft(await commitButton.count()).toEqual(1); + + // Click text=Unnamed PageTest Page >> button + await page.locator('text=Unnamed PageTest Page >> button').click(); + // Click text=Delete Page + await page.locator('text=Delete Page').click(); + // Click text=Ok + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Ok').click() + ]); + + // deleted page, should no longer exist + const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`); + expect.soft(await deletedPageElement.count()).toEqual(0); + }); +}); + +test.describe('Restricted Notebook with a page locked and with an embed', () => { + + test.beforeEach(async ({ page }) => { + await startAndAddNotebookObject(page); + await dragAndDropEmbed(page); + }); + + test('Allows embeds to be deleted if page unlocked', async ({ page }) => { + // Click .c-ne__embed__name .c-popup-menu-button + await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu + + const embedMenu = page.locator('body >> .c-menu'); + await expect.soft(embedMenu).toContainText('Remove This Embed'); + }); + + test('Disallows embeds to be deleted if page locked', async ({ page }) => { + await lockPage(page); + // Click .c-ne__embed__name .c-popup-menu-button + await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu + + const embedMenu = page.locator('body >> .c-menu'); + await expect.soft(embedMenu).not.toContainText('Remove This Embed'); + }); + +}); diff --git a/e2e/tests/recycled_storage.json b/e2e/tests/recycled_storage.json index 86c3f906bc..c20b7ee73d 100644 --- a/e2e/tests/recycled_storage.json +++ b/e2e/tests/recycled_storage.json @@ -6,11 +6,11 @@ "localStorage": [ { "name": "tcHistory", - "value": "{\"utc\":[{\"start\":1651513945533,\"end\":1651515745533}]}" + "value": "{\"utc\":[{\"start\":1652301954635,\"end\":1652303754635}]}" }, { "name": "mct", - "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1651515746374,\"modified\":1651515746374},\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"e35a066b-eb0e-4b05-a4c9-cc31dc202572\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1651515746373,\"location\":\"mine\",\"persisted\":1651515746373}}" + "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1652303756008,\"modified\":1652303756007},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002}}" }, { "name": "mct-tree-expanded", diff --git a/e2e/tests/visual/default.visual.spec.js b/e2e/tests/visual/default.visual.spec.js index dd88b893ad..b069f2e95f 100644 --- a/e2e/tests/visual/default.visual.spec.js +++ b/e2e/tests/visual/default.visual.spec.js @@ -192,7 +192,6 @@ test('Visual - Save Successful Banner', async ({ page }) => { //Wait until Save Banner is gone await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); await percySnapshot(page, 'Banner message gone'); - }); test('Visual - Display Layout Icon is correct', async ({ page }) => { diff --git a/src/plugins/notebook/NotebookType.js b/src/plugins/notebook/NotebookType.js new file mode 100644 index 0000000000..d112502b79 --- /dev/null +++ b/src/plugins/notebook/NotebookType.js @@ -0,0 +1,88 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import { IMAGE_MIGRATION_VER } from '../notebook/utils/notebook-migration'; + +export default class NotebookType { + constructor(name, description, icon) { + this.name = name; + this.description = description; + this.cssClass = icon; + this.creatable = true; + this.form = [ + { + key: 'defaultSort', + name: 'Entry Sorting', + control: 'select', + options: [ + { + name: 'Newest First', + value: "newest" + }, + { + name: 'Oldest First', + value: "oldest" + } + ], + cssClass: 'l-inline', + property: [ + "configuration", + "defaultSort" + ] + }, + { + key: 'sectionTitle', + name: 'Section Title', + control: 'textfield', + cssClass: 'l-inline', + required: true, + property: [ + "configuration", + "sectionTitle" + ] + }, + { + key: 'pageTitle', + name: 'Page Title', + control: 'textfield', + cssClass: 'l-inline', + required: true, + property: [ + "configuration", + "pageTitle" + ] + } + ]; + } + + initialize(domainObject) { + domainObject.configuration = { + defaultSort: 'oldest', + entries: {}, + imageMigrationVer: IMAGE_MIGRATION_VER, + pageTitle: 'Page', + sections: [], + sectionTitle: 'Section', + type: 'General' + }; + } +} diff --git a/src/plugins/notebook/NotebookViewProvider.js b/src/plugins/notebook/NotebookViewProvider.js new file mode 100644 index 0000000000..66617789c7 --- /dev/null +++ b/src/plugins/notebook/NotebookViewProvider.js @@ -0,0 +1,72 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +import Vue from 'vue'; +import Notebook from './components/Notebook.vue'; +import Agent from '@/utils/agent/Agent'; + +export default class NotebookViewProvider { + constructor(openmct, name, key, type, cssClass, snapshotContainer) { + this.openmct = openmct; + this.key = key; + this.name = `${name} View`; + this.type = type; + this.cssClass = cssClass; + this.snapshotContainer = snapshotContainer; + } + + canView(domainObject) { + return domainObject.type === this.type; + } + + view(domainObject) { + let component; + let openmct = this.openmct; + let snapshotContainer = this.snapshotContainer; + let agent = new Agent(window); + + return { + show(container) { + component = new Vue({ + el: container, + components: { + Notebook + }, + provide: { + openmct, + snapshotContainer, + agent + }, + data() { + return { + domainObject + }; + }, + template: '' + }); + }, + destroy() { + component.$destroy(); + } + }; + } +} diff --git a/src/plugins/notebook/components/Notebook.vue b/src/plugins/notebook/components/Notebook.vue index 0fbc83f68a..9dbead306b 100644 --- a/src/plugins/notebook/components/Notebook.vue +++ b/src/plugins/notebook/components/Notebook.vue @@ -21,7 +21,10 @@ *****************************************************************************/ diff --git a/src/plugins/notebook/components/sidebar.scss b/src/plugins/notebook/components/sidebar.scss index e5c8a8cd00..91e8b16996 100644 --- a/src/plugins/notebook/components/sidebar.scss +++ b/src/plugins/notebook/components/sidebar.scss @@ -3,19 +3,18 @@ background: $sideBarBg; display: flex; justify-content: stretch; - max-width: 300px; + max-width: 600px; &.c-drawer--push.is-expanded { margin-right: $interiorMargin; - width: 50%; + width: 30%; } &.c-drawer--overlays.is-expanded { width: 95%; } - > * { - // Hardcoded for two columns + &__pane { background: $sideBarBg; display: flex; flex: 1 1 50%; @@ -31,32 +30,30 @@ } } - &__pane { - > * + * { margin-top: $interiorMargin; } + &__right-edge { + flex: 0 0 auto; + padding: $interiorMarginSm; } &__header-w { - // Wraps header, used for page pane with collapse button + // Wraps header, used for page pane with collapse buttons display: flex; flex: 0 0 auto; background: $sideBarHeaderBg; align-items: center; - - .c-icon-button { - font-size: 0.8em; - color: $colorBodyFg; - } } &__header { color: $sideBarHeaderFg; display: flex; + align-items: center; flex: 1 1 auto; - padding: $interiorMargin; + padding: $interiorMarginSm $interiorMargin; text-transform: uppercase; - > * { + &-label { @include ellipsize(); + flex: 1 1 auto; } } @@ -66,17 +63,8 @@ flex-direction: column; flex: 1 1 auto; - > * { - margin: auto $interiorMargin $interiorMargin $interiorMargin; - - &:first-child { - border-bottom: 1px solid $colorInteriorBorder; - flex: 0 0 auto; - } - - + * { - margin-top: $interiorMargin; - } + > * + * { + margin-top: $interiorMargin; } } @@ -87,12 +75,6 @@ padding: auto $interiorMargin; } - .c-list-button { - .c-button { - font-size: 0.8em; - } - } - .c-list__item { @include hover() { [class*="__menu-indicator"] { diff --git a/src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js b/src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js index e4c22db376..8222b5effe 100644 --- a/src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js +++ b/src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js @@ -1,10 +1,10 @@ -import {NOTEBOOK_TYPE} from './notebook-constants'; +import { isNotebookType } from './notebook-constants'; export default function (openmct) { const apiSave = openmct.objects.save.bind(openmct.objects); openmct.objects.save = async (domainObject) => { - if (domainObject.type !== NOTEBOOK_TYPE) { + if (!isNotebookType(domainObject)) { return apiSave(domainObject); } diff --git a/src/plugins/notebook/notebook-constants.js b/src/plugins/notebook/notebook-constants.js index d5163f797b..6f2e5af3e8 100644 --- a/src/plugins/notebook/notebook-constants.js +++ b/src/plugins/notebook/notebook-constants.js @@ -1,5 +1,18 @@ export const NOTEBOOK_TYPE = 'notebook'; +export const RESTRICTED_NOTEBOOK_TYPE = 'restricted-notebook'; export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED'; export const NOTEBOOK_DEFAULT = 'DEFAULT'; export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT'; export const NOTEBOOK_VIEW_TYPE = 'notebook-vue'; +export const RESTRICTED_NOTEBOOK_VIEW_TYPE = 'restricted-notebook-vue'; +export const NOTEBOOK_INSTALLED_KEY = '_NOTEBOOK_PLUGIN_INSTALLED'; +export const RESTRICTED_NOTEBOOK_INSTALLED_KEY = '_RESTRICTED_NOTEBOOK_PLUGIN_INSTALLED'; + +// these only deals with constants, figured this could skip going into a utils file +export function isNotebookType(domainObject) { + return [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE].includes(domainObject.type); +} + +export function isNotebookViewType(view) { + return [NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_VIEW_TYPE].includes(view); +} diff --git a/src/plugins/notebook/plugin.js b/src/plugins/notebook/plugin.js index be02262972..7fcabbe747 100644 --- a/src/plugins/notebook/plugin.js +++ b/src/plugins/notebook/plugin.js @@ -1,178 +1,155 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + import CopyToNotebookAction from './actions/CopyToNotebookAction'; -import Notebook from './components/Notebook.vue'; import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue'; +import NotebookViewProvider from './NotebookViewProvider'; +import NotebookType from './NotebookType'; import SnapshotContainer from './snapshot-container'; import monkeyPatchObjectAPIForNotebooks from './monkeyPatchObjectAPIForNotebooks.js'; -import { notebookImageMigration, IMAGE_MIGRATION_VER } from '../notebook/utils/notebook-migration'; -import { NOTEBOOK_TYPE } from './notebook-constants'; +import { notebookImageMigration } from '../notebook/utils/notebook-migration'; +import { + NOTEBOOK_TYPE, + RESTRICTED_NOTEBOOK_TYPE, + NOTEBOOK_VIEW_TYPE, + RESTRICTED_NOTEBOOK_VIEW_TYPE, + NOTEBOOK_INSTALLED_KEY, + RESTRICTED_NOTEBOOK_INSTALLED_KEY +} from './notebook-constants'; import Vue from 'vue'; -import Agent from '@/utils/agent/Agent'; -export default function NotebookPlugin() { +let notebookSnapshotContainer; +function getSnapshotContainer(openmct) { + if (!notebookSnapshotContainer) { + notebookSnapshotContainer = new SnapshotContainer(openmct); + } + + return notebookSnapshotContainer; +} + +function addLegacyNotebookGetInterceptor(openmct) { + openmct.objects.addGetInterceptor({ + appliesTo: (identifier, domainObject) => { + return domainObject && domainObject.type === NOTEBOOK_TYPE; + }, + invoke: (identifier, domainObject) => { + notebookImageMigration(openmct, domainObject); + + return domainObject; + } + }); +} + +function installBaseNotebookFunctionality(openmct) { + // only need to do this once + if (openmct[NOTEBOOK_INSTALLED_KEY] || openmct[RESTRICTED_NOTEBOOK_INSTALLED_KEY]) { + return; + } + + const snapshotContainer = getSnapshotContainer(openmct); + const notebookSnapshotImageType = { + name: 'Notebook Snapshot Image Storage', + description: 'Notebook Snapshot Image Storage object', + creatable: false, + initialize: domainObject => { + domainObject.configuration = { + fullSizeImageURL: undefined, + thumbnailImageURL: undefined + }; + } + }; + openmct.types.addType('notebookSnapshotImage', notebookSnapshotImageType); + openmct.actions.register(new CopyToNotebookAction(openmct)); + + const notebookSnapshotIndicator = new Vue ({ + components: { + NotebookSnapshotIndicator + }, + provide: { + openmct, + snapshotContainer + }, + template: '' + }); + const indicator = { + element: notebookSnapshotIndicator.$mount().$el, + key: 'notebook-snapshot-indicator', + priority: openmct.priority.DEFAULT + }; + + openmct.indicators.add(indicator); + + monkeyPatchObjectAPIForNotebooks(openmct); +} + +function NotebookPlugin(name = 'Notebook') { return function install(openmct) { - if (openmct._NOTEBOOK_PLUGIN_INSTALLED) { + if (openmct[NOTEBOOK_INSTALLED_KEY]) { return; - } else { - openmct._NOTEBOOK_PLUGIN_INSTALLED = true; } - openmct.actions.register(new CopyToNotebookAction(openmct)); - const agent = new Agent(window); - const notebookType = { - name: 'Notebook', - description: 'Create and save timestamped notes with embedded object snapshots.', - creatable: true, - cssClass: 'icon-notebook', - initialize: domainObject => { - domainObject.configuration = { - defaultSort: 'oldest', - entries: {}, - imageMigrationVer: IMAGE_MIGRATION_VER, - pageTitle: 'Page', - sections: [], - sectionTitle: 'Section', - type: 'General' - }; - }, - form: [ - { - key: 'defaultSort', - name: 'Entry Sorting', - control: 'select', - options: [ - { - name: 'Newest First', - value: "newest" - }, - { - name: 'Oldest First', - value: "oldest" - } - ], - cssClass: 'l-inline', - property: [ - "configuration", - "defaultSort" - ] - }, - { - key: 'type', - name: 'Note book Type', - control: 'textfield', - cssClass: 'l-inline', - property: [ - "configuration", - "type" - ] - }, - { - key: 'sectionTitle', - name: 'Section Title', - control: 'textfield', - cssClass: 'l-inline', - required: true, - property: [ - "configuration", - "sectionTitle" - ] - }, - { - key: 'pageTitle', - name: 'Page Title', - control: 'textfield', - cssClass: 'l-inline', - required: true, - property: [ - "configuration", - "pageTitle" - ] - } - ] - }; + const icon = 'icon-notebook'; + const description = 'Create and save timestamped notes with embedded object snapshots.'; + const snapshotContainer = getSnapshotContainer(openmct); + + addLegacyNotebookGetInterceptor(openmct); + + const notebookType = new NotebookType(name, description, icon); openmct.types.addType(NOTEBOOK_TYPE, notebookType); - const notebookSnapshotImageType = { - name: 'Notebook Snapshot Image Storage', - description: 'Notebook Snapshot Image Storage object', - creatable: false, - initialize: domainObject => { - domainObject.configuration = { - fullSizeImageURL: undefined, - thumbnailImageURL: undefined - }; - } - }; - openmct.types.addType('notebookSnapshotImage', notebookSnapshotImageType); + const notebookView = new NotebookViewProvider(openmct, name, NOTEBOOK_VIEW_TYPE, NOTEBOOK_TYPE, icon, snapshotContainer); + openmct.objectViews.addProvider(notebookView); - const snapshotContainer = new SnapshotContainer(openmct); - const notebookSnapshotIndicator = new Vue ({ - components: { - NotebookSnapshotIndicator - }, - provide: { - openmct, - snapshotContainer - }, - template: '' - }); - const indicator = { - element: notebookSnapshotIndicator.$mount().$el, - key: 'notebook-snapshot-indicator', - priority: openmct.priority.DEFAULT - }; + installBaseNotebookFunctionality(openmct); - openmct.indicators.add(indicator); - - openmct.objectViews.addProvider({ - key: 'notebook-vue', - name: 'Notebook View', - cssClass: 'icon-notebook', - canView: function (domainObject) { - return domainObject.type === 'notebook'; - }, - view: function (domainObject) { - let component; - - return { - show(container) { - component = new Vue({ - el: container, - components: { - Notebook - }, - provide: { - agent, - openmct, - snapshotContainer - }, - data() { - return { - domainObject - }; - }, - template: '' - }); - }, - destroy() { - component.$destroy(); - } - }; - } - }); - - openmct.objects.addGetInterceptor({ - appliesTo: (identifier, domainObject) => { - return domainObject && domainObject.type === 'notebook'; - }, - invoke: (identifier, domainObject) => { - notebookImageMigration(openmct, domainObject); - - return domainObject; - } - }); - - monkeyPatchObjectAPIForNotebooks(openmct); + openmct[NOTEBOOK_INSTALLED_KEY] = true; }; } + +function RestrictedNotebookPlugin(name = 'Notebook Shift Log') { + return function install(openmct) { + if (openmct[RESTRICTED_NOTEBOOK_INSTALLED_KEY]) { + return; + } + + const icon = 'icon-notebook-shift-log'; + const description = 'Create and save timestamped notes with embedded object snapshots with the ability to commit and lock pages.'; + const snapshotContainer = getSnapshotContainer(openmct); + + const notebookType = new NotebookType(name, description, icon); + openmct.types.addType(RESTRICTED_NOTEBOOK_TYPE, notebookType); + + const notebookView = new NotebookViewProvider(openmct, name, RESTRICTED_NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_TYPE, icon, snapshotContainer); + openmct.objectViews.addProvider(notebookView); + + installBaseNotebookFunctionality(openmct); + + openmct[RESTRICTED_NOTEBOOK_INSTALLED_KEY] = true; + }; +} + +export { + NotebookPlugin, + RestrictedNotebookPlugin +}; diff --git a/src/plugins/notebook/pluginSpec.js b/src/plugins/notebook/pluginSpec.js index a2aec3a70d..ba83a4f4bc 100644 --- a/src/plugins/notebook/pluginSpec.js +++ b/src/plugins/notebook/pluginSpec.js @@ -21,7 +21,7 @@ *****************************************************************************/ import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing'; -import notebookPlugin from './plugin'; +import { NotebookPlugin } from './plugin'; import Vue from 'vue'; describe("Notebook plugin:", () => { @@ -55,7 +55,7 @@ describe("Notebook plugin:", () => { child = document.createElement('div'); element.appendChild(child); - openmct.install(notebookPlugin()); + openmct.install(NotebookPlugin()); originalAnnotations = openmct.annotation.getNotebookAnnotation; // eslint-disable-next-line require-await openmct.annotation.getNotebookAnnotation = async function () { diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js index e7ab40025c..37ff6198e8 100644 --- a/src/plugins/persistence/couch/CouchObjectProvider.js +++ b/src/plugins/persistence/couch/CouchObjectProvider.js @@ -22,7 +22,7 @@ import CouchDocument from "./CouchDocument"; import CouchObjectQueue from "./CouchObjectQueue"; -import { NOTEBOOK_TYPE } from '../../notebook/notebook-constants.js'; +import { isNotebookType } from '../../notebook/notebook-constants.js'; const REV = "_rev"; const ID = "_id"; @@ -185,7 +185,7 @@ class CouchObjectProvider { this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]); } - if (object.type === NOTEBOOK_TYPE) { + if (isNotebookType(object)) { //Temporary measure until object sync is supported for all object types //Always update notebook revision number because we have realtime sync, so always assume it's the latest. this.objectQueue[key].updateRevision(response[REV]); diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index e488a54da9..eb55cfa51e 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -186,7 +186,8 @@ define([ plugins.SummaryWidget = SummaryWidget; plugins.TelemetryMean = TelemetryMean; plugins.URLIndicator = URLIndicatorPlugin; - plugins.Notebook = Notebook.default; + plugins.Notebook = Notebook.NotebookPlugin; + plugins.RestrictedNotebook = Notebook.RestrictedNotebookPlugin; plugins.DisplayLayout = DisplayLayoutPlugin.default; plugins.FormActions = FormActions; plugins.FolderView = FolderView; diff --git a/src/styles/_constants-espresso.scss b/src/styles/_constants-espresso.scss index 28f813fc47..c8745b90a9 100644 --- a/src/styles/_constants-espresso.scss +++ b/src/styles/_constants-espresso.scss @@ -94,7 +94,7 @@ $shellPanePad: $interiorMargin, 7px; $drawerBg: lighten($colorBodyBg, 5%); $drawerFg: lighten($colorBodyFg, 5%); $sideBarBg: $drawerBg; -$sideBarHeaderBg: rgba($colorBodyFg, 0.2); +$sideBarHeaderBg: rgba($colorBodyFg, 0.1); $sideBarHeaderFg: rgba($colorBodyFg, 0.7); // Status colors, mainly used for messaging and item ancillary symbols diff --git a/src/styles/fonts/Open-MCT-Symbols-16px.ttf b/src/styles/fonts/Open-MCT-Symbols-16px.ttf index 94ab53538b6ee4ee72e83b1e7a1afc6b78958239..33008462f5479b6eff64970c694cc2693dd8cf96 100644 GIT binary patch delta 49 xcmZ2-nsLc##tDJUxw5(&Lu})v_e@$I&u{aUft!T^1n%vB6Aq&{uZjP`3IM&U5>@~J delta 49 xcmZ2-nsLc##tDJU8AqBohS@~J diff --git a/src/styles/fonts/Open-MCT-Symbols-16px.woff b/src/styles/fonts/Open-MCT-Symbols-16px.woff index 510d6e9a9b85dae9d62daa494a38a0bdad0131be..4fbc1d1e40e7dd5679df42c860b8b757836df6c9 100644 GIT binary patch delta 49 xcmexxn(@PF#tFU5xw5(&C-}xo@0qkbp5Nvx12+o;2;AHMCLBg@{t^F!6#(yt6T$!h delta 49 xcmexxn(@PF#tFU58AqBoPVkMFKI;5Cp5Nvx12+o;2;AFyfC)x#{t^F!6#xs)6T$!h diff --git a/src/styles/notebook.scss b/src/styles/notebook.scss index f7b7b7eadb..09d74e0570 100644 --- a/src/styles/notebook.scss +++ b/src/styles/notebook.scss @@ -45,13 +45,13 @@ &__nav { flex: 0 0 auto; + * { overflow: hidden; } } .c-sidebar { - background: $sideBarBg; .c-sidebar__pane { flex-basis: 50%; } @@ -75,8 +75,10 @@ flex: 1 1 auto; flex-direction: column; width: 100%; + > * { flex: 0 0 auto; + + * { margin-top: $interiorMargin; } @@ -111,18 +113,23 @@ flex: 1 1 auto; } + + &__page-locked, &__drag-area { - // TODO: recast this element to use c-drop-hint element - background: rgba($colorKey, 0.1); - border: 1px dashed rgba($colorKey, 0.7); border-radius: $controlCr; - color: rgba($colorBodyFg, 0.7); padding: 10px; - cursor: pointer; &:before { margin-right: 7px !important; } + } + + &__drag-area { + background: rgba($colorKey, 0.1); + border: 1px dashed rgba($colorKey, 0.7); + color: $colorKey; + cursor: pointer; + justify-content: center; [class*="__label"] { font-style: italic; @@ -131,7 +138,7 @@ &:hover { background: rgba($colorKey, 0.2); - color: $colorBodyFg; + //color: $colorBodyFg; } &.drag-active, @@ -151,6 +158,7 @@ display: flex; flex-wrap: wrap; // Allows wrapping in mobile portrait and narrow placements line-height: 220%; + > * { flex: 0 0 auto; } @@ -162,9 +170,11 @@ overflow: hidden; white-space: nowrap; font-size: $headerFontSize; + > * { // Section flex: 0 0 auto; + + * { // Page display: inline; @@ -188,6 +198,13 @@ [class*="__entry"] + [class*="__entry"] { margin-top: $interiorMarginSm; } + + .commit-button { + @include cButton(); + position: absolute; + right: 5px; + bottom: 5px; + } } /***** SEARCH RESULTS */ @@ -218,6 +235,30 @@ } } } + + /***** RESTRICTED NOTEBOOK */ + &__page-locked { + background: rgba($colorAlert, 0.2); + display: flex; + padding: 5px; + + > * + * { + margin-left: $interiorMargin; + } + + [class*='icon'] { + flex: 0 0 auto; + } + + [class*='__message'] { + flex: 1 1 auto; + } + } + + &__commit-entries-control { + display: flex; + justify-content: flex-end; + } } .is-notebook-default, @@ -287,9 +328,9 @@ flex-direction: column; flex: 1 1 auto; - > * + * { - margin-top: $interiorMargin; - } + > [class*="__"] + [class*="__"] { + margin-top: $interiorMarginSm; + } } &__text { @@ -311,8 +352,8 @@ padding-right: $p; @include hover { - &:not(:focus) { - background: rgba($colorBodyFg, 0.2); + &:not(:focus, .locked) { + background: rgba($colorBodyFg, 0.1); } } @@ -412,6 +453,7 @@ @include snapThumb(); } } + /****************************** SNAPSHOTTING */ // LEGACY: TODO: refactor these names .t-contents, @@ -426,7 +468,10 @@ color: $colorBodyFg; padding: $interiorMarginSm !important; // Prevents items from going right to the edge of the image - .l-sticky-headers .l-tabular-body { overflow: auto; } + .l-sticky-headers .l-tabular-body { + overflow: auto; + } + .l-browse-bar { display: none; // Suppress browse-bar when snapshotting from view-large overlay + * { @@ -470,6 +515,7 @@ > * { flex: 1 1 auto; + &:first-child { flex: 0 0 auto; } @@ -507,13 +553,19 @@ display: flex; flex-direction: column; position: absolute; - top: $m; right: 0; bottom: $m; left: 0; // LEGACY, deal with .editor border-radius clipping stuff + top: $m; + right: 0; + bottom: $m; + left: 0; // LEGACY, deal with .editor border-radius clipping stuff } #snap-annotation-wrapper, #snap-annotation-bar { position: relative; - top: auto; right: auto; bottom: auto; left: auto; + top: auto; + right: auto; + bottom: auto; + left: auto; } #snap-annotation-wrapper { @@ -541,7 +593,9 @@ > div { display: contents; - > * + * { margin-left: $interiorMargin !important; } + > * + * { + margin-left: $interiorMargin !important; + } } .ptro-tool-controls { @@ -613,7 +667,7 @@ } .ptro-color-active-control { - background: $colorBtnMajorBg !important; + background: $colorBtnMajorBg !important; color: $colorBtnMajorFg !important; } @@ -631,7 +685,10 @@ /****************************** MOBILE */ body.mobile { - .c-notebook__drag-area { display: none; } + .c-notebook__drag-area { + display: none; + } + .c-notebook__entry { [class*="local-controls"] { display: none; @@ -664,3 +721,26 @@ body.mobile { $c: $colorOk; @include pulseProp($animName: flashSnapshot, $dur: 500ms, $iter: infinite, $prop: background, $valStart: rgba($c, 0.4), $valEnd: rgba($c, 0)); } + +/****************************** RESTRICTED NOTEBOOK / SHIFT LOG */ +.c-notebook--restricted { + .c-notebook__pages { + .c-list__item { + // Can display lock icon when a page is committed. + &:before { + $s: 0.8em; + color: $colorAlert; + display: block; + font-size: $s; + width: $s; + margin-right: $interiorMarginSm; + } + + &:not([class*='lock']) { + &:before { + content: ''; + } + } + } + } +} diff --git a/src/ui/layout/CreateButton.vue b/src/ui/layout/CreateButton.vue index 4615aff3e3..2443f6b66c 100644 --- a/src/ui/layout/CreateButton.vue +++ b/src/ui/layout/CreateButton.vue @@ -19,32 +19,18 @@ import objectUtils from 'objectUtils'; export default { inject: ['openmct'], data: function () { - let items = []; - - this.openmct.types.listKeys().forEach(key => { - let menuItem = this.openmct.types.get(key).definition; - - if (menuItem.creatable) { - let menuItemTemplate = { - cssClass: menuItem.cssClass, - name: menuItem.name, - description: menuItem.description, - onItemClicked: () => this.create(key) - }; - - items.push(menuItemTemplate); - } - }); return { - items: items, + menuItems: {}, selectedMenuItem: {}, opened: false }; }, computed: { sortedItems() { - return this.items.slice().sort((a, b) => { + let items = this.getItems(); + + return items.sort((a, b) => { if (a.name < b.name) { return -1; } else if (a.name > b.name) { @@ -56,6 +42,26 @@ export default { } }, methods: { + getItems() { + let keys = this.openmct.types.listKeys(); + + keys.forEach(key => { + if (!this.menuItems[key]) { + let typeDef = this.openmct.types.get(key).definition; + + if (typeDef.creatable) { + this.menuItems[key] = { + cssClass: typeDef.cssClass, + name: typeDef.name, + description: typeDef.description, + onItemClicked: () => this.create(key) + }; + } + } + }); + + return Object.values(this.menuItems); + }, showCreateMenu() { const elementBoundingClientRect = this.$refs.createButton.getBoundingClientRect(); const x = elementBoundingClientRect.x; diff --git a/src/ui/layout/mct-tree.scss b/src/ui/layout/mct-tree.scss index bab20a0c28..1f31c3c7b9 100644 --- a/src/ui/layout/mct-tree.scss +++ b/src/ui/layout/mct-tree.scss @@ -6,7 +6,9 @@ flex: 1 1 auto; overflow: auto; - > * + * { margin-top: $interiorMargin; } + > * + * { + margin-top: $interiorMargin; + } &__search { flex: 0 0 auto; @@ -59,7 +61,6 @@ @include userSelectNone(); overflow-x: hidden; overflow-y: auto; - padding-right: $interiorMarginSm; .icon-arrow-nav-to-parent { visibility: hidden; @@ -71,6 +72,7 @@ li { position: relative; + &[class*="__item-h"] { display: block; width: 100%; @@ -82,7 +84,6 @@ } &__item { - border-radius: $controlCr; display: flex; align-items: center; cursor: pointer; @@ -107,12 +108,14 @@ color: $colorItemTreeSelectedFg; } } + &.is-new { animation-name: animTemporaryHighlight; animation-timing-function: ease-out; animation-duration: 3s; animation-iteration-count: 1; } + &.is-context-clicked { box-shadow: inset $colorItemTreeSelectedBg 0 0 0 1px; } @@ -128,11 +131,15 @@ } .c-tree { + padding-right: $interiorMarginSm; + .c-tree { margin-left: 15px; } &__item { + border-radius: $smallCr; + [class*="view-control"] { padding: 2px 10px; } @@ -161,6 +168,7 @@ @include button($bg: $colorMobilePaneLeftTreeItemBg, $fg: $colorMobilePaneLeftTreeItemFg); height: $mobileTreeItemH; margin-bottom: $interiorMarginSm; + [class*="view-control"] { width: ceil($mobileTreeItemH * 0.5); } @@ -202,10 +210,11 @@ .c-tree { &__item { - body.mobile & { + body.mobile & { @include button($bg: $colorMobilePaneLeftTreeItemBg, $fg: $colorMobilePaneLeftTreeItemFg); height: $mobileTreeItemH; margin-bottom: $interiorMarginSm; + [class*="view-control"] { width: ceil($mobileTreeItemH * 0.5); } @@ -218,9 +227,9 @@ } .c-list { - padding-right: $interiorMargin; - &__item { + border-radius: $smallCr; + &__name { $p: $interiorMarginSm; @include ellipsize(); @@ -254,7 +263,8 @@ content: ''; display: block; position: absolute; - left: 50%; top: 50%; + left: 50%; + top: 50%; height: $dimension; width: $dimension; } diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue index 5cb61a3893..220f82b428 100644 --- a/src/ui/layout/mct-tree.vue +++ b/src/ui/layout/mct-tree.vue @@ -448,7 +448,7 @@ export default { }, scrollTo(navigationPath) { - if (this.isItemInView(navigationPath)) { + if (!this.$refs.scrollable || this.isItemInView(navigationPath)) { return; } @@ -467,6 +467,10 @@ export default { } }, scrollEndEvent() { + if (!this.$refs.srcrollable) { + return; + } + this.$nextTick(() => { if (this.scrollToPath) { if (!this.isItemInView(this.scrollToPath)) {