mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
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:
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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, '');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
20
extensions/notebook/src/book/tocEntryPathHandler.ts
Normal file
20
extensions/notebook/src/book/tocEntryPathHandler.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
111
extensions/notebook/src/dialog/addFileDialog.ts
Normal file
111
extensions/notebook/src/dialog/addFileDialog.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}));
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user