Simple text export of Notebook (#6510)

* add simple prototype

* tags and metadata now exported

* add form for options

* revert notebook

* add simple e2e test

* add test stubs

* death to debug
This commit is contained in:
Scott Bell 2023-03-30 19:44:12 +02:00 committed by GitHub
parent 20789601b4
commit 3007b28b0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 184 additions and 0 deletions

View File

@ -377,4 +377,31 @@ test.describe('Notebook entry tests', () => {
expect.soft(await sanitizedLink.count()).toBe(1);
expect(await unsanitizedLink.count()).toBe(0);
});
test('can export notebook as text', 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 }) => {});
async function streamToString(readable) {
let result = '';
for await (const chunk of readable) {
result += chunk;
}
return result;
}
});

View File

@ -0,0 +1,155 @@
import {saveAs} from 'saveAs';
import Moment from 'moment';
const UNKNOWN_USER = 'Unknown';
const UNKNOWN_TIME = 'Unknown';
export default class ExportNotebookAsTextAction {
constructor(openmct) {
this.openmct = openmct;
this.cssClass = 'icon-export';
this.description = 'Exports notebook contents as a text file';
this.group = "action";
this.key = 'exportNotebookAsText';
this.name = 'Export Notebook as Text';
this.priority = 1;
}
invoke(objectPath) {
this.showForm(objectPath);
}
getTagName(tagId, availableTags) {
const foundTag = availableTags.find(tag => tag.id === tagId);
if (foundTag) {
return foundTag.label;
} else {
return tagId;
}
}
getTagsForEntry(entry, domainObjectKeyString, annotations) {
const foundTags = [];
annotations.forEach(annotation => {
const target = annotation.targets?.[domainObjectKeyString];
if (target?.entryId === entry.id) {
annotation.tags.forEach(tag => {
if (!foundTags.includes(tag)) {
foundTags.push(tag);
}
});
}
});
return foundTags;
}
formatTimeStamp(timestamp) {
if (timestamp) {
return `${Moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss')} UTC`;
} else {
return UNKNOWN_TIME;
}
}
appliesTo(objectPath) {
const domainObject = objectPath[0];
const type = this.openmct.types.get(domainObject.type);
return type?.definition?.name === 'Notebook';
}
async onSave(changes, objectPath) {
const availableTags = this.openmct.annotation.getAvailableTags();
const identifier = objectPath[0].identifier;
const domainObject = await this.openmct.objects.get(identifier);
let foundAnnotations = [];
// only load annotations if there are tags
if (availableTags.length) {
foundAnnotations = await this.openmct.annotation.getAnnotations(domainObject.identifier);
}
let notebookAsText = `# ${domainObject.name}\n\n`;
if (changes.exportMetaData) {
const createdTimestamp = domainObject.created;
const createdBy = domainObject.createdBy ?? UNKNOWN_USER;
const modifiedBy = domainObject.modifiedBy ?? UNKNOWN_USER;
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`;
}
const notebookSections = domainObject.configuration.sections;
const notebookEntries = domainObject.configuration.entries;
notebookSections.forEach(section => {
notebookAsText += `## ${section.name}\n\n`;
const notebookPages = section.pages;
notebookPages.forEach(page => {
notebookAsText += `### ${page.name}\n\n`;
const notebookPageEntries = notebookEntries[section.id]?.[page.id];
notebookPageEntries.forEach(entry => {
if (changes.exportMetaData) {
const createdTimestamp = entry.createdOn;
const createdBy = entry.createdBy ?? UNKNOWN_USER;
const modifiedBy = entry.modifiedBy ?? UNKNOWN_USER;
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`;
}
if (changes.exportTags) {
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const tags = this.getTagsForEntry(entry, domainObjectKeyString, foundAnnotations);
const tagNames = tags.map(tag => this.getTagName(tag, availableTags));
if (tagNames) {
notebookAsText += `Tags: ${tagNames.join(', ')}\n\n`;
}
}
notebookAsText += `${entry.text}\n\n`;
});
});
});
const blob = new Blob([notebookAsText], {type: "text/markdown"});
const fileName = domainObject.name + '.md';
saveAs(blob, fileName);
}
async showForm(objectPath) {
const formStructure = {
title: "Export Notebook Text",
sections: [
{
rows: [
{
key: "exportMetaData",
control: "toggleSwitch",
name: "Include Metadata (created/modified, etc.)",
required: true,
value: false
},
{
name: "Include Tags",
control: "toggleSwitch",
required: true,
key: 'exportTags',
value: false
}
]
}
]
};
const changes = await this.openmct.forms.showForm(formStructure);
return this.onSave(changes, objectPath);
}
}

View File

@ -21,6 +21,7 @@
*****************************************************************************/
import CopyToNotebookAction from './actions/CopyToNotebookAction';
import ExportNotebookAsTextAction from './actions/ExportNotebookAsTextAction';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import NotebookViewProvider from './NotebookViewProvider';
import NotebookType from './NotebookType';
@ -80,6 +81,7 @@ function installBaseNotebookFunctionality(openmct) {
};
openmct.types.addType('notebookSnapshotImage', notebookSnapshotImageType);
openmct.actions.register(new CopyToNotebookAction(openmct));
openmct.actions.register(new ExportNotebookAsTextAction(openmct));
const notebookSnapshotIndicator = new Vue ({
components: {