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
This commit is contained in:
Barbara Valdez
2021-03-30 18:44:52 -07:00
committed by GitHub
parent b774f09b6c
commit 8aa222d1c8
13 changed files with 428 additions and 247 deletions

View File

@@ -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"
},
{

View File

@@ -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",

View File

@@ -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<void> | undefined = undefined;
private _queuedPromises: Deferred<void>[] = [];
@@ -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;
}
}

View File

@@ -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<void>;
removeNotebook(element: BookTreeItem): Promise<void>;
createBook(bookContentPath: string, contentFolder: string): Promise<void>;
recovery(): Promise<void>
addNewFile(pathDetails: TocEntryPathHandler, bookItem: BookTreeItem): Promise<void>;
recovery(): Promise<void>;
}
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<string, string> = new Map<string, string>();
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<void> {
async updateTOC(version: BookVersion, tocPath: string, findSection?: JupyterBookSection, addSection?: JupyterBookSection): Promise<void> {
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<void> {
async moveSectionFiles(section: BookTreeItem, book: BookTreeItem): Promise<void> {
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<void> {
async moveFile(file: BookTreeItem, book: BookTreeItem): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<string> {

View File

@@ -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, '');

View File

@@ -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<BookTreeIte
const pickedSection = selectionResults.quickPickSection;
const updateBook = selectionResults.book;
const targetSection = pickedSection.detail !== undefined ? updateBook.findChildSection(pickedSection.detail) : undefined;
if (movingElement.tableOfContents.sections) {
if (movingElement.contextValue === 'savedNotebook' || movingElement.contextValue === 'savedBookNotebook') {
let sourceBook = this.books.find(book => 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<BookTreeIte
}
}
async createMarkdownFile(bookItem: BookTreeItem): Promise<void> {
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<void> {
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<void> {
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<BookTreeIte
let destinationUri: vscode.Uri = vscode.Uri.file(path.join(pickedFolder.fsPath, path.basename(this.currentBook.bookPath)));
if (destinationUri) {
if (await fs.pathExists(destinationUri.fsPath)) {
let doReplace = await confirmReplace(this.prompter);
let doReplace = await confirmMessageDialog(this.prompter, loc.confirmReplace);
if (!doReplace) {
return undefined;
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { FileExtension } from '../common/utils';
export class TocEntryPathHandler {
public readonly fileInTocEntry: string;
public readonly titleInTocEntry: string;
public readonly fileExtension: FileExtension;
constructor(public readonly filePath: string, public readonly bookRoot: string, title?: string) {
const relativePath = path.relative(bookRoot, filePath);
const pathDetails = path.parse(relativePath);
this.fileInTocEntry = relativePath.replace(pathDetails.ext, '');
this.titleInTocEntry = title ?? pathDetails.name;
this.fileExtension = pathDetails.ext === FileExtension.Notebook ? FileExtension.Notebook : FileExtension.Markdown;
}
}

View File

@@ -94,8 +94,18 @@ export const name = localize('name', "Name");
export const saveLocation = localize('saveLocation', "Save location");
export const contentFolder = localize('contentFolder', "Content folder (Optional)");
export const msgContentFolderError = localize('msgContentFolderError', "Content folder path does not exist");
export const msgSaveFolderError = localize('msgSaveFolderError', "Save location path does not exist");
export const msgSaveFolderError = localize('msgSaveFolderError', "Save location path does not exist.");
export function msgCreateBookWarningMsg(file: string): string { return localize('msgCreateBookWarningMsg', "Error while trying to access: {0}", file); }
// Add a notebook dialog constants
export const newNotebook = localize('newNotebook', "New Notebook (Preview)");
export const newMarkdown = localize('newMarkdown', "New Markdown (Preview)");
export const fileExtension = localize('fileExtension', "File Extension");
export const confirmOverwrite = localize('confirmOverwrite', "File already exists. Are you sure you want to overwrite this file?");
export const title = localize('title', "Title");
export const fileName = localize('fileName', "File Name");
export const msgInvalidSaveFolder = localize('msgInvalidSaveFolder', "Save location path is not valid.");
export function msgDuplicadFileName(file: string): string { return localize('msgDuplicadFileName', "File {0} already exists in the destination folder", file); }

View File

@@ -12,8 +12,8 @@ import * as azdata from 'azdata';
import * as crypto from 'crypto';
import { notebookLanguages, notebookConfigKey, pinnedBooksConfigKey, AUTHTYPE, INTEGRATED_AUTH, KNOX_ENDPOINT_PORT, KNOX_ENDPOINT_SERVER } from './constants';
import { IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
import * as loc from '../common/localizedConstants';
import { BookTreeItemFormat, BookTreeItemType } from '../book/bookTreeItem';
import * as loc from './localizedConstants';
const localize = nls.loadMessageBundle();
@@ -488,11 +488,30 @@ export interface IBookNotebook {
notebookPath: string;
}
export enum FileExtension {
Markdown = '.md',
Notebook = '.ipynb'
}
//Confirmation message dialog
export async function confirmReplace(prompter: IPrompter): Promise<boolean> {
export async function confirmMessageDialog(prompter: IPrompter, msg: string): Promise<boolean> {
return await prompter.promptSingle<boolean>(<IQuestion>{
type: QuestionTypes.confirm,
message: loc.confirmReplace,
message: msg,
default: false
});
}
export async function selectFolder(): Promise<string | undefined> {
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;
}

View File

@@ -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<void> {
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<void> {
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<boolean> {
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;
}
}
}

View File

@@ -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<boolean> {
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);

View File

@@ -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);
}));

View File

@@ -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,