From b7a671d392f91133a44f5d2fb57015813c724968 Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Sat, 1 Apr 2023 07:08:22 +0200 Subject: [PATCH] Allow Restricted Notebooks to export text (#6542) * Refactor string to stream * add to restricted notebooks and allow for blank users * forgot to add notebook * use better types and fix test * move streamToString * add export group * catch blank pages --- e2e/baseFixtures.js | 1 + e2e/pluginFixtures.js | 14 +++++++++ .../plugins/notebook/notebook.e2e.spec.js | 11 +------ .../notebook/restrictedNotebook.e2e.spec.js | 29 +++++++++++++++++- src/api/actions/ActionsAPI.js | 2 +- .../exportAsJSONAction/ExportAsJSONAction.js | 2 +- .../ImportFromJSONAction.js | 2 +- .../actions/ExportNotebookAsTextAction.js | 30 +++++++++++++------ .../notebook/utils/notebook-entries.js | 2 +- 9 files changed, 69 insertions(+), 24 deletions(-) diff --git a/e2e/baseFixtures.js b/e2e/baseFixtures.js index 448b831b5d..532f000513 100644 --- a/e2e/baseFixtures.js +++ b/e2e/baseFixtures.js @@ -170,5 +170,6 @@ exports.test = base.test.extend({ } } }); + exports.expect = expect; exports.waitForAnimations = waitForAnimations; diff --git a/e2e/pluginFixtures.js b/e2e/pluginFixtures.js index da326c9bf2..375d411639 100644 --- a/e2e/pluginFixtures.js +++ b/e2e/pluginFixtures.js @@ -150,3 +150,17 @@ exports.test = test.extend({ } }); exports.expect = expect; + +/** + * Takes a readable stream and returns a string. + * @param {ReadableStream} readable - the readable stream + * @return {Promise} the stringified stream + */ +exports.streamToString = async function (readable) { + let result = ''; + for await (const chunk of readable) { + result += chunk; + } + + return result; +}; diff --git a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js index 7842595298..10f5bcb14f 100644 --- a/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/notebook.e2e.spec.js @@ -24,7 +24,7 @@ This test suite is dedicated to tests which verify the basic operations surrounding Notebooks. */ -const { test, expect } = require('../../../../pluginFixtures'); +const { test, expect, streamToString } = require('../../../../pluginFixtures'); const { createDomainObjectWithDefaults } = require('../../../../appActions'); const nbUtils = require('../../../../helper/notebookUtils'); const path = require('path'); @@ -395,13 +395,4 @@ test.describe('Notebook entry tests', () => { test.fixme('can export all notebook entry metdata', async ({ page }) => {}); test.fixme('can export all notebook tags', async ({ page }) => {}); test.fixme('can export all notebook snapshots', async ({ page }) => {}); - - async function streamToString(readable) { - let result = ''; - for await (const chunk of readable) { - result += chunk; - } - - return result; - } }); diff --git a/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js index 0043a1dc38..d13025220f 100644 --- a/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/restrictedNotebook.e2e.spec.js @@ -20,7 +20,7 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -const { test, expect } = require('../../../../pluginFixtures'); +const { test, expect, streamToString } = require('../../../../pluginFixtures'); const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions'); const path = require('path'); const nbUtils = require('../../../../helper/notebookUtils'); @@ -169,6 +169,33 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit }); +test.describe('can export restricted notebook as text', () => { + test.beforeEach(async ({ page }) => { + await startAndAddRestrictedNotebookObject(page); + }); + + test('basic functionality ', async ({ page }) => { + await nbUtils.enterTextEntry(page, `Foo bar entry`); + // Click on 3 Dot Menu + await page.locator('button[title="More options"]').click(); + const downloadPromise = page.waitForEvent('download'); + + await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click(); + + await page.getByRole('button', { name: 'Save' }).click(); + const download = await downloadPromise; + const readStream = await download.createReadStream(); + const exportedText = await streamToString(readStream); + expect(exportedText).toContain('Foo bar entry'); + + }); + + test.fixme('can export multiple notebook entries as text ', async ({ page }) => {}); + test.fixme('can export all notebook entry metdata', async ({ page }) => {}); + test.fixme('can export all notebook tags', async ({ page }) => {}); + test.fixme('can export all notebook snapshots', async ({ page }) => {}); +}); + /** * @param {import('@playwright/test').Page} page */ diff --git a/src/api/actions/ActionsAPI.js b/src/api/actions/ActionsAPI.js index 43e5b0c6c2..9e3f1da501 100644 --- a/src/api/actions/ActionsAPI.js +++ b/src/api/actions/ActionsAPI.js @@ -31,7 +31,7 @@ class ActionsAPI extends EventEmitter { this._actionCollections = new WeakMap(); this._openmct = openmct; - this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json']; + this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'export', 'import']; this.register = this.register.bind(this); this.getActionsCollection = this.getActionsCollection.bind(this); diff --git a/src/plugins/exportAsJSONAction/ExportAsJSONAction.js b/src/plugins/exportAsJSONAction/ExportAsJSONAction.js index 77a6a26028..460847f3f1 100644 --- a/src/plugins/exportAsJSONAction/ExportAsJSONAction.js +++ b/src/plugins/exportAsJSONAction/ExportAsJSONAction.js @@ -32,7 +32,7 @@ export default class ExportAsJSONAction { this.key = 'export.JSON'; this.description = ''; this.cssClass = "icon-export"; - this.group = "json"; + this.group = "export"; this.priority = 1; this.externalIdentifiers = []; diff --git a/src/plugins/importFromJSONAction/ImportFromJSONAction.js b/src/plugins/importFromJSONAction/ImportFromJSONAction.js index a41e71864d..46968a8702 100644 --- a/src/plugins/importFromJSONAction/ImportFromJSONAction.js +++ b/src/plugins/importFromJSONAction/ImportFromJSONAction.js @@ -29,7 +29,7 @@ export default class ImportAsJSONAction { this.key = 'import.JSON'; this.description = ''; this.cssClass = "icon-import"; - this.group = "json"; + this.group = "import"; this.priority = 2; this.openmct = openmct; diff --git a/src/plugins/notebook/actions/ExportNotebookAsTextAction.js b/src/plugins/notebook/actions/ExportNotebookAsTextAction.js index 17c48bdd9b..7f0f692e3d 100644 --- a/src/plugins/notebook/actions/ExportNotebookAsTextAction.js +++ b/src/plugins/notebook/actions/ExportNotebookAsTextAction.js @@ -1,8 +1,9 @@ import {saveAs} from 'saveAs'; import Moment from 'moment'; - +import {NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE} from '../notebook-constants'; const UNKNOWN_USER = 'Unknown'; const UNKNOWN_TIME = 'Unknown'; +const ALLOWED_TYPES = [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE]; export default class ExportNotebookAsTextAction { @@ -11,10 +12,9 @@ export default class ExportNotebookAsTextAction { this.cssClass = 'icon-export'; this.description = 'Exports notebook contents as a text file'; - this.group = "action"; + this.group = "export"; this.key = 'exportNotebookAsText'; this.name = 'Export Notebook as Text'; - this.priority = 1; } invoke(objectPath) { @@ -56,9 +56,8 @@ export default class ExportNotebookAsTextAction { appliesTo(objectPath) { const domainObject = objectPath[0]; - const type = this.openmct.types.get(domainObject.type); - return type?.definition?.name === 'Notebook'; + return ALLOWED_TYPES.includes(domainObject.type); } async onSave(changes, objectPath) { @@ -75,8 +74,8 @@ export default class ExportNotebookAsTextAction { if (changes.exportMetaData) { const createdTimestamp = domainObject.created; - const createdBy = domainObject.createdBy ?? UNKNOWN_USER; - const modifiedBy = domainObject.modifiedBy ?? UNKNOWN_USER; + const createdBy = this.getUserName(domainObject.createdBy); + const modifiedBy = this.getUserName(domainObject.modifiedBy); const modifiedTimestamp = domainObject.modified ?? domainObject.created; notebookAsText += `Created on ${this.formatTimeStamp(createdTimestamp)} by user ${createdBy}\n\n`; notebookAsText += `Updated on ${this.formatTimeStamp(modifiedTimestamp)} by user ${modifiedBy}\n\n`; @@ -94,11 +93,16 @@ export default class ExportNotebookAsTextAction { notebookAsText += `### ${page.name}\n\n`; const notebookPageEntries = notebookEntries[section.id]?.[page.id]; + if (!notebookPageEntries) { + // blank page + return; + } + notebookPageEntries.forEach(entry => { if (changes.exportMetaData) { const createdTimestamp = entry.createdOn; - const createdBy = entry.createdBy ?? UNKNOWN_USER; - const modifiedBy = entry.modifiedBy ?? UNKNOWN_USER; + const createdBy = this.getUserName(entry.createdBy); + const modifiedBy = this.getUserName(entry.modifiedBy); const modifiedTimestamp = entry.modified ?? entry.created; notebookAsText += `Created on ${this.formatTimeStamp(createdTimestamp)} by user ${createdBy}\n\n`; notebookAsText += `Updated on ${this.formatTimeStamp(modifiedTimestamp)} by user ${modifiedBy}\n\n`; @@ -123,6 +127,14 @@ export default class ExportNotebookAsTextAction { saveAs(blob, fileName); } + getUserName(userId) { + if (userId && userId.length) { + return userId; + } + + return UNKNOWN_USER; + } + async showForm(objectPath) { const formStructure = { title: "Export Notebook Text", diff --git a/src/plugins/notebook/utils/notebook-entries.js b/src/plugins/notebook/utils/notebook-entries.js index 42995c013c..0a45fadfe8 100644 --- a/src/plugins/notebook/utils/notebook-entries.js +++ b/src/plugins/notebook/utils/notebook-entries.js @@ -2,7 +2,7 @@ import objectLink from '../../../ui/mixins/object-link'; import { v4 as uuid } from 'uuid'; async function getUsername(openmct) { - let username = ''; + let username = null; if (openmct.user.hasProvider()) { const user = await openmct.user.getCurrentUser();