From 8aa222d1c85594fa558a559bfcb0623cb52645d9 Mon Sep 17 00:00:00 2001 From: Barbara Valdez <34872381+barbaravaldez@users.noreply.github.com> Date: Tue, 30 Mar 2021 18:44:52 -0700 Subject: [PATCH] Add new file UI (#14773) Adds a notebook or markdown file to a book's top level or a section by right click on Book Tree View --- extensions/notebook/package.json | 34 ++- extensions/notebook/package.nls.json | 6 +- extensions/notebook/src/book/bookModel.ts | 7 +- .../notebook/src/book/bookTocManager.ts | 147 +++++++----- extensions/notebook/src/book/bookTreeItem.ts | 6 +- extensions/notebook/src/book/bookTreeView.ts | 52 ++-- .../notebook/src/book/tocEntryPathHandler.ts | 20 ++ .../notebook/src/common/localizedConstants.ts | 12 +- extensions/notebook/src/common/utils.ts | 25 +- .../notebook/src/dialog/addFileDialog.ts | 111 +++++++++ .../notebook/src/dialog/createBookDialog.ts | 4 +- extensions/notebook/src/extension.ts | 28 +-- .../src/test/book/bookTocManager.test.ts | 223 ++++++++---------- 13 files changed, 428 insertions(+), 247 deletions(-) create mode 100644 extensions/notebook/src/book/tocEntryPathHandler.ts create mode 100644 extensions/notebook/src/dialog/addFileDialog.ts diff --git a/extensions/notebook/package.json b/extensions/notebook/package.json index 3c3738e138..f9175226c1 100644 --- a/extensions/notebook/package.json +++ b/extensions/notebook/package.json @@ -235,13 +235,21 @@ "command": "notebook.command.closeBook", "title": "%title.closeJupyterBook%" }, + { + "command": "notebook.command.addNotebook", + "title": "%title.addNotebook%" + }, + { + "command": "notebook.command.addMarkdown", + "title": "%title.addMarkdown%" + }, { "command": "notebook.command.closeNotebook", - "title": "%title.closeJupyterNotebook%" + "title": "%title.closeNotebook%" }, { "command": "notebook.command.removeNotebook", - "title": "%title.removeJupyterNotebook%" + "title": "%title.removeNotebook%" }, { "command": "notebook.command.moveTo", @@ -389,6 +397,14 @@ "command": "notebook.command.closeBook", "when": "false" }, + { + "command": "notebook.command.addNotebook", + "when": "false" + }, + { + "command": "notebook.command.addMarkdown", + "when": "false" + }, { "command": "notebook.command.closeNotebook", "when": "false" @@ -477,13 +493,23 @@ "command": "notebook.command.removeNotebook", "when": "view == bookTreeView && viewItem == savedBookNotebook" }, + { + "command": "notebook.command.addNotebook", + "when": "view == bookTreeView && viewItem == section || view == bookTreeView && viewItem == savedBook", + "group": "newFile@1" + }, + { + "command": "notebook.command.addMarkdown", + "when": "view == bookTreeView && viewItem == section || view == bookTreeView && viewItem == savedBook", + "group": "newFile@1" + }, { "command": "notebook.command.moveTo", - "when": "view == bookTreeView && viewItem == savedNotebook || view == bookTreeView && viewItem == savedBookNotebook || view == bookTreeView && viewItem == section" + "when": "view == bookTreeView && viewItem == savedNotebook || view == bookTreeView && viewItem == savedBookNotebook || view == bookTreeView && viewItem == section || view == bookTreeView && viewItem == Markdown" }, { "command": "notebook.command.pinNotebook", - "when": "view == bookTreeView && viewItem == savedNotebook || view == bookTreeView && viewItem == savedBookNotebook", + "when": "view == bookTreeView && viewItem == savedNotebook || view == bookTreeView && viewItem == savedBookNotebook || view == bookTreeView && viewItem == Markdown", "group": "inline" }, { diff --git a/extensions/notebook/package.nls.json b/extensions/notebook/package.nls.json index 6b21ad9ad9..6ddcd4923e 100644 --- a/extensions/notebook/package.nls.json +++ b/extensions/notebook/package.nls.json @@ -42,8 +42,10 @@ "title.PreviewLocalizedBook": "Get localized SQL Server 2019 guide", "title.openJupyterBook": "Open Book", "title.closeJupyterBook": "Close Book", - "title.closeJupyterNotebook": "Close Notebook", - "title.removeJupyterNotebook": "Remove Notebook", + "title.closeNotebook": "Close Notebook", + "title.removeNotebook": "Remove Notebook", + "title.addNotebook": "Add Notebook", + "title.addMarkdown": "Add Markdown File", "title.revealInBooksViewlet": "Reveal in Books", "title.createJupyterBook": "Create Book (Preview)", "title.openNotebookFolder": "Open Notebooks in Folder", diff --git a/extensions/notebook/src/book/bookModel.ts b/extensions/notebook/src/book/bookModel.ts index 576361795c..4c9b9ca8c4 100644 --- a/extensions/notebook/src/book/bookModel.ts +++ b/extensions/notebook/src/book/bookModel.ts @@ -25,7 +25,6 @@ export class BookModel { private _contentFolderPath: string; private _configPath: string; private _bookVersion: BookVersion; - private _rootPath: string; private _errorMessage: string; private _activePromise: Deferred | undefined = undefined; private _queuedPromises: Deferred[] = []; @@ -104,11 +103,9 @@ export class BookModel { } this._bookVersion = BookVersion.v1; this._contentFolderPath = path.posix.join(this.bookPath, content, ''); - this._rootPath = path.dirname(path.dirname(this._tableOfContentsPath)); } else { this._contentFolderPath = this.bookPath; this._tableOfContentsPath = path.posix.join(this.bookPath, '_toc.yml'); - this._rootPath = path.dirname(this._tableOfContentsPath); this._bookVersion = BookVersion.v2; } } @@ -190,7 +187,7 @@ export class BookModel { version: this._bookVersion, title: config.title, contentPath: this._tableOfContentsPath, - root: this._rootPath, + root: this.bookPath, tableOfContents: { sections: this.parseJupyterSections(this._bookVersion, tableOfContents) }, page: tableOfContents, type: BookTreeItemType.Book, @@ -334,7 +331,7 @@ export class BookModel { return this._errorMessage; } - public get version(): string { + public get version(): BookVersion { return this._bookVersion; } } diff --git a/extensions/notebook/src/book/bookTocManager.ts b/extensions/notebook/src/book/bookTocManager.ts index 39d8e05b64..ce0f6a3f52 100644 --- a/extensions/notebook/src/book/bookTocManager.ts +++ b/extensions/notebook/src/book/bookTocManager.ts @@ -11,12 +11,15 @@ import { BookVersion, convertTo } from './bookVersionHandler'; import * as vscode from 'vscode'; import * as loc from '../common/localizedConstants'; import { BookModel } from './bookModel'; +import { TocEntryPathHandler } from './tocEntryPathHandler'; +import { FileExtension } from '../common/utils'; export interface IBookTocManager { updateBook(element: BookTreeItem, book: BookTreeItem, targetSection?: JupyterBookSection): Promise; removeNotebook(element: BookTreeItem): Promise; createBook(bookContentPath: string, contentFolder: string): Promise; - recovery(): Promise + addNewFile(pathDetails: TocEntryPathHandler, bookItem: BookTreeItem): Promise; + recovery(): Promise; } export interface quickPickResults { @@ -24,7 +27,7 @@ export interface quickPickResults { book?: BookTreeItem } -const allowedFileExtensions: string[] = ['.md', '.ipynb']; +const allowedFileExtensions: string[] = [FileExtension.Markdown, FileExtension.Notebook]; export function hasSections(node: JupyterBookSection): boolean { return node.sections !== undefined && node.sections.length > 0; @@ -38,12 +41,12 @@ export class BookTocManager implements IBookTocManager { public tocFiles: Map = new Map(); private sourceBookContentPath: string; private targetBookContentPath: string; - private _sourceBook: BookModel; - constructor(targetBook?: BookModel, sourceBook?: BookModel) { - this._sourceBook = sourceBook; - this.sourceBookContentPath = sourceBook?.bookItems[0].rootContentPath; - this.targetBookContentPath = targetBook?.bookItems[0].rootContentPath; + constructor(private _sourceBook?: BookModel, private _targetBook?: BookModel) { + this._targetBook?.unwatchTOC(); + this._sourceBook?.unwatchTOC(); + this.sourceBookContentPath = this._sourceBook?.bookItems[0].rootContentPath; + this.targetBookContentPath = this._targetBook?.bookItems[0].rootContentPath; } /** @@ -180,19 +183,25 @@ export class BookTocManager implements IBookTocManager { } /** - * Reads and modifies the table of contents file of the target book. - * @param version the version of the target book + * Reads and modifies the table of contents file of book. + * @param version the version of book. * @param tocPath Path to the table of contents - * @param findSection The section that will be modified. + * @param findSection The section that will be modified. If findSection is undefined then the added section is added at the end of the toc file. * @param addSection The section that'll be added to the target section. If it's undefined then the target section (findSection) is removed from the table of contents. */ - async updateTOC(version: BookVersion, tocPath: string, findSection: JupyterBookSection, addSection?: JupyterBookSection): Promise { + async updateTOC(version: BookVersion, tocPath: string, findSection?: JupyterBookSection, addSection?: JupyterBookSection): Promise { const tocFile = await fs.readFile(tocPath, 'utf8'); this.tableofContents = yaml.safeLoad(tocFile); if (!this.tocFiles.has(tocPath)) { this.tocFiles.set(tocPath, tocFile); } - const isModified = this.modifyToc(version, this.tableofContents, findSection, addSection); + let isModified = false; + if (findSection) { + isModified = this.modifyToc(version, this.tableofContents, findSection, addSection); + } else if (addSection) { + this.tableofContents.push(addSection); + isModified = true; + } if (isModified) { await fs.writeFile(tocPath, yaml.safeDump(this.tableofContents, { lineWidth: Infinity, noRefs: true, skipInvalid: true })); } else { @@ -307,7 +316,7 @@ export class BookTocManager implements IBookTocManager { * @param section The section that's been moved. * @param book The target book. */ - async addSection(section: BookTreeItem, book: BookTreeItem): Promise { + async moveSectionFiles(section: BookTreeItem, book: BookTreeItem): Promise { const uri = path.posix.join(path.posix.sep, path.relative(section.rootContentPath, section.book.contentPath)); let moveFile = path.join(path.parse(uri).dir, path.parse(uri).name); let fileName = undefined; @@ -347,20 +356,20 @@ export class BookTocManager implements IBookTocManager { } /** - * Moves a notebook to a book top level or a book's section. If there's a target section we add the the targetSection directory if it has one and append it to the - * notebook's path. The overwrite option is set to false to prevent any issues with duplicated file names. - * @param element Notebook, Markdown File, or section that will be added to the book. + * Moves a file to a book top level or a book's section. If there's a target section we add the the targetSection directory if it has one and append it to the + * files's path. The overwrite option is set to false to prevent any issues with duplicated file names. + * @param element Notebook, Markdown File, or book's notebook that will be added to the book. * @param targetBook Book that will be modified. */ - async addNotebook(notebook: BookTreeItem, book: BookTreeItem): Promise { + async moveFile(file: BookTreeItem, book: BookTreeItem): Promise { const rootPath = book.rootContentPath; - const notebookPath = path.parse(notebook.book.contentPath); + const filePath = path.parse(file.book.contentPath); let fileName = undefined; try { - await fs.move(notebook.book.contentPath, path.join(rootPath, notebookPath.base), { overwrite: false }); + await fs.move(file.book.contentPath, path.join(rootPath, filePath.base), { overwrite: false }); } catch (error) { if (error.code === 'EEXIST') { - fileName = await this.renameFile(notebook.book.contentPath, path.join(rootPath, notebookPath.base)); + fileName = await this.renameFile(file.book.contentPath, path.join(rootPath, filePath.base)); } else { throw (error); @@ -368,14 +377,14 @@ export class BookTocManager implements IBookTocManager { } if (this._sourceBook) { - const sectionTOC = this._sourceBook.bookItems[0].findChildSection(notebook.uri); + const sectionTOC = this._sourceBook.bookItems[0].findChildSection(file.uri); if (sectionTOC) { this.newSection = sectionTOC; } } - fileName = fileName === undefined ? notebookPath.name : path.parse(fileName).name; + fileName = fileName === undefined ? filePath.name : path.parse(fileName).name; this.newSection.file = path.posix.join(path.posix.sep, fileName); - this.newSection.title = notebook.book.title; + this.newSection.title = file.book.title; if (book.version === BookVersion.v1) { // here we only convert if is v1 because we are already using the v2 notation for every book that we read. this.newSection = convertTo(book.version, this.newSection); @@ -388,50 +397,76 @@ export class BookTocManager implements IBookTocManager { * @param targetBook Book that will be modified. * @param targetSection Book section that'll be modified. */ - public async updateBook(element: BookTreeItem, targetBook: BookTreeItem, targetSection?: JupyterBookSection): Promise { - if (element.contextValue === 'section') { - // modify the sourceBook toc and remove the section - const findSection: JupyterBookSection = { file: element.book.page.file, title: element.book.page.title }; - await this.addSection(element, targetBook); - await this.updateTOC(element.book.version, element.tableOfContentsPath, findSection, undefined); - if (targetSection) { - // adding new section to the target book toc file - await this.updateTOC(targetBook.book.version, targetBook.tableOfContentsPath, targetSection, this.newSection); + public async updateBook(element: BookTreeItem, targetItem: BookTreeItem, targetSection?: JupyterBookSection): Promise { + try { + if (element.contextValue === 'section') { + // modify the sourceBook toc and remove the section + const findSection: JupyterBookSection = { file: element.book.page.file, title: element.book.page.title }; + await this.moveSectionFiles(element, targetItem); + // remove section from book + await this.updateTOC(element.book.version, element.tableOfContentsPath, findSection, undefined); + // add section to book + await this.updateTOC(targetItem.book.version, targetItem.tableOfContentsPath, targetSection, this.newSection); } else { - //since there's not a target section, we just append the section at the end of the file - if (this.targetBookContentPath !== this.sourceBookContentPath) { - this.tableofContents = targetBook.sections.map(section => convertTo(targetBook.version, section)); + // the notebook is part of a book so we need to modify its toc as well + const findSection = { file: element.book.page.file, title: element.book.page.title }; + await this.moveFile(element, targetItem); + if (element.contextValue === 'savedBookNotebook' || element.contextValue === 'Markdown') { + // remove notebook entry from book toc + await this.updateTOC(element.book.version, element.tableOfContentsPath, findSection, undefined); + } else { + // close the standalone notebook, so it doesn't throw an error when we move the notebook to new location. + await vscode.commands.executeCommand('notebook.command.closeNotebook', element); + } + await this.updateTOC(targetItem.book.version, targetItem.tableOfContentsPath, targetSection, this.newSection); + } + } catch (e) { + await this.recovery(); + vscode.window.showErrorMessage(loc.editBookError(element.book.contentPath, e instanceof Error ? e.message : e)); + } finally { + try { + await this._targetBook.reinitializeContents(); + } finally { + if (this._sourceBook && this._sourceBook.bookPath !== this._targetBook.bookPath) { + // refresh source book model to pick up latest changes + await this._sourceBook.reinitializeContents(); } - this.tableofContents.push(this.newSection); - await fs.writeFile(targetBook.tableOfContentsPath, yaml.safeDump(this.tableofContents, { lineWidth: Infinity, noRefs: true, skipInvalid: true })); } } - else if (element.contextValue === 'savedNotebook' || element.contextValue === 'savedBookNotebook') { - // the notebook is part of a book so we need to modify its toc as well - const findSection = { file: element.book.page.file, title: element.book.page.title }; - await this.addNotebook(element, targetBook); - if (element.tableOfContentsPath) { - await this.updateTOC(element.book.version, element.tableOfContentsPath, findSection, undefined); - } else { - // close the standalone notebook, so it doesn't throw an error when we move the notebook to new location. - await vscode.commands.executeCommand('notebook.command.closeNotebook', element); - } - if (!targetSection) { - if (this.targetBookContentPath !== this.sourceBookContentPath) { - this.tableofContents = targetBook.sections.map(section => convertTo(targetBook.version, section)); - } - this.tableofContents.push(this.newSection); - await fs.writeFile(targetBook.tableOfContentsPath, yaml.safeDump(this.tableofContents, { lineWidth: Infinity, noRefs: true, skipInvalid: true })); - } else { - await this.updateTOC(targetBook.book.version, targetBook.tableOfContentsPath, targetSection, this.newSection); - } + } + + public async addNewFile(pathDetails: TocEntryPathHandler, bookItem: BookTreeItem): Promise { + let findSection: JupyterBookSection | undefined = undefined; + await fs.writeFile(pathDetails.filePath, ''); + if (bookItem.contextValue === 'section') { + findSection = { file: bookItem.book.page.file, title: bookItem.book.page.title }; + } + let fileEntryInToc: JupyterBookSection = { + title: pathDetails.titleInTocEntry, + file: pathDetails.fileInTocEntry + }; + if (bookItem.book.version === BookVersion.v1) { + fileEntryInToc = convertTo(BookVersion.v1, fileEntryInToc); + } + // book is already opened in notebooks view, so modifying the toc will add the new file automatically + await this.updateTOC(bookItem.book.version, bookItem.tableOfContentsPath, findSection, fileEntryInToc); + await this._sourceBook.reinitializeContents(); + await this.openResource(pathDetails); + } + + public async openResource(pathDetails: TocEntryPathHandler): Promise { + if (pathDetails.fileExtension === FileExtension.Notebook) { + await vscode.commands.executeCommand('bookTreeView.openNotebook', pathDetails.filePath); + } else { + await vscode.commands.executeCommand('bookTreeView.openMarkdown', pathDetails.filePath); } } public async removeNotebook(element: BookTreeItem): Promise { const findSection = { file: element.book.page.file, title: element.book.page.title }; await this.updateTOC(element.book.version, element.tableOfContentsPath, findSection, undefined); + await this._sourceBook.reinitializeContents(); } public get modifiedDir(): Set { diff --git a/extensions/notebook/src/book/bookTreeItem.ts b/extensions/notebook/src/book/bookTreeItem.ts index 044bb8d8b6..7b52b90b97 100644 --- a/extensions/notebook/src/book/bookTreeItem.ts +++ b/extensions/notebook/src/book/bookTreeItem.ts @@ -41,7 +41,7 @@ export class BookTreeItem extends vscode.TreeItem { private _uri: string | undefined; private _previousUri: string; private _nextUri: string; - public readonly version: string; + public readonly version: BookVersion; public command: vscode.Command; public resourceUri: vscode.Uri; private _rootContentPath: string; @@ -71,7 +71,7 @@ export class BookTreeItem extends vscode.TreeItem { this.contextValue = BookTreeItemType.ExternalLink; } else { - this.contextValue = book.type === BookTreeItemType.Notebook ? (isBookItemPinned(book.contentPath) ? BookTreeItemType.pinnedNotebook : getNotebookType(book)) : BookTreeItemType.section; + this.contextValue = book.type === BookTreeItemType.Notebook ? (isBookItemPinned(book.contentPath) ? BookTreeItemType.pinnedNotebook : getNotebookType(book)) : BookTreeItemType.Markdown; } this.setPageVariables(); this.setCommand(); @@ -84,7 +84,7 @@ export class BookTreeItem extends vscode.TreeItem { } else { // if it's a section, book or a notebook's book then we set the table of contents path. - if (this.book.type === BookTreeItemType.Book || this.contextValue === BookTreeItemType.section || (book.tableOfContents.sections && book.type === BookTreeItemType.Notebook)) { + if (this.book.type === BookTreeItemType.Book || this.contextValue === BookTreeItemType.section || this.contextValue === BookTreeItemType.savedBookNotebook || book.tableOfContents.sections && book.type === BookTreeItemType.Markdown) { this._tableOfContentsPath = getTocPath(this.book.version, this.book.root); } this._rootContentPath = getContentPath(this.book.version, this.book.root, ''); diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts index 6d4de2391b..b85d1c3612 100644 --- a/extensions/notebook/src/book/bookTreeView.ts +++ b/extensions/notebook/src/book/bookTreeView.ts @@ -16,10 +16,11 @@ import { Deferred } from '../common/promise'; import { IBookTrustManager, BookTrustManager } from './bookTrustManager'; import * as loc from '../common/localizedConstants'; import * as glob from 'fast-glob'; -import { getPinnedNotebooks, confirmReplace, getNotebookType } from '../common/utils'; +import { getPinnedNotebooks, confirmMessageDialog, getNotebookType, FileExtension } from '../common/utils'; import { IBookPinManager, BookPinManager } from './bookPinManager'; import { BookTocManager, IBookTocManager, quickPickResults } from './bookTocManager'; import { CreateBookDialog } from '../dialog/createBookDialog'; +import { AddFileDialog } from '../dialog/addFileDialog'; import { getContentPath } from './bookVersionHandler'; import { TelemetryReporter, BookTelemetryView, NbTelemetryActions } from '../telemetry'; @@ -197,37 +198,10 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider book.getNotebook(path.normalize(movingElement.book.contentPath))); - movingElement.tableOfContents.sections = sourceBook?.bookItems[0].sections; - } - } const sourceBook = this.books.find(book => book.bookPath === movingElement.book.root); const targetBook = this.books.find(book => book.bookPath === updateBook.book.root); - this.bookTocManager = new BookTocManager(targetBook, sourceBook); - // remove watch on toc file from source book. - if (sourceBook) { - sourceBook.unwatchTOC(); - } - try { - await this.bookTocManager.updateBook(movingElement, updateBook, targetSection); - } catch (e) { - await this.bookTocManager.recovery(); - vscode.window.showErrorMessage(loc.editBookError(updateBook.book.contentPath, e instanceof Error ? e.message : e)); - } finally { - try { - await targetBook.reinitializeContents(); - } finally { - if (sourceBook && sourceBook.bookPath !== targetBook.bookPath) { - // refresh source book model to pick up latest changes - await sourceBook.reinitializeContents(); - } - if (sourceBook) { - sourceBook.watchTOC(); - } - } - } + this.bookTocManager = new BookTocManager(sourceBook, targetBook); + await this.bookTocManager.updateBook(movingElement, updateBook, targetSection); } } @@ -276,7 +250,23 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { + const book = this.books.find(b => b.bookPath === bookItem.root); + this.bookTocManager = new BookTocManager(book); + const dialog = new AddFileDialog(this.bookTocManager, bookItem, FileExtension.Markdown); + await dialog.createDialog(); + } + + async createNotebook(bookItem: BookTreeItem): Promise { + const book = this.books.find(b => b.bookPath === bookItem.root); + this.bookTocManager = new BookTocManager(book); + const dialog = new AddFileDialog(this.bookTocManager, bookItem, FileExtension.Notebook); + await dialog.createDialog(); + } + async removeNotebook(bookItem: BookTreeItem): Promise { + const book = this.books.find(b => b.bookPath === bookItem.root); + this.bookTocManager = new BookTocManager(book); return this.bookTocManager.removeNotebook(bookItem); } @@ -479,7 +469,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { +export async function confirmMessageDialog(prompter: IPrompter, msg: string): Promise { return await prompter.promptSingle({ type: QuestionTypes.confirm, - message: loc.confirmReplace, + message: msg, default: false }); } + +export async function selectFolder(): Promise { + let uris = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectMany: false, + canSelectFolders: true, + openLabel: loc.labelSelectFolder + }); + if (uris?.length > 0) { + return uris[0].fsPath; + } + return undefined; +} diff --git a/extensions/notebook/src/dialog/addFileDialog.ts b/extensions/notebook/src/dialog/addFileDialog.ts new file mode 100644 index 0000000000..0dac3bc648 --- /dev/null +++ b/extensions/notebook/src/dialog/addFileDialog.ts @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as azdata from 'azdata'; +import * as path from 'path'; +import { pathExists } from 'fs-extra'; +import * as loc from '../common/localizedConstants'; +import { IBookTocManager } from '../book/bookTocManager'; +import { confirmMessageDialog, FileExtension } from '../common/utils'; +import { IPrompter } from '../prompts/question'; +import CodeAdapter from '../prompts/adapter'; +import { BookTreeItem, BookTreeItemType } from '../book/bookTreeItem'; +import { TocEntryPathHandler } from '../book/tocEntryPathHandler'; + +export class AddFileDialog { + private _dialog: azdata.window.Dialog; + private readonly _dialogName = 'addNewFileBookTreeViewDialog'; + public view: azdata.ModelView; + private _formModel: azdata.FormContainer; + private _fileNameInputBox: azdata.InputBoxComponent; + private _titleInputBox: azdata.InputBoxComponent; + private _saveLocationInputBox: azdata.TextComponent; + private _prompter: IPrompter; + + constructor(private _tocManager: IBookTocManager, private _bookItem: BookTreeItem, private _extension: FileExtension) { + this._prompter = new CodeAdapter(); + } + + public async validatePath(folderPath: string, fileBasename: string): Promise { + const destinationUri = path.join(folderPath, fileBasename); + if (await pathExists(destinationUri)) { + const doOverwrite = await confirmMessageDialog(this._prompter, loc.confirmOverwrite); + if (!doOverwrite) { + throw (new Error(loc.msgDuplicadFileName(destinationUri))); + } + } + if (!(await pathExists(folderPath))) { + throw (new Error(loc.msgSaveFolderError)); + } + } + + public async createDialog(): Promise { + const dialogTitle = this._extension === FileExtension.Notebook ? loc.newNotebook : loc.newMarkdown; + this._dialog = azdata.window.createModelViewDialog(dialogTitle, this._dialogName); + this._dialog.registerContent(async view => { + this.view = view; + this._fileNameInputBox = this.view.modelBuilder.inputBox() + .withProperties({ + enabled: true, + width: '400px' + }).component(); + + this._titleInputBox = this.view.modelBuilder.inputBox() + .withProperties({ + enabled: true, + width: '400px' + }).component(); + + this._saveLocationInputBox = this.view.modelBuilder.inputBox() + .withProperties({ + value: this._bookItem.contextValue === BookTreeItemType.Book ? this._bookItem.rootContentPath : path.dirname(this._bookItem.resourceUri.fsPath), + enabled: false, + width: '400px' + }).component(); + + this._formModel = this.view.modelBuilder.formContainer() + .withFormItems([{ + components: [ + { + title: loc.title, + required: true, + component: this._titleInputBox + }, + { + component: this._fileNameInputBox, + title: loc.fileName, + required: true + }, + { + component: this._saveLocationInputBox, + title: loc.saveLocation, + required: false + } + ], + title: '' + }]).component(); + await this.view.initializeModel(this._formModel); + }); + this._dialog.okButton.label = loc.add; + this._dialog.registerCloseValidator(async () => await this.createFile()); + azdata.window.openDialog(this._dialog); + } + + private async createFile(): Promise { + try { + const dirPath = this._bookItem.contextValue === BookTreeItemType.Book ? this._bookItem.rootContentPath : path.dirname(this._bookItem.resourceUri.fsPath); + const filePath = path.join(dirPath, this._fileNameInputBox.value).concat(this._extension); + await this.validatePath(dirPath, this._fileNameInputBox.value.concat(this._extension)); + const pathDetails = new TocEntryPathHandler(filePath, this._bookItem.rootContentPath, this._titleInputBox.value); + await this._tocManager.addNewFile(pathDetails, this._bookItem); + return true; + } catch (error) { + this._dialog.message = { + text: error.message, + level: azdata.window.MessageLevel.Error + }; + return false; + } + } +} diff --git a/extensions/notebook/src/dialog/createBookDialog.ts b/extensions/notebook/src/dialog/createBookDialog.ts index 9a1ec8237e..bfea20d317 100644 --- a/extensions/notebook/src/dialog/createBookDialog.ts +++ b/extensions/notebook/src/dialog/createBookDialog.ts @@ -9,7 +9,7 @@ import { pathExists, remove } from 'fs-extra'; import * as loc from '../common/localizedConstants'; import { IconPathHelper } from '../common/iconHelper'; import { IBookTocManager } from '../book/bookTocManager'; -import { confirmReplace } from '../common/utils'; +import { confirmMessageDialog } from '../common/utils'; import { IPrompter } from '../prompts/question'; import CodeAdapter from '../prompts/adapter'; @@ -51,7 +51,7 @@ export class CreateBookDialog { public async validatePath(folderPath: string): Promise { const destinationUri = path.join(folderPath, path.basename(this.bookNameInputBox.value)); if (await pathExists(destinationUri)) { - const doReplace = await confirmReplace(this.prompter); + const doReplace = await confirmMessageDialog(this.prompter, loc.confirmReplace); if (doReplace) { //remove folder if exists await remove(destinationUri); diff --git a/extensions/notebook/src/extension.ts b/extensions/notebook/src/extension.ts index ae214e6249..15fa3f567d 100644 --- a/extensions/notebook/src/extension.ts +++ b/extensions/notebook/src/extension.ts @@ -36,32 +36,32 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi * This is the command used in the extension generator to open a Jupyter Book. */ extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openBook', (bookPath: string, openAsUntitled: boolean, urlToOpen?: string) => openAsUntitled ? providedBookTreeViewProvider.openBook(bookPath, urlToOpen, true) : bookTreeViewProvider.openBook(bookPath, urlToOpen, true))); - extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebook', (resource) => bookTreeViewProvider.openNotebook(resource))); - extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openUntitledNotebook', (resource) => providedBookTreeViewProvider.openNotebookAsUntitled(resource))); - extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openMarkdown', (resource) => bookTreeViewProvider.openMarkdown(resource))); - extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openExternalLink', (resource) => bookTreeViewProvider.openExternalLink(resource))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.addFileToView', (resource: string) => bookTreeViewProvider.openBook(resource, resource, true, true))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebook', (resource: string) => bookTreeViewProvider.openNotebook(resource))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openUntitledNotebook', (resource: string) => providedBookTreeViewProvider.openNotebookAsUntitled(resource))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openMarkdown', (resource: string) => bookTreeViewProvider.openMarkdown(resource))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openExternalLink', (resource: string) => bookTreeViewProvider.openExternalLink(resource))); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.saveBook', () => providedBookTreeViewProvider.saveJupyterBooks())); - extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.trustBook', (resource) => bookTreeViewProvider.trustBook(resource))); - extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.searchBook', (item) => bookTreeViewProvider.searchJupyterBooks(item))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.trustBook', (item: BookTreeItem) => bookTreeViewProvider.trustBook(item))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.searchBook', (item: BookTreeItem) => bookTreeViewProvider.searchJupyterBooks(item))); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.searchProvidedBook', () => providedBookTreeViewProvider.searchJupyterBooks())); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.openBook', () => bookTreeViewProvider.openNewBook())); - extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.closeBook', (book: any) => bookTreeViewProvider.closeBook(book))); - extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.closeNotebook', (book: any) => bookTreeViewProvider.closeBook(book))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.closeBook', (book: BookTreeItem) => bookTreeViewProvider.closeBook(book))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.closeNotebook', (notebook: BookTreeItem) => bookTreeViewProvider.closeBook(notebook))); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.removeNotebook', (book: BookTreeItem) => bookTreeViewProvider.removeNotebook(book))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.addNotebook', (book: BookTreeItem) => bookTreeViewProvider.createNotebook(book))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.addMarkdown', (book: BookTreeItem) => bookTreeViewProvider.createMarkdownFile(book))); + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.createBook', () => bookTreeViewProvider.createBook())); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.openNotebookFolder', (folderPath?: string, urlToOpen?: string, showPreview?: boolean) => bookTreeViewProvider.openNotebookFolder(folderPath, urlToOpen, showPreview))); - extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.pinNotebook', async (book: any) => { + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.pinNotebook', async (book: BookTreeItem) => { await bookTreeViewProvider.pinNotebook(book); await pinnedBookTreeViewProvider.addNotebookToPinnedView(book); })); - extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.unpinNotebook', async (book: any) => { + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.unpinNotebook', async (book: BookTreeItem) => { await bookTreeViewProvider.unpinNotebook(book); await pinnedBookTreeViewProvider.removeNotebookFromPinnedView(book); })); - extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.createBook', async () => { - await bookTreeViewProvider.createBook(); - })); - extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.moveTo', async (book: BookTreeItem) => { await bookTreeViewProvider.editBook(book); })); diff --git a/extensions/notebook/src/test/book/bookTocManager.test.ts b/extensions/notebook/src/test/book/bookTocManager.test.ts index 1526f8413e..b30acf3408 100644 --- a/extensions/notebook/src/test/book/bookTocManager.test.ts +++ b/extensions/notebook/src/test/book/bookTocManager.test.ts @@ -22,7 +22,6 @@ import * as loc from '../../common/localizedConstants'; import { BookVersion } from '../../book/bookVersionHandler'; import * as yaml from 'js-yaml'; - export function equalTOC(actualToc: IJupyterBookSectionV2[], expectedToc: IJupyterBookSectionV2[]): boolean { for (let [i, section] of actualToc.entries()) { if (section.title !== expectedToc[i].title || section.file !== expectedToc[i].file) { @@ -45,6 +44,37 @@ export function equalSections(actualSection: JupyterBookSection, expectedSection return true; } +function BookModelStub(root: string, bookItem: BookTreeItem, extension: MockExtensionContext): BookModel { + const bookModel = new BookModel(root, false, false, extension, undefined); + sinon.stub(bookModel, 'bookItems').value([bookItem]); + sinon.stub(bookModel, 'unwatchTOC').returns(); + sinon.stub(bookModel, 'reinitializeContents').resolves(); + sinon.stub(bookModel, 'bookPath').value(root); + return bookModel; +} + +function createBookTreeItemFormat(item: any, root: string, version: BookVersion): BookTreeItemFormat { + const pageFormat = item.type === BookTreeItemType.section ? { + title: item.sectionName, + file: item.uri, + sections: item.sectionFormat + } : item.sectionFormat; + const sections = item.type === BookTreeItemType.section ? item.sectionFormat : [item.sectionFormat]; + return { + title: item.sectionName, + contentPath: item.contentPath, + root: root, + tableOfContents: { + sections: sections + }, + isUntitled: undefined, + treeItemCollapsibleState: undefined, + type: item.type, + version: version, + page: pageFormat + }; +} + describe('BookTocManagerTests', function () { describe('CreatingBooks', () => { let notebooks: string[]; @@ -145,12 +175,14 @@ describe('BookTocManagerTests', function () { 'title': 'Notebook 2', 'file': path.posix.join(path.posix.sep, 'sectionA', 'notebook2') } - ] + ], + 'type': BookTreeItemType.savedBook }, sectionA: { 'contentPath': path.posix.join(sourceBookFolderPath, 'content', 'sectionA', 'readme.md'), 'sectionRoot': path.posix.join(sourceBookFolderPath, 'content', 'sectionA'), 'sectionName': 'Section A', + 'uri': path.posix.join(path.posix.sep, 'sectionA', 'readme'), 'notebook1': path.posix.join(sourceBookFolderPath, 'content', 'sectionA', 'notebook1.ipynb'), 'notebook2': path.posix.join(sourceBookFolderPath, 'content', 'sectionA', 'notebook2.ipynb'), 'sectionFormat': [ @@ -162,12 +194,14 @@ describe('BookTocManagerTests', function () { 'title': 'Notebook 2', 'file': path.posix.join(path.posix.sep, 'sectionA', 'notebook2') } - ] + ], + 'type': BookTreeItemType.section }, sectionB: { 'contentPath': path.posix.join(sourceBookFolderPath, 'content', 'sectionB', 'readme.md'), 'sectionRoot': path.posix.join(sourceBookFolderPath, 'content', 'sectionB'), 'sectionName': 'Section B', + 'uri': path.posix.join(path.posix.sep, 'sectionB', 'readme'), 'notebook3': path.posix.join(sourceBookFolderPath, 'content', 'sectionB', 'notebook3.ipynb'), 'notebook4': path.posix.join(sourceBookFolderPath, 'content', 'sectionB', 'notebook4.ipynb'), 'sectionFormat': [ @@ -179,10 +213,24 @@ describe('BookTocManagerTests', function () { 'title': 'Notebook 4', 'file': path.posix.join(path.posix.sep, 'sectionB', 'notebook4') } - ] + ], + 'type': BookTreeItemType.section }, notebook5: { - 'contentPath': path.posix.join(sourceBookFolderPath, 'content', 'notebook5.ipynb') + 'contentPath': path.posix.join(sourceBookFolderPath, 'content', 'notebook5.ipynb'), + 'sectionFormat': { + 'title': 'Notebook 5', + 'file': path.posix.join(path.posix.sep, 'notebook5') + }, + 'type': BookTreeItemType.Notebook + }, + duplicatedNotebook: { + 'contentPath': path.posix.join(duplicatedNotebookPath, 'notebook5.ipynb'), + 'sectionFormat': { + 'title': 'Notebook 5', + 'file': path.posix.join(path.posix.sep, 'notebook5') + }, + 'type': BookTreeItemType.Notebook }, targetBook: { 'rootBookFolderPath': targetBookFolderPath, @@ -204,19 +252,22 @@ describe('BookTocManagerTests', function () { } ] } - ] + ], + 'type': BookTreeItemType.Book }, sectionC: { 'contentPath': path.posix.join(targetBookFolderPath, 'content', 'sectionC', 'readme.md'), 'sectionRoot': path.posix.join(targetBookFolderPath, 'content', 'sectionC'), 'sectionName': 'Section C', + 'uri': path.posix.join(path.posix.sep, 'sectionC', 'readme'), 'notebook6': path.posix.join(targetBookFolderPath, 'content', 'sectionC', 'notebook6.ipynb'), 'sectionFormat': [ { 'title': 'Notebook 6', 'file': path.posix.join(path.posix.sep, 'sectionC', 'notebook6') } - ] + ], + 'type': BookTreeItemType.section } }, { it: 'using the jupyter-book legacy version >= 0.7.0', @@ -231,6 +282,7 @@ describe('BookTocManagerTests', function () { 'contentPath': path.posix.join(sourceBookFolderPath, 'sectionA', 'readme.md'), 'sectionRoot': path.posix.join(sourceBookFolderPath, 'sectionA'), 'sectionName': 'Section A', + 'uri': path.posix.join(path.posix.sep, 'sectionA', 'readme'), 'notebook1': path.posix.join(sourceBookFolderPath, 'sectionA', 'notebook1.ipynb'), 'notebook2': path.posix.join(sourceBookFolderPath, 'sectionA', 'notebook2.ipynb'), 'sectionFormat': [ @@ -242,12 +294,14 @@ describe('BookTocManagerTests', function () { 'title': 'Notebook 2', 'file': path.posix.join(path.posix.sep, 'sectionA', 'notebook2') } - ] + ], + 'type': BookTreeItemType.section }, sectionB: { 'contentPath': path.posix.join(sourceBookFolderPath, 'sectionB', 'readme.md'), 'sectionRoot': path.posix.join(sourceBookFolderPath, 'sectionB'), 'sectionName': 'Section B', + 'uri': path.posix.join(path.posix.sep, 'sectionB', 'readme'), 'notebook3': path.posix.join(sourceBookFolderPath, 'sectionB', 'notebook3.ipynb'), 'notebook4': path.posix.join(sourceBookFolderPath, 'sectionB', 'notebook4.ipynb'), 'sectionFormat': [ @@ -259,10 +313,24 @@ describe('BookTocManagerTests', function () { 'title': 'Notebook 4', 'file': path.posix.join(path.posix.sep, 'sectionB', 'notebook4') } - ] + ], + 'type': BookTreeItemType.section }, notebook5: { - 'contentPath': path.posix.join(sourceBookFolderPath, 'notebook5.ipynb') + 'contentPath': path.posix.join(sourceBookFolderPath, 'notebook5.ipynb'), + 'sectionFormat': { + 'title': 'Notebook 5', + 'file': path.posix.join(path.posix.sep, 'notebook5') + }, + 'type': BookTreeItemType.Notebook + }, + duplicatedNotebook: { + 'contentPath': path.posix.join(duplicatedNotebookPath, 'notebook5.ipynb'), + 'sectionFormat': { + 'title': 'Notebook 5', + 'file': path.posix.join(path.posix.sep, 'notebook5') + }, + 'type': BookTreeItemType.Notebook }, targetBook: { 'rootBookFolderPath': targetBookFolderPath, @@ -284,19 +352,22 @@ describe('BookTocManagerTests', function () { } ] } - ] + ], + 'type': BookTreeItemType.Book }, sectionC: { 'contentPath': path.posix.join(targetBookFolderPath, 'sectionC', 'readme.md'), 'sectionRoot': path.posix.join(targetBookFolderPath, 'sectionC'), 'sectionName': 'Section C', + 'uri': path.posix.join(path.posix.sep, 'sectionC', 'readme'), 'notebook6': path.posix.join(targetBookFolderPath, 'sectionC', 'notebook6.ipynb'), 'sectionFormat': [ { 'title': 'Notebook 6', 'file': path.posix.join(path.posix.sep, 'sectionC', 'notebook6') } - ] + ], + 'type': BookTreeItemType.section } } ]; @@ -317,103 +388,11 @@ describe('BookTocManagerTests', function () { version: run.version, page: run.targetBook.toc }; - - let sectionCTreeItemFormat: BookTreeItemFormat = { - title: run.sectionC.sectionName, - contentPath: run.sectionC.contentPath, - root: run.targetBook.rootBookFolderPath, - tableOfContents: { - sections: run.sectionC.sectionFormat - }, - isUntitled: undefined, - treeItemCollapsibleState: undefined, - type: BookTreeItemType.Markdown, - version: run.version, - page: run.sectionC.sectionFormat - }; - - // section A is from source book - let sectionATreeItemFormat: BookTreeItemFormat = { - title: run.sectionA.sectionName, - contentPath: run.sectionA.contentPath, - root: run.sourceBook.rootBookFolderPath, - tableOfContents: { - sections: run.sectionA.sectionFormat - }, - isUntitled: undefined, - treeItemCollapsibleState: undefined, - type: BookTreeItemType.Markdown, - version: run.version, - page: { - title: run.sectionA.sectionName, - file: path.posix.join(path.posix.sep, 'sectionA', 'readme'), - sections: run.sectionA.sectionFormat - } - }; - - // section B is from source book - let sectionBTreeItemFormat: BookTreeItemFormat = { - title: run.sectionB.sectionName, - contentPath: run.sectionB.contentPath, - root: run.sourceBook.rootBookFolderPath, - tableOfContents: { - sections: run.sectionB.sectionFormat - }, - isUntitled: undefined, - treeItemCollapsibleState: undefined, - type: BookTreeItemType.Markdown, - version: run.version, - page: { - title: run.sectionB.sectionName, - file: path.posix.join(path.posix.sep, 'sectionB', 'readme'), - sections: run.sectionB.sectionFormat - } - }; - - // notebook5 is from source book - let notebookTreeItemFormat: BookTreeItemFormat = { - title: '', - contentPath: run.notebook5.contentPath, - root: run.sourceBook.rootBookFolderPath, - tableOfContents: { - sections: [ - { - 'title': 'Notebook 5', - 'file': path.posix.join(path.posix.sep, 'notebook5') - } - ] - }, - isUntitled: undefined, - treeItemCollapsibleState: undefined, - type: BookTreeItemType.Notebook, - version: run.version, - page: { - 'title': 'Notebook 5', - 'file': path.posix.join(path.posix.sep, 'notebook5') - } - }; - - let duplicatedNbTreeItemFormat: BookTreeItemFormat = { - title: 'Duplicated Notebook', - contentPath: path.posix.join(duplicatedNotebookPath, 'notebook5.ipynb'), - root: duplicatedNotebookPath, - tableOfContents: { - sections: [ - { - 'title': 'Notebook 5', - 'file': path.posix.join(path.posix.sep, 'notebook5') - } - ] - }, - isUntitled: undefined, - treeItemCollapsibleState: undefined, - type: BookTreeItemType.Notebook, - version: run.version, - page: { - 'title': 'Notebook 5', - 'file': path.posix.join(path.posix.sep, 'notebook5') - } - }; + const sectionCTreeItemFormat = createBookTreeItemFormat(run.sectionC, run.targetBook.rootBookFolderPath, run.version); + const sectionATreeItemFormat = createBookTreeItemFormat(run.sectionA, run.sourceBook.rootBookFolderPath, run.version); + const sectionBTreeItemFormat = createBookTreeItemFormat(run.sectionB, run.sourceBook.rootBookFolderPath, run.version); + const notebookTreeItemFormat = createBookTreeItemFormat(run.notebook5, run.sourceBook.rootBookFolderPath, run.version); + const duplicatedNbTreeItemFormat = createBookTreeItemFormat(run.duplicatedNotebook, duplicatedNotebookPath, undefined); targetBook = new BookTreeItem(targetBookTreeItemFormat, undefined); sectionC = new BookTreeItem(sectionCTreeItemFormat, undefined); @@ -443,12 +422,7 @@ describe('BookTocManagerTests', function () { sectionA.sections = run.sectionA.sectionFormat; sectionB.sections = run.sectionB.sectionFormat; sectionC.sections = run.sectionC.sectionFormat; - notebook.sections = [ - { - 'title': 'Notebook 5', - 'file': path.posix.join(path.posix.sep, 'notebook5') - } - ]; + notebook.sections = [run.notebook5.sectionFormat]; duplicatedNotebook.sections = notebook.sections; await fs.promises.mkdir(run.targetBook.bookContentFolderPath, { recursive: true }); @@ -480,16 +454,13 @@ describe('BookTocManagerTests', function () { const mockExtensionContext = new MockExtensionContext(); - sourceBookModel = new BookModel(run.sourceBook.rootBookFolderPath, false, false, mockExtensionContext, undefined); - targetBookModel = new BookModel(run.targetBook.rootBookFolderPath, false, false, mockExtensionContext, undefined); - // create book model mock objects - sinon.stub(sourceBookModel, 'bookItems').value([sectionA]); - sinon.stub(targetBookModel, 'bookItems').value([targetBook]); + sourceBookModel = BookModelStub(run.sourceBook.rootBookFolderPath, sectionA, mockExtensionContext); + targetBookModel = BookModelStub(run.targetBook.rootBookFolderPath, targetBook, mockExtensionContext); }); it('Add section to book', async () => { - bookTocManager = new BookTocManager(targetBookModel, sourceBookModel); + bookTocManager = new BookTocManager(sourceBookModel, targetBookModel); await bookTocManager.updateBook(sectionA, targetBook, undefined); const listFiles = await fs.promises.readdir(path.join(run.targetBook.bookContentFolderPath, 'sectionA')); const listSourceFiles = await fs.promises.readdir(path.join(run.sourceBook.bookContentFolderPath)); @@ -498,7 +469,7 @@ describe('BookTocManagerTests', function () { }); it('Add section to section', async () => { - bookTocManager = new BookTocManager(targetBookModel, sourceBookModel); + bookTocManager = new BookTocManager(sourceBookModel, targetBookModel); await bookTocManager.updateBook(sectionB, sectionC, { 'title': 'Notebook 6', 'file': path.posix.join(path.posix.sep, 'sectionC', 'notebook6') @@ -510,7 +481,7 @@ describe('BookTocManagerTests', function () { }); it('Add notebook to book', async () => { - bookTocManager = new BookTocManager(targetBookModel); + bookTocManager = new BookTocManager(undefined, targetBookModel); await bookTocManager.updateBook(notebook, targetBook); const listFiles = await fs.promises.readdir(run.targetBook.bookContentFolderPath); should(JSON.stringify(listFiles).includes('notebook5.ipynb')).be.true('Notebook 5 should be under the target book content folder'); @@ -526,7 +497,7 @@ describe('BookTocManagerTests', function () { }); should(notebookInToc).be.true('Verify the notebook is in toc before removing'); - bookTocManager = new BookTocManager(); + bookTocManager = new BookTocManager(sourceBookModel); await bookTocManager.removeNotebook(notebook); const listFiles = await fs.promises.readdir(run.sourceBook.bookContentFolderPath); @@ -542,7 +513,7 @@ describe('BookTocManagerTests', function () { }); it('Add duplicated notebook to book', async () => { - bookTocManager = new BookTocManager(targetBookModel); + bookTocManager = new BookTocManager(undefined, targetBookModel); await bookTocManager.updateBook(notebook, targetBook); await bookTocManager.updateBook(duplicatedNotebook, targetBook); const listFiles = await fs.promises.readdir(run.targetBook.bookContentFolderPath); @@ -553,7 +524,7 @@ describe('BookTocManagerTests', function () { it('Recovery method is called after error', async () => { const mockExtensionContext = new MockExtensionContext(); const recoverySpy = sinon.spy(BookTocManager.prototype, 'recovery'); - sinon.stub(BookTocManager.prototype, 'updateBook').throws(new Error('Unexpected error.')); + sinon.stub(BookTocManager.prototype, 'updateTOC').throws(new Error('Unexpected error.')); const bookTreeViewProvider = new BookTreeViewProvider([], mockExtensionContext, false, 'bookTreeView', NavigationProviders.NotebooksNavigator); const results: quickPickResults = { book: targetBook,