diff --git a/extensions/notebook/src/book/bookModel.ts b/extensions/notebook/src/book/bookModel.ts index a835fc67e0..17fa2f3035 100644 --- a/extensions/notebook/src/book/bookModel.ts +++ b/extensions/notebook/src/book/bookModel.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import * as yaml from 'js-yaml'; -import { BookTreeItem, BookTreeItemType, BookTreeItemFormat } from './bookTreeItem'; +import { BookTreeItem, BookTreeItemFormat } from './bookTreeItem'; import * as constants from '../common/constants'; import * as path from 'path'; import * as fileServices from 'fs'; @@ -13,7 +13,7 @@ import * as fs from 'fs-extra'; import * as loc from '../common/localizedConstants'; import { IJupyterBookToc, JupyterBookSection } from '../contracts/content'; import { convertFrom, getContentPath, BookVersion } from './bookVersionHandler'; -import { debounce, IPinnedNotebook } from '../common/utils'; +import { debounce, IPinnedNotebook, BookTreeItemType } from '../common/utils'; import { Deferred } from '../common/promise'; const fsPromises = fileServices.promises; const content = 'content'; @@ -243,7 +243,8 @@ export class BookModel { treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed, isUntitled: this.openAsUntitled, version: book.version, - parent: element + parent: element, + hierarchyId: this.generateHierarchyId(i, element.book.hierarchyId) }, { light: this._extensionContext.asAbsolutePath('resources/light/link.svg'), @@ -269,7 +270,8 @@ export class BookModel { treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed, isUntitled: this.openAsUntitled, version: book.version, - parent: element + parent: element, + hierarchyId: this.generateHierarchyId(i, element.book.hierarchyId) }, { light: this._extensionContext.asAbsolutePath('resources/light/notebook.svg'), @@ -301,7 +303,8 @@ export class BookModel { treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed, isUntitled: this.openAsUntitled, version: book.version, - parent: element + parent: element, + hierarchyId: this.generateHierarchyId(i, element.book.hierarchyId) }, { light: this._extensionContext.asAbsolutePath('resources/light/markdown.svg'), @@ -331,6 +334,15 @@ export class BookModel { return treeItems; } + /** + * Creates a hierarchyId used to identify a tree item's descendants. + * @param treeItemIndex - tree item index based on the book toc. This index is generated when loading a section. + * @param hierarchyId (Optional) - hierarchyId of the parent element + */ + private generateHierarchyId(treeItemIndex: number, hierarchyId?: string): string { + return hierarchyId ? hierarchyId.concat('/', treeItemIndex.toString()) : treeItemIndex.toString(); + } + /** * Recursively parses out a section of a Jupyter Book. * @param section The input data to parse diff --git a/extensions/notebook/src/book/bookTocManager.ts b/extensions/notebook/src/book/bookTocManager.ts index e0a48b48ae..2acabb186f 100644 --- a/extensions/notebook/src/book/bookTocManager.ts +++ b/extensions/notebook/src/book/bookTocManager.ts @@ -12,7 +12,7 @@ import * as vscode from 'vscode'; import * as loc from '../common/localizedConstants'; import { BookModel } from './bookModel'; import { TocEntryPathHandler } from './tocEntryPathHandler'; -import { FileExtension } from '../common/utils'; +import { FileExtension, BookTreeItemType } from '../common/utils'; export interface IBookTocManager { updateBook(sources: BookTreeItem[], target: BookTreeItem, targetSection?: JupyterBookSection): Promise; @@ -20,6 +20,7 @@ export interface IBookTocManager { createBook(bookContentPath: string, contentFolder: string): Promise; addNewTocEntry(pathDetails: TocEntryPathHandler, bookItem: BookTreeItem, isSection?: boolean): Promise; recovery(): Promise; + enableDnd: boolean; } export interface quickPickResults { @@ -41,6 +42,7 @@ export class BookTocManager implements IBookTocManager { public tocFiles: Map = new Map(); private sourceBookContentPath: string; private targetBookContentPath: string; + private _enableDnd: boolean = false; constructor(private _sourceBook?: BookModel, private _targetBook?: BookModel) { this._targetBook?.unwatchTOC(); @@ -279,14 +281,19 @@ export class BookTocManager implements IBookTocManager { for (const elem of files) { if (elem.file) { let fileName = undefined; + // the toc does not provide the extension of the file, so we need to try for notebooks and markdown try { this.movedFiles.set(path.join(this.sourceBookContentPath, elem.file).concat('.ipynb'), path.join(this.targetBookContentPath, elem.file).concat('.ipynb')); await fs.move(path.join(this.sourceBookContentPath, elem.file).concat('.ipynb'), path.join(this.targetBookContentPath, elem.file).concat('.ipynb'), { overwrite: false }); } catch (error) { if (error.code === 'EEXIST') { + // if the file already exists in destination, then rename it before moving it. fileName = await this.renameFile(path.join(this.sourceBookContentPath, elem.file).concat('.ipynb'), path.join(this.targetBookContentPath, elem.file).concat('.ipynb')); + } else if (error.code === 'ENOENT') { + // if it doesnt exist then remove it from movedFiles + this.movedFiles.delete(path.join(this.sourceBookContentPath, elem.file).concat('.ipynb')); } - else if (error.code !== 'ENOENT') { + else { throw (error); } } @@ -295,9 +302,13 @@ export class BookTocManager implements IBookTocManager { await fs.move(path.join(this.sourceBookContentPath, elem.file).concat('.md'), path.join(this.targetBookContentPath, elem.file).concat('.md'), { overwrite: false }); } catch (error) { if (error.code === 'EEXIST') { + // if the file already exists in destination, then rename it before moving it. fileName = await this.renameFile(path.join(this.sourceBookContentPath, elem.file).concat('.md'), path.join(this.targetBookContentPath, elem.file).concat('.md')); + } else if (error.code === 'ENOENT') { + // if it doesnt exist then remove it from movedFiles + this.movedFiles.delete(path.join(this.sourceBookContentPath, elem.file).concat('.md')); } - else if (error.code !== 'ENOENT') { + else { throw (error); } } @@ -367,8 +378,8 @@ export class BookTocManager implements IBookTocManager { let fileName = undefined; try { // no op if the notebook is already in the dest location + this.movedFiles.set(file.book.contentPath, path.join(rootPath, filePath.base)); if (file.book.contentPath !== path.join(rootPath, filePath.base)) { - this.movedFiles.set(file.book.contentPath, path.join(rootPath, filePath.base)); await fs.move(file.book.contentPath, path.join(rootPath, filePath.base), { overwrite: false }); } } catch (error) { @@ -403,9 +414,13 @@ export class BookTocManager implements IBookTocManager { */ public async updateBook(sources: BookTreeItem[], target: BookTreeItem, section?: JupyterBookSection): Promise { for (let element of sources) { + if (element.contextValue === BookTreeItemType.savedBook || this.isParent(element, target, section) || this.isDescendant(element, target)) { + // no op if the moving element is a book, the target dest is descendant of the moving element, the target dest is already the moving element parent + return; + } try { - const targetSection = section ? section : (target.contextValue === 'section' ? { file: target.book.page.file, title: target.book.page.title } : undefined); - if (element.contextValue === 'section') { + const targetSection = section ? section : (target.contextValue === BookTreeItemType.section ? { file: target.book.page.file, title: target.book.page.title } : undefined); + if (element.contextValue === BookTreeItemType.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, target); @@ -418,7 +433,7 @@ export class BookTocManager implements IBookTocManager { // 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, target); - if (element.contextValue === 'savedBookNotebook' || element.contextValue === 'Markdown') { + if (element.contextValue === BookTreeItemType.savedBookNotebook || element.contextValue === BookTreeItemType.Markdown) { // remove notebook entry from book toc await this.updateTOC(element.book.version, element.tableOfContentsPath, findSection, undefined); } else { @@ -446,7 +461,7 @@ export class BookTocManager implements IBookTocManager { public async addNewTocEntry(pathDetails: TocEntryPathHandler, bookItem: BookTreeItem, isSection?: boolean): Promise { let findSection: JupyterBookSection | undefined = undefined; await fs.writeFile(pathDetails.filePath, ''); - if (bookItem.contextValue === 'section') { + if (bookItem.contextValue === BookTreeItemType.section) { findSection = { file: bookItem.book.page.file, title: bookItem.book.page.title }; } let fileEntryInToc: JupyterBookSection = { @@ -481,6 +496,37 @@ export class BookTocManager implements IBookTocManager { await this._sourceBook.reinitializeContents(); } + /** + * Checks that the targetTreeItem is descendant of the tree item used by the onDrop method. + * @param treeItem The moving element when using dnd. + * @param targetTreeItem The target element where the moving element is dropped. + */ + isDescendant(treeItem: BookTreeItem, targetTreeItem: BookTreeItem): boolean { + return this._enableDnd && treeItem.rootContentPath === targetTreeItem.rootContentPath && targetTreeItem.book.hierarchyId?.includes(treeItem.book.hierarchyId); + } + + /** + * Checks that the book tree item is the parent of the passed element used by the onDrop and the moveTo method. + * @param treeItem The child of the parent tree item. + * @param parentTreeItem The parent of the passed element or the Saved Book tree item. + * @param section (Optional) In case the parentTreeItem is the saved book, verify that the passed Jupyter Book Section is the parent of the treeItem. + */ + isParent(treeItem: BookTreeItem, parentTreeItem: BookTreeItem, section?: JupyterBookSection): boolean { + if (section) { + return section.file === treeItem.book.parent?.uri; + } + + return treeItem.book.parent?.uri === parentTreeItem.uri && + treeItem.book.parent?.rootContentPath === parentTreeItem.rootContentPath && + treeItem.book.parent?.contextValue === parentTreeItem.contextValue && + treeItem.book.parent?.sections.length === parentTreeItem.sections.length && + treeItem.book.parent?.book.contentPath === parentTreeItem.book.contentPath; + } + + public set enableDnd(useDnd: boolean) { + this._enableDnd = useDnd; + } + public get modifiedDir(): Set { return this._modifiedDirectory; } diff --git a/extensions/notebook/src/book/bookTreeItem.ts b/extensions/notebook/src/book/bookTreeItem.ts index de04efd426..6706a72fbd 100644 --- a/extensions/notebook/src/book/bookTreeItem.ts +++ b/extensions/notebook/src/book/bookTreeItem.ts @@ -7,23 +7,9 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import { JupyterBookSection, IJupyterBookToc } from '../contracts/content'; import * as loc from '../common/localizedConstants'; -import { isBookItemPinned, getNotebookType } from '../common/utils'; +import { isBookItemPinned, getNotebookType, BookTreeItemType } from '../common/utils'; import { BookVersion, getContentPath, getTocPath } from './bookVersionHandler'; -export enum BookTreeItemType { - Book = 'Book', - Notebook = 'Notebook', - Markdown = 'Markdown', - ExternalLink = 'ExternalLink', - providedBook = 'providedBook', - savedBook = 'savedBook', - unsavedNotebook = 'unsavedNotebook', - savedNotebook = 'savedNotebook', - pinnedNotebook = 'pinnedNotebook', - section = 'section', - savedBookNotebook = 'savedBookNotebook' -} - export interface BookTreeItemFormat { title: string; contentPath: string; @@ -36,6 +22,12 @@ export interface BookTreeItemFormat { version?: BookVersion; parent?: BookTreeItem; hasChildren?: boolean; + /** + * Use to identify the hierarchy of nested book tree items. + * For instance, the hierarchyId of the first node would be "0" and its children would have + * a hierarchyId starting with "0/" + */ + hierarchyId?: string; } export class BookTreeItem extends vscode.TreeItem { diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts index b282e0eabf..8c1c994b1b 100644 --- a/extensions/notebook/src/book/bookTreeView.ts +++ b/extensions/notebook/src/book/bookTreeView.ts @@ -10,13 +10,13 @@ import * as fs from 'fs-extra'; import * as constants from '../common/constants'; import { IPrompter } from '../prompts/question'; import CodeAdapter from '../prompts/adapter'; -import { BookTreeItem, BookTreeItemType } from './bookTreeItem'; +import { BookTreeItem } from './bookTreeItem'; import { BookModel } from './bookModel'; import { Deferred } from '../common/promise'; import { IBookTrustManager, BookTrustManager } from './bookTrustManager'; import * as loc from '../common/localizedConstants'; import * as glob from 'fast-glob'; -import { getPinnedNotebooks, confirmMessageDialog, getNotebookType, FileExtension, IPinnedNotebook } from '../common/utils'; +import { getPinnedNotebooks, confirmMessageDialog, getNotebookType, FileExtension, IPinnedNotebook, BookTreeItemType } from '../common/utils'; import { IBookPinManager, BookPinManager } from './bookPinManager'; import { BookTocManager, IBookTocManager, quickPickResults } from './bookTocManager'; import { CreateBookDialog } from '../dialog/createBookDialog'; @@ -755,6 +755,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider book.bookPath === target.book.root); for (let [book, items] of sourcesByBook) { this.bookTocManager = new BookTocManager(book, targetBook); + this.bookTocManager.enableDnd = true; await this.bookTocManager.updateBook(items, target); } } diff --git a/extensions/notebook/src/common/utils.ts b/extensions/notebook/src/common/utils.ts index d8806ad9a6..c75712f963 100644 --- a/extensions/notebook/src/common/utils.ts +++ b/extensions/notebook/src/common/utils.ts @@ -12,7 +12,7 @@ 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 { BookTreeItemFormat, BookTreeItemType } from '../book/bookTreeItem'; +import { BookTreeItemFormat } from '../book/bookTreeItem'; import * as loc from './localizedConstants'; const localize = nls.loadMessageBundle(); @@ -442,6 +442,20 @@ export function isBookItemPinned(notebookPath: string): boolean { return false; } +export enum BookTreeItemType { + Book = 'Book', + Notebook = 'Notebook', + Markdown = 'Markdown', + ExternalLink = 'ExternalLink', + providedBook = 'providedBook', + savedBook = 'savedBook', + unsavedNotebook = 'unsavedNotebook', + savedNotebook = 'savedNotebook', + pinnedNotebook = 'pinnedNotebook', + section = 'section', + savedBookNotebook = 'savedBookNotebook' +} + export function getNotebookType(book: BookTreeItemFormat): BookTreeItemType { if (book.tableOfContents.sections) { return BookTreeItemType.savedBookNotebook; diff --git a/extensions/notebook/src/dialog/addTocEntryDialog.ts b/extensions/notebook/src/dialog/addTocEntryDialog.ts index 43630c1297..944ab4f0ef 100644 --- a/extensions/notebook/src/dialog/addTocEntryDialog.ts +++ b/extensions/notebook/src/dialog/addTocEntryDialog.ts @@ -7,10 +7,10 @@ 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 { confirmMessageDialog, FileExtension, BookTreeItemType } from '../common/utils'; import { IPrompter } from '../prompts/question'; import CodeAdapter from '../prompts/adapter'; -import { BookTreeItem, BookTreeItemType } from '../book/bookTreeItem'; +import { BookTreeItem } from '../book/bookTreeItem'; import { TocEntryPathHandler } from '../book/tocEntryPathHandler'; export class AddTocEntryDialog { diff --git a/extensions/notebook/src/test/book/addTocEntry.test.ts b/extensions/notebook/src/test/book/addTocEntry.test.ts index bdc21bdcb4..85949f1242 100644 --- a/extensions/notebook/src/test/book/addTocEntry.test.ts +++ b/extensions/notebook/src/test/book/addTocEntry.test.ts @@ -10,7 +10,7 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import { AddTocEntryDialog } from '../../dialog/addTocEntryDialog'; import { IBookTocManager } from '../../book/bookTocManager'; -import { BookTreeItem, BookTreeItemFormat, BookTreeItemType } from '../../book/bookTreeItem'; +import { BookTreeItem, BookTreeItemFormat } from '../../book/bookTreeItem'; import * as utils from '../../common/utils'; import * as sinon from 'sinon'; import { TocEntryPathHandler } from '../../book/tocEntryPathHandler'; @@ -27,7 +27,7 @@ describe('Add File Dialog', function () { bookTocManager = mockBookManager.object; let mockTreeItem = TypeMoq.Mock.ofType(); - mockTreeItem.setup(i => i.contextValue).returns(() => BookTreeItemType.savedBook); + mockTreeItem.setup(i => i.contextValue).returns(() => utils.BookTreeItemType.savedBook); mockTreeItem.setup(i => i.rootContentPath).returns(() => ''); let mockItemFormat = TypeMoq.Mock.ofType(); @@ -100,7 +100,7 @@ describe('Add File Dialog', function () { mockBookManager.setup(m => m.addNewTocEntry(TypeMoq.It.isAny(), TypeMoq.It.isAny(),TypeMoq.It.isAny())).returns((path, item) => { testPathDetails.push(path); return Promise.resolve(); }); let mockTreeItem = TypeMoq.Mock.ofType(); - mockTreeItem.setup(i => i.contextValue).returns(() => BookTreeItemType.savedBook); + mockTreeItem.setup(i => i.contextValue).returns(() => utils.BookTreeItemType.savedBook); mockTreeItem.setup(i => i.rootContentPath).returns(() => testDir); fileDialog = new AddTocEntryDialog(mockBookManager.object, mockTreeItem.object, fileExtension); diff --git a/extensions/notebook/src/test/book/book.test.ts b/extensions/notebook/src/test/book/book.test.ts index 2227b513f9..f45e4e6fa6 100644 --- a/extensions/notebook/src/test/book/book.test.ts +++ b/extensions/notebook/src/test/book/book.test.ts @@ -12,10 +12,10 @@ import * as rimraf from 'rimraf'; import * as os from 'os'; import * as uuid from 'uuid'; import { BookTreeViewProvider } from '../../book/bookTreeView'; -import { BookTreeItem, BookTreeItemType } from '../../book/bookTreeItem'; +import { BookTreeItem } from '../../book/bookTreeItem'; import { promisify } from 'util'; import { MockExtensionContext } from '../common/stubs'; -import { exists } from '../../common/utils'; +import { exists, BookTreeItemType } from '../../common/utils'; import { BookModel } from '../../book/bookModel'; import { BookTrustManager } from '../../book/bookTrustManager'; import { NavigationProviders } from '../../common/constants'; diff --git a/extensions/notebook/src/test/book/bookPinManager.test.ts b/extensions/notebook/src/test/book/bookPinManager.test.ts index 06c297391c..3b838d3e49 100644 --- a/extensions/notebook/src/test/book/bookPinManager.test.ts +++ b/extensions/notebook/src/test/book/bookPinManager.test.ts @@ -8,11 +8,11 @@ import * as path from 'path'; import * as TypeMoq from 'typemoq'; import * as constants from '../../common/constants'; import { IBookPinManager, BookPinManager } from '../../book/bookPinManager'; -import { BookTreeItem, BookTreeItemFormat, BookTreeItemType } from '../../book/bookTreeItem'; +import { BookTreeItem, BookTreeItemFormat } from '../../book/bookTreeItem'; import * as vscode from 'vscode'; import { BookModel } from '../../book/bookModel'; import * as sinon from 'sinon'; -import { isBookItemPinned } from '../../common/utils'; +import { isBookItemPinned, BookTreeItemType } from '../../common/utils'; describe('BookPinManagerTests', function () { diff --git a/extensions/notebook/src/test/book/bookTocManager.test.ts b/extensions/notebook/src/test/book/bookTocManager.test.ts index 200c532a56..60845851e0 100644 --- a/extensions/notebook/src/test/book/bookTocManager.test.ts +++ b/extensions/notebook/src/test/book/bookTocManager.test.ts @@ -5,23 +5,22 @@ import * as should from 'should'; import * as path from 'path'; import { BookTocManager, hasSections } from '../../book/bookTocManager'; -import { BookTreeItem, BookTreeItemFormat, BookTreeItemType } from '../../book/bookTreeItem'; +import { BookTreeItem, BookTreeItemFormat } from '../../book/bookTreeItem'; import * as sinon from 'sinon'; import { IJupyterBookSectionV1, IJupyterBookSectionV2, JupyterBookSection } from '../../contracts/content'; import * as fs from 'fs-extra'; import * as os from 'os'; import * as uuid from 'uuid'; -import { exists } from '../../common/utils'; import * as rimraf from 'rimraf'; import { promisify } from 'util'; import { BookModel } from '../../book/bookModel'; import { MockExtensionContext } from '../common/stubs'; import { BookTreeViewProvider } from '../../book/bookTreeView'; import { NavigationProviders } from '../../common/constants'; -import { BookVersion } from '../../book/bookVersionHandler'; +import { BookVersion, getContentPath, getTocPath } from '../../book/bookVersionHandler'; import * as yaml from 'js-yaml'; import { TocEntryPathHandler } from '../../book/tocEntryPathHandler'; -import * as utils from '../../common/utils'; +import { exists, BookTreeItemType, FileExtension, generateGuid } from '../../common/utils'; export function equalTOC(actualToc: IJupyterBookSectionV2[], expectedToc: IJupyterBookSectionV2[]): boolean { for (let [i, section] of actualToc.entries()) { @@ -54,28 +53,31 @@ function BookModelStub(root: string, bookItem: BookTreeItem, extension: MockExte 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]; +function createBookTreeItemFormat(item: any): BookTreeItemFormat { + const pageFormat = item.type === BookTreeItemType.savedBook ? item.tocEntry : item.tocEntry[0]; return { - title: item.sectionName, + title: item.title, contentPath: item.contentPath, - root: root, + root: item.root, tableOfContents: { - sections: sections + sections: item.tocEntry }, isUntitled: undefined, treeItemCollapsibleState: undefined, type: item.type, - version: version, + version: item.version, page: pageFormat }; } +function createBookTreeItem(itemFormat: BookTreeItemFormat, tocPath?: string): BookTreeItem { + let treeItem = new BookTreeItem(itemFormat, undefined); + treeItem.contextValue = itemFormat.type; + treeItem.tableOfContentsPath = tocPath; + treeItem.book.version = itemFormat.version; + return treeItem; +} + describe('BookTocManagerTests', function () { describe('CreatingBooks', () => { let notebooks: string[]; @@ -150,298 +152,268 @@ describe('BookTocManagerTests', function () { let sourceBookModel: BookModel; let targetBookModel: BookModel; let targetBook: BookTreeItem; + let sourceBook: BookTreeItem; let sectionC: BookTreeItem; let sectionA: BookTreeItem; let sectionB: BookTreeItem; - let notebook: BookTreeItem; + let notebook1: BookTreeItem; + let notebook5: BookTreeItem; let duplicatedNotebook: BookTreeItem; let sourceBookFolderPath: string = path.join(os.tmpdir(), uuid.v4(), 'sourceBook'); let targetBookFolderPath: string = path.join(os.tmpdir(), uuid.v4(), 'targetBook'); let duplicatedNotebookPath: string = path.join(os.tmpdir(), uuid.v4(), 'duplicatedNotebook'); - let runs = [ - { - it: 'using the jupyter-book legacy version < 0.7.0', - version: BookVersion.v1, - sourceBook: { - 'rootBookFolderPath': sourceBookFolderPath, - 'bookContentFolderPath': path.posix.join(sourceBookFolderPath, 'content'), - 'tocPath': path.posix.join(sourceBookFolderPath, '_data', 'toc.yml'), - 'readme': path.posix.join(sourceBookFolderPath, 'content', 'readme.md'), - 'toc': [ - { - 'title': 'Notebook 1', - 'file': path.posix.join(path.posix.sep, 'sectionA', 'notebook1') - }, - { - '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': [ - { - 'title': 'Notebook 1', - 'file': path.posix.join(path.posix.sep, 'sectionA', 'notebook1') - }, - { - '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': [ - { - 'title': 'Notebook 3', - 'file': path.posix.join(path.posix.sep, 'sectionB', 'notebook3') - }, - { - 'title': 'Notebook 4', - 'file': path.posix.join(path.posix.sep, 'sectionB', 'notebook4') - } - ], - 'type': BookTreeItemType.section - }, - notebook5: { - '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, - 'bookContentFolderPath': path.posix.join(targetBookFolderPath, 'content'), - 'tocPath': path.posix.join(targetBookFolderPath, '_data', 'toc.yml'), - 'readme': path.posix.join(targetBookFolderPath, 'content', 'readme.md'), - 'toc': [ - { - 'title': 'Welcome page', - 'file': path.posix.join(path.posix.sep, 'readme'), - }, - { - 'title': 'Section C', - 'file': path.posix.join(path.posix.sep, 'sectionC', 'readme'), - 'sections': [ - { - 'title': 'Notebook 6', - 'file': path.posix.join(path.posix.sep, 'sectionC', 'notebook6') - } - ] - } - ], - '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', - version: BookVersion.v2, - sourceBook: { - 'rootBookFolderPath': sourceBookFolderPath, - 'bookContentFolderPath': sourceBookFolderPath, - 'tocPath': path.posix.join(sourceBookFolderPath, '_toc.yml'), - 'readme': path.posix.join(sourceBookFolderPath, 'readme.md') - }, - sectionA: { - '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': [ - { - 'title': 'Notebook 1', - 'file': path.posix.join(path.posix.sep, 'sectionA', 'notebook1') - }, - { - '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': [ - { - 'title': 'Notebook 3', - 'file': path.posix.join(path.posix.sep, 'sectionB', 'notebook3') - }, - { - 'title': 'Notebook 4', - 'file': path.posix.join(path.posix.sep, 'sectionB', 'notebook4') - } - ], - 'type': BookTreeItemType.section - }, - notebook5: { - '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, - 'bookContentFolderPath': targetBookFolderPath, - 'tocPath': path.posix.join(targetBookFolderPath, '_toc.yml'), - 'readme': path.posix.join(targetBookFolderPath, 'readme.md'), - 'toc': [ - { - 'title': 'Welcome', - 'file': path.posix.join(path.posix.sep, 'readme'), - }, - { - 'title': 'Section C', - 'file': path.posix.join(path.posix.sep, 'sectionC', 'readme'), - 'sections': [ - { - 'title': 'Notebook 6', - 'file': path.posix.join(path.posix.sep, 'sectionC', 'notebook6') - } - ] - } - ], - '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 - } - } + let versions = [ + BookVersion.v1, + BookVersion.v2 ]; - runs.forEach(function (run) { + let testRuns = versions.map(v => { + const sourceBookContentFolder = getContentPath(v, sourceBookFolderPath, ''); + const targetBookContentFolder = getContentPath(v, targetBookFolderPath, ''); + return { + it: `using book version: ${v}`, + version: v, + sourceBook: { + 'title': 'Source Book', + 'root': sourceBookFolderPath, + 'contentFolder': sourceBookContentFolder, + 'tocPath': getTocPath(v, sourceBookFolderPath), + 'contentPath': path.posix.join(sourceBookContentFolder, 'readme.md'), + 'tocEntry': [ + { + title: 'readme', + file: path.posix.join(path.posix.sep, 'readme') + }, + { + title: 'Section A', + file: path.posix.join(path.posix.sep, 'sectionA', 'readme'), + sections: [ + { + 'title': 'Notebook 1', + 'file': path.posix.join(path.posix.sep, 'sectionA', 'notebook1') + }, + { + 'title': 'Notebook 2', + 'file': path.posix.join(path.posix.sep, 'sectionA', 'notebook2') + } + ] + }, + { + title: 'Section B', + file: path.posix.join(path.posix.sep, 'sectionB', 'readme'), + sections: [ + { + 'title': 'Notebook 3', + 'file': path.posix.join(path.posix.sep, 'sectionB', 'notebook3') + }, + { + 'title': 'Notebook 4', + 'file': path.posix.join(path.posix.sep, 'sectionB', 'notebook4') + } + ] + } + ], + 'type': BookTreeItemType.savedBook, + 'version': v, + 'files': [ + path.posix.join(sourceBookContentFolder, 'readme.md'), + path.posix.join(sourceBookContentFolder, 'sectionA', 'readme.md'), + path.posix.join(sourceBookContentFolder, 'sectionA', 'notebook1.ipynb'), + path.posix.join(sourceBookContentFolder, 'sectionA', 'notebook2.ipynb'), + path.posix.join(sourceBookContentFolder, 'sectionB', 'readme.md'), + path.posix.join(sourceBookContentFolder, 'sectionB', 'notebook3.ipynb'), + path.posix.join(sourceBookContentFolder, 'sectionB', 'notebook4.ipynb'), + ] + }, + sectionA: { + 'title': 'Section A', + 'root': sourceBookFolderPath, + 'contentFolder': getContentPath(v, sourceBookFolderPath, ''), + 'tocPath': getTocPath(v, sourceBookFolderPath), + 'contentPath': getContentPath(v, sourceBookFolderPath, path.posix.join('sectionA', 'readme.md')), + 'tocEntry': [ + { + title: 'Section A', + file: path.posix.join(path.posix.sep, 'sectionA', 'readme'), + sections: [ + { + 'title': 'Notebook 1', + 'file': path.posix.join(path.posix.sep, 'sectionA', 'notebook1') + }, + { + 'title': 'Notebook 2', + 'file': path.posix.join(path.posix.sep, 'sectionA', 'notebook2') + } + ] + + }], + 'type': BookTreeItemType.section, + 'version': v + }, + sectionB: { + 'title': 'Section B', + 'root': sourceBookFolderPath, + 'contentFolder': getContentPath(v, sourceBookFolderPath, ''), + 'tocPath': getTocPath(v, sourceBookFolderPath), + 'contentPath': getContentPath(v, sourceBookFolderPath, path.posix.join('sectionB', 'readme.md')), + 'tocEntry': [ + { + title: 'Section B', + file: path.posix.join(path.posix.sep, 'sectionB', 'readme'), + sections: [ + { + 'title': 'Notebook 3', + 'file': path.posix.join(path.posix.sep, 'sectionB', 'notebook3') + }, + { + 'title': 'Notebook 4', + 'file': path.posix.join(path.posix.sep, 'sectionB', 'notebook4') + } + ] + + }], + 'type': BookTreeItemType.section, + 'version': v + }, + targetBook: { + 'title': 'Target Book', + 'root': targetBookFolderPath, + 'contentFolder': getContentPath(v, targetBookFolderPath, ''), + 'tocPath': getTocPath(v, targetBookFolderPath), + 'contentPath': getContentPath(v, targetBookFolderPath, 'readme.md'), + 'tocEntry': [ + { + title: 'readme', + file: path.posix.join(path.posix.sep, 'readme') + }, + { + title: 'Section C', + file: path.posix.join(path.posix.sep, 'sectionC', 'readme'), + sections: [ + { + 'title': 'Notebook 6', + 'file': path.posix.join(path.posix.sep, 'sectionC', 'notebook6') + } + ] + }], + 'type': BookTreeItemType.savedBook, + 'version': v, + 'files': [ + path.posix.join(targetBookContentFolder, 'readme.md'), + path.posix.join(targetBookContentFolder, 'sectionC', 'readme.md'), + path.posix.join(targetBookContentFolder, 'sectionC', 'notebook6.ipynb'), + ] + }, + sectionC: { + 'title': 'Section C', + 'root': targetBookFolderPath, + 'contentFolder': getContentPath(v, targetBookFolderPath, ''), + 'tocPath': getTocPath(v, targetBookFolderPath), + 'contentPath': getContentPath(v, targetBookFolderPath, path.posix.join('sectionC', 'readme.md')), + 'tocEntry': [ + { + title: 'Section C', + file: path.posix.join(path.posix.sep, 'sectionC', 'readme'), + sections: [ + { + 'title': 'Notebook 6', + 'file': path.posix.join(path.posix.sep, 'sectionC', 'notebook6') + } + ] + }], + 'type': BookTreeItemType.section, + 'version': v + }, + notebook1: { + 'title': 'Notebook 1', + 'root': sourceBookFolderPath, + 'contentFolder': sourceBookContentFolder, + 'tocPath': getTocPath(v, sourceBookFolderPath), + 'contentPath': path.posix.join(sourceBookContentFolder, 'sectionA', 'notebook1.ipynb'), + 'tocEntry': [ + { + 'title': 'Notebook 1', + 'file': path.posix.join(path.posix.sep, 'sectionA', 'notebook1') + }], + 'type': BookTreeItemType.savedBookNotebook, + 'version': v + }, + notebook5: { + 'title': 'Notebook 5', + 'root': sourceBookFolderPath, + 'contentFolder': sourceBookContentFolder, + 'tocPath': getTocPath(v, sourceBookFolderPath), + 'contentPath': path.posix.join(sourceBookContentFolder, 'notebook5.ipynb'), + 'tocEntry': [ + { + 'title': 'Notebook 5', + 'file': path.posix.join(path.posix.sep, 'notebook5') + } + ], + 'type': BookTreeItemType.savedBookNotebook, + 'version': v + }, + duplicatedNotebook: { + 'title': 'Duplicated Notebook', + 'root': duplicatedNotebookPath, + 'contentFolder': duplicatedNotebookPath, + 'tocPath': undefined, + 'contentPath': path.posix.join(duplicatedNotebookPath, 'notebook5.ipynb'), + 'tocEntry': [ + { + 'title': 'Notebook 5', + 'file': path.posix.join(path.posix.sep, 'notebook5') + } + ], + 'type': BookTreeItemType.savedNotebook + } + } + }); + + testRuns.forEach(function (run) { describe('Editing Books ' + run.it, function (): void { beforeEach(async () => { - let targetBookTreeItemFormat: BookTreeItemFormat = { - contentPath: run.targetBook.readme, - root: run.targetBook.rootBookFolderPath, - tableOfContents: { - sections: run.targetBook.toc - }, - isUntitled: undefined, - title: 'Target Book', - treeItemCollapsibleState: undefined, - type: BookTreeItemType.Book, - version: run.version, - page: run.targetBook.toc - }; - 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); - sectionA = new BookTreeItem(sectionATreeItemFormat, undefined); - sectionB = new BookTreeItem(sectionBTreeItemFormat, undefined); - notebook = new BookTreeItem(notebookTreeItemFormat, undefined); - duplicatedNotebook = new BookTreeItem(duplicatedNbTreeItemFormat, undefined); + const targetBookTreeItemFormat = createBookTreeItemFormat(run.targetBook); + const sourceBookTreeItemFormat = createBookTreeItemFormat(run.sourceBook); + const sectionCTreeItemFormat = createBookTreeItemFormat(run.sectionC); + const sectionATreeItemFormat = createBookTreeItemFormat(run.sectionA); + const sectionBTreeItemFormat = createBookTreeItemFormat(run.sectionB); + const notebook5TreeItemFormat = createBookTreeItemFormat(run.notebook5); + const notebook1TreeItemFormat = createBookTreeItemFormat(run.notebook1); + const duplicatedNbTreeItemFormat = createBookTreeItemFormat(run.duplicatedNotebook); + targetBook = createBookTreeItem(targetBookTreeItemFormat, run.targetBook.tocPath); + sourceBook = createBookTreeItem(sourceBookTreeItemFormat, run.sourceBook.tocPath); + sectionC = createBookTreeItem(sectionCTreeItemFormat, run.sectionC.tocPath); + sectionA = createBookTreeItem(sectionATreeItemFormat, run.sectionA.tocPath); + sectionB = createBookTreeItem(sectionBTreeItemFormat, run.sectionB.tocPath); + notebook1 = createBookTreeItem(notebook1TreeItemFormat, run.notebook1.tocPath); + notebook5 = createBookTreeItem(notebook5TreeItemFormat, run.notebook5.tocPath); + duplicatedNotebook = createBookTreeItem(duplicatedNbTreeItemFormat, run.duplicatedNotebook.tocPath); sectionC.uri = path.posix.join('sectionC', 'readme'); sectionA.uri = path.posix.join('sectionA', 'readme'); sectionB.uri = path.posix.join('sectionB', 'readme'); + notebook1.parent = sectionA; - targetBook.contextValue = 'savedBook'; - sectionA.contextValue = 'section'; - sectionB.contextValue = 'section'; - sectionC.contextValue = 'section'; - notebook.contextValue = 'savedNotebook'; - duplicatedNotebook.contextValue = 'savedNotebook'; + await fs.promises.mkdir(run.targetBook.root, { recursive: true }); + await fs.promises.mkdir(path.dirname(run.sectionA.contentPath), { recursive: true }); + await fs.promises.mkdir(path.dirname(run.sectionB.contentPath), { recursive: true }); + await fs.promises.mkdir(path.dirname(run.sectionC.contentPath), { recursive: true }); + await fs.promises.mkdir(run.duplicatedNotebook.root, { recursive: true }); - sectionC.tableOfContentsPath = run.targetBook.tocPath; - sectionA.tableOfContentsPath = run.sourceBook.tocPath; - sectionB.tableOfContentsPath = run.sourceBook.tocPath; - notebook.tableOfContentsPath = run.sourceBook.tocPath; - duplicatedNotebook.tableOfContentsPath = undefined; + for (let file of run.targetBook.files) { + await fs.writeFile(file, ''); + } - sectionA.sections = run.sectionA.sectionFormat; - sectionB.sections = run.sectionB.sectionFormat; - sectionC.sections = run.sectionC.sectionFormat; - notebook.sections = [run.notebook5.sectionFormat]; - duplicatedNotebook.sections = notebook.sections; + for (let file of run.sourceBook.files) { + await fs.writeFile(file, ''); + } - await fs.promises.mkdir(run.targetBook.bookContentFolderPath, { recursive: true }); - await fs.promises.mkdir(run.sectionA.contentPath, { recursive: true }); - await fs.promises.mkdir(run.sectionB.contentPath, { recursive: true }); - await fs.promises.mkdir(run.sectionC.contentPath, { recursive: true }); - await fs.promises.mkdir(duplicatedNotebookPath, { recursive: true }); - - await fs.writeFile(run.sectionA.notebook1, ''); - await fs.writeFile(run.sectionA.notebook2, ''); - await fs.writeFile(run.sectionB.notebook3, ''); - await fs.writeFile(run.sectionB.notebook4, ''); - await fs.writeFile(run.sectionC.notebook6, ''); await fs.writeFile(run.notebook5.contentPath, ''); - await fs.writeFile(duplicatedNotebook.book.contentPath, ''); - await fs.writeFile(path.join(run.targetBook.rootBookFolderPath, '_config.yml'), 'title: Target Book'); - await fs.writeFile(path.join(run.sourceBook.rootBookFolderPath, '_config.yml'), 'title: Source Book'); - + await fs.writeFile(run.duplicatedNotebook.contentPath, ''); + await fs.writeFile(path.join(run.targetBook.root, '_config.yml'), 'title: Target Book'); + await fs.writeFile(path.join(run.sourceBook.root, '_config.yml'), 'title: Source Book'); if (run.version === 'v1') { await fs.promises.mkdir(path.dirname(run.targetBook.tocPath), { recursive: true }); @@ -455,16 +427,15 @@ describe('BookTocManagerTests', function () { const mockExtensionContext = new MockExtensionContext(); - sourceBookModel = BookModelStub(run.sourceBook.rootBookFolderPath, sectionA, mockExtensionContext); - targetBookModel = BookModelStub(run.targetBook.rootBookFolderPath, targetBook, mockExtensionContext); + sourceBookModel = BookModelStub(run.sourceBook.root, sourceBook, mockExtensionContext); + targetBookModel = BookModelStub(run.targetBook.root, targetBook, mockExtensionContext); }); - it('Add section to book', async () => { 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)); + const listFiles = await fs.promises.readdir(path.join(run.targetBook.contentFolder, 'sectionA')); + const listSourceFiles = await fs.promises.readdir(path.join(run.sourceBook.contentFolder)); should(JSON.stringify(listSourceFiles).includes('sectionA')).be.false('The source book files should not contain the section A files'); should(JSON.stringify(listFiles)).be.equal(JSON.stringify(['notebook1.ipynb', 'notebook2.ipynb', 'readme.md']), 'The files of the section should be moved to the target book folder'); }); @@ -475,21 +446,21 @@ describe('BookTocManagerTests', function () { 'title': 'Notebook 6', 'file': path.posix.join(path.posix.sep, 'sectionC', 'notebook6') }); - const sectionCFiles = await fs.promises.readdir(path.join(run.targetBook.bookContentFolderPath, 'sectionC')); - const sectionBFiles = await fs.promises.readdir(path.join(run.targetBook.bookContentFolderPath, 'sectionB')); + const sectionCFiles = await fs.promises.readdir(path.join(run.targetBook.contentFolder, 'sectionC')); + const sectionBFiles = await fs.promises.readdir(path.join(run.targetBook.contentFolder, 'sectionB')); should(JSON.stringify(sectionCFiles)).be.equal(JSON.stringify(['notebook6.ipynb', 'readme.md']), 'sectionB has been moved under target book content directory'); should(JSON.stringify(sectionBFiles)).be.equal(JSON.stringify(['notebook3.ipynb', 'notebook4.ipynb', 'readme.md']), ' Verify that the files on sectionB had been moved to the targetBook'); }); it('Add notebook to book', async () => { bookTocManager = new BookTocManager(undefined, targetBookModel); - await bookTocManager.updateBook([notebook], targetBook); - const listFiles = await fs.promises.readdir(run.targetBook.bookContentFolderPath); + await bookTocManager.updateBook([notebook5], targetBook); + const listFiles = await fs.promises.readdir(run.targetBook.contentFolder); should(JSON.stringify(listFiles).includes('notebook5.ipynb')).be.true('Notebook 5 should be under the target book content folder'); }); it('Remove notebook from book', async () => { - let toc: JupyterBookSection[] = yaml.safeLoad((await fs.promises.readFile(notebook.tableOfContentsPath)).toString()); + let toc: JupyterBookSection[] = yaml.safeLoad((await fs.promises.readFile(notebook5.tableOfContentsPath)).toString()); let notebookInToc = toc.some(section => { if (section.title === 'Notebook 5' && section.file === path.posix.join(path.posix.sep, 'notebook5')) { return true; @@ -497,12 +468,10 @@ describe('BookTocManagerTests', function () { return false; }); should(notebookInToc).be.true('Verify the notebook is in toc before removing'); - bookTocManager = new BookTocManager(sourceBookModel); - await bookTocManager.removeNotebook(notebook); - - const listFiles = await fs.promises.readdir(run.sourceBook.bookContentFolderPath); - toc = yaml.safeLoad((await fs.promises.readFile(notebook.tableOfContentsPath)).toString()); + await bookTocManager.removeNotebook(notebook5); + const listFiles = await fs.promises.readdir(run.sourceBook.contentFolder); + toc = yaml.safeLoad((await fs.promises.readFile(notebook5.tableOfContentsPath)).toString()); notebookInToc = toc.some(section => { if (section.title === 'Notebook 5' && section.file === path.posix.join(path.posix.sep, 'notebook5')) { return true; @@ -514,10 +483,11 @@ describe('BookTocManagerTests', function () { }); it('Add duplicated notebook to book', async () => { + bookTocManager = new BookTocManager(sourceBookModel, targetBookModel); + await bookTocManager.updateBook([notebook5], targetBook); bookTocManager = new BookTocManager(undefined, targetBookModel); - await bookTocManager.updateBook([notebook], targetBook); await bookTocManager.updateBook([duplicatedNotebook], targetBook); - const listFiles = await fs.promises.readdir(run.targetBook.bookContentFolderPath); + const listFiles = await fs.promises.readdir(run.targetBook.contentFolder); should(JSON.stringify(listFiles).includes('notebook5 - 2.ipynb')).be.true('Should rename the notebook to notebook5 - 2.ipynb'); should(JSON.stringify(listFiles).includes('notebook5.ipynb')).be.true('Should keep notebook5.ipynb'); }); @@ -528,9 +498,9 @@ describe('BookTocManagerTests', function () { sinon.stub(BookTocManager.prototype, 'updateTOC').throws(new Error('Unexpected error.')); const bookTreeViewProvider = new BookTreeViewProvider([], mockExtensionContext, false, 'bookTreeView', NavigationProviders.NotebooksNavigator); bookTocManager = new BookTocManager(targetBookModel); - sinon.stub(bookTreeViewProvider, 'moveTreeItems').returns(Promise.resolve(bookTocManager.updateBook([notebook], targetBook))); + sinon.stub(bookTreeViewProvider, 'moveTreeItems').returns(Promise.resolve(bookTocManager.updateBook([notebook5], targetBook))); try { - await bookTreeViewProvider.moveTreeItems([notebook]); + await bookTreeViewProvider.moveTreeItems([notebook5]); } catch (error) { should(recoverySpy.calledOnce).be.true('If unexpected error then recovery method is called.'); } @@ -538,29 +508,29 @@ describe('BookTocManagerTests', function () { it('Clean up folder with files didnt move', async () => { bookTocManager = new BookTocManager(targetBookModel); - bookTocManager.movedFiles.set(notebook.book.contentPath, 'movedtest'); - await fs.writeFile(path.join(run.sourceBook.bookContentFolderPath, 'test.ipynb'), ''); - await bookTocManager.cleanUp(path.dirname(notebook.book.contentPath)); - const listFiles = await fs.promises.readdir(path.dirname(notebook.book.contentPath)); + bookTocManager.movedFiles.set(notebook5.book.contentPath, 'movedtest'); + await fs.writeFile(path.join(run.sourceBook.contentFolder, 'test.ipynb'), ''); + await bookTocManager.cleanUp(path.dirname(notebook5.book.contentPath)); + const listFiles = await fs.promises.readdir(path.dirname(notebook5.book.contentPath)); should(JSON.stringify(listFiles).includes('test.ipynb')).be.true('Notebook test.ipynb should not be removed'); }); it('Clean up folder when there is an empty folder within the modified directory', async () => { - await fs.promises.mkdir(path.join(run.sourceBook.bookContentFolderPath, 'test')); - bookTocManager.modifiedDir.add(path.join(run.sourceBook.bookContentFolderPath, 'test')); - bookTocManager.movedFiles.set(notebook.book.contentPath, 'movedtest'); - await bookTocManager.cleanUp(path.dirname(notebook.book.contentPath)); - const listFiles = await fs.promises.readdir(run.sourceBook.bookContentFolderPath); + await fs.promises.mkdir(path.join(run.sourceBook.contentFolder, 'test')); + bookTocManager.modifiedDir.add(path.join(run.sourceBook.contentFolder, 'test')); + bookTocManager.movedFiles.set(notebook5.book.contentPath, 'movedtest'); + await bookTocManager.cleanUp(path.dirname(notebook5.book.contentPath)); + const listFiles = await fs.promises.readdir(run.sourceBook.contentFolder); should(JSON.stringify(listFiles).includes('test')).be.true('Empty directories within the moving element directory are not deleted'); }); it('Add new section', async () => { bookTocManager = new BookTocManager(sourceBookModel); - const fileBasename = `addSectionTest-${utils.generateGuid()}`; + const fileBasename = `addSectionTest-${generateGuid()}`; const sectionTitle = 'Section Test'; - const testFilePath = path.join(run.sectionA.sectionRoot, fileBasename).concat(utils.FileExtension.Markdown); + const testFilePath = path.join(run.sectionA.contentFolder, 'sectionA', fileBasename).concat(FileExtension.Markdown); await fs.writeFile(testFilePath, ''); - const pathDetails = new TocEntryPathHandler(testFilePath, run.sourceBook.rootBookFolderPath, sectionTitle); + const pathDetails = new TocEntryPathHandler(testFilePath, run.sourceBook.root, sectionTitle); await bookTocManager.addNewTocEntry(pathDetails, sectionA, true); let toc: JupyterBookSection[] = yaml.safeLoad((await fs.promises.readFile(run.sourceBook.tocPath)).toString()); const sectionAIndex = toc.findIndex(entry => entry.title === sectionA.title); @@ -574,6 +544,35 @@ describe('BookTocManagerTests', function () { should(newSection.sections).not.undefined(); }); + it('Section A is parent of notebook1 using both sectionA and notebook1 book tree items', async () => { + bookTocManager = new BookTocManager(sourceBookModel); + let isParent = bookTocManager.isParent(notebook1, sectionA); + should(isParent).be.true('Section A is parent of notebook1'); + }); + + it('Section A is parent of notebook1 passing the JupyterBookSection', async () => { + bookTocManager = new BookTocManager(sourceBookModel); + const section: JupyterBookSection = { + title: sectionA.title, + file: sectionA.uri + } + let isParent = bookTocManager.isParent(notebook1, sourceBook, section); + should(isParent).be.true('Section A is parent of notebook1'); + }); + + it('Check notebook1 is descendant of Section C', async () => { + bookTocManager = new BookTocManager(sourceBookModel); + sectionA.book.hierarchyId = '0'; + notebook1.book.hierarchyId = '0/1'; + sectionA.rootContentPath = run.sectionA.contentFolder; + notebook1.rootContentPath = run.sectionA.contentFolder; + bookTocManager.enableDnd = true; + let isDescendant = bookTocManager.isDescendant(sectionA, notebook1); + should(isDescendant).be.true('Notebook 1 is descendant of Section A'); + isDescendant = bookTocManager.isDescendant(sectionB, notebook1); + should(isDescendant).be.false('Notebook 1 is not descendant of Section B'); + }); + afterEach(async function (): Promise { sinon.restore(); if (await exists(sourceBookFolderPath)) { diff --git a/extensions/notebook/src/test/book/bookTrustManager.test.ts b/extensions/notebook/src/test/book/bookTrustManager.test.ts index f02e5ae54a..55c2d6f113 100644 --- a/extensions/notebook/src/test/book/bookTrustManager.test.ts +++ b/extensions/notebook/src/test/book/bookTrustManager.test.ts @@ -7,8 +7,9 @@ import * as should from 'should'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; import * as constants from '../../common/constants'; +import { BookTreeItemType } from '../../common/utils' import { IBookTrustManager, BookTrustManager } from '../../book/bookTrustManager'; -import { BookTreeItem, BookTreeItemFormat, BookTreeItemType } from '../../book/bookTreeItem'; +import { BookTreeItem, BookTreeItemFormat } from '../../book/bookTreeItem'; import * as vscode from 'vscode'; import { BookModel } from '../../book/bookModel'; import * as sinon from 'sinon';