diff --git a/extensions/notebook/resources/dark/folder_inverse.svg b/extensions/notebook/resources/dark/folder_inverse.svg
new file mode 100644
index 0000000000..f94d427cb1
--- /dev/null
+++ b/extensions/notebook/resources/dark/folder_inverse.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/extensions/notebook/resources/light/folder.svg b/extensions/notebook/resources/light/folder.svg
new file mode 100644
index 0000000000..8442363a76
--- /dev/null
+++ b/extensions/notebook/resources/light/folder.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/extensions/notebook/src/book/bookTocManager.ts b/extensions/notebook/src/book/bookTocManager.ts
index 2476a8d9bc..7f4b15dc6f 100644
--- a/extensions/notebook/src/book/bookTocManager.ts
+++ b/extensions/notebook/src/book/bookTocManager.ts
@@ -221,14 +221,22 @@ export class BookTocManager implements IBookTocManager {
/**
* Follows the same logic as the JupyterBooksCreate.ipynb. It receives a path that contains a notebooks and
* a path where it creates the book. It copies the contents from one folder to another and creates a table of contents.
- * @param bookContentPath The path to the book folder, the basename of the path is the name of the book
- * @param contentFolder The path to the folder that contains the notebooks and markdown files to be added to the created book.
+ * @param bookContentPath - The path to the book folder, the basename of the path is the name of the book
+ * @param contentFolder - (Optional) The path to the folder that contains the notebooks and markdown files to be added to the created book.
+ * If it's undefined then a blank notebook is attached to the book.
*/
- public async createBook(bookContentPath: string, contentFolder: string): Promise {
- await fs.promises.mkdir(bookContentPath);
- await fs.copy(contentFolder, bookContentPath);
- let filesinDir = await fs.readdir(bookContentPath);
- this.tableofContents = await this.getAllFiles([], bookContentPath, filesinDir, bookContentPath);
+ public async createBook(bookContentPath: string, contentFolder?: string): Promise {
+ let filesinDir: string[];
+ await fs.promises.mkdir(bookContentPath, { recursive: true });
+ if (contentFolder) {
+ await fs.copy(contentFolder, bookContentPath);
+ filesinDir = await fs.readdir(bookContentPath);
+ this.tableofContents = await this.getAllFiles([], bookContentPath, filesinDir, bookContentPath);
+ } else {
+ await fs.writeFile(path.join(bookContentPath, 'README.md'), '');
+ filesinDir = ['readme.md'];
+ this.tableofContents = await this.getAllFiles([], bookContentPath, filesinDir, bookContentPath);
+ }
await fs.writeFile(path.join(bookContentPath, '_config.yml'), yaml.safeDump({ title: path.basename(bookContentPath) }));
await fs.writeFile(path.join(bookContentPath, '_toc.yml'), yaml.safeDump(this.tableofContents, { lineWidth: Infinity }));
await vscode.commands.executeCommand('notebook.command.openNotebookFolder', bookContentPath, undefined, true);
diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts
index 2b0c2596a0..6b97e12c15 100644
--- a/extensions/notebook/src/book/bookTreeView.ts
+++ b/extensions/notebook/src/book/bookTreeView.ts
@@ -8,7 +8,7 @@ import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs-extra';
import * as constants from '../common/constants';
-import { IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
+import { IPrompter } from '../prompts/question';
import CodeAdapter from '../prompts/adapter';
import { BookTreeItem, BookTreeItemType } from './bookTreeItem';
import { BookModel } from './bookModel';
@@ -16,9 +16,10 @@ import { Deferred } from '../common/promise';
import { IBookTrustManager, BookTrustManager } from './bookTrustManager';
import * as loc from '../common/localizedConstants';
import * as glob from 'fast-glob';
-import { debounce, getPinnedNotebooks } from '../common/utils';
+import { debounce, getPinnedNotebooks, confirmReplace } from '../common/utils';
import { IBookPinManager, BookPinManager } from './bookPinManager';
import { BookTocManager, IBookTocManager, quickPickResults } from './bookTocManager';
+import { CreateBookDialog } from '../dialog/createBookDialog';
import { getContentPath } from './bookVersionHandler';
import { TelemetryReporter, BookTelemetryView, NbTelemetryActions } from '../telemetry';
@@ -52,6 +53,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider console.error(e));
this.prompter = new CodeAdapter();
this._bookTrustManager = new BookTrustManager(this.books);
+ this.bookTocManager = new BookTocManager();
this._extensionContext.subscriptions.push(azdata.nb.registerNavigationProvider(this));
}
@@ -142,10 +144,9 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider {
- bookPath = path.normalize(bookPath);
- contentPath = path.normalize(contentPath);
- await this.bookTocManager.createBook(bookPath, contentPath);
+ async createBook(): Promise {
+ const dialog = new CreateBookDialog(this.bookTocManager);
+ dialog.createDialog();
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.CreateBook).send();
}
@@ -502,7 +503,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider {
- return await this.prompter.promptSingle({
- type: QuestionTypes.confirm,
- message: loc.confirmReplace,
- default: false
- });
- }
-
getNavigation(uri: vscode.Uri): Thenable {
let result: azdata.nb.NavigationResult;
let notebook = this.currentBook?.getNotebook(uri.fsPath);
diff --git a/extensions/notebook/src/common/iconHelper.ts b/extensions/notebook/src/common/iconHelper.ts
index b307fc1cae..23d1011399 100644
--- a/extensions/notebook/src/common/iconHelper.ts
+++ b/extensions/notebook/src/common/iconHelper.ts
@@ -14,6 +14,7 @@ export class IconPathHelper {
private static extensionContext: vscode.ExtensionContext;
public static delete: IconPath;
+ public static folder: IconPath;
public static setExtensionContext(extensionContext: vscode.ExtensionContext) {
IconPathHelper.extensionContext = extensionContext;
@@ -21,5 +22,9 @@ export class IconPathHelper {
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/delete_inverse.svg'),
light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/delete.svg')
};
+ IconPathHelper.folder = {
+ dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'),
+ light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/folder.svg')
+ };
}
}
diff --git a/extensions/notebook/src/common/localizedConstants.ts b/extensions/notebook/src/common/localizedConstants.ts
index f1ab7a3643..94823b4e88 100644
--- a/extensions/notebook/src/common/localizedConstants.ts
+++ b/extensions/notebook/src/common/localizedConstants.ts
@@ -81,3 +81,18 @@ export const msgReleaseNotFound = localize('msgReleaseNotFound', "Releases not F
export const msgUndefinedAssetError = localize('msgUndefinedAssetError', "The selected book is not valid");
export function httpRequestError(code: number, message: string): string { return localize('httpRequestError', "Http Request failed with error: {0} {1}", code, message); }
export function msgDownloadLocation(downloadLocation: string): string { return localize('msgDownloadLocation', "Downloading to {0}", downloadLocation); }
+
+// Create Book dialog constants
+export const newGroup = localize('newGroup', "New Group");
+export const groupDescription = localize('groupDescription', "Groups are used to organize Notebooks.");
+export const locationBrowser = localize('locationBrowser', "Browse locations...");
+export const selectContentFolder = localize('selectContentFolder', "Select content folder");
+export const browse = localize('browse', "Browse");
+export const create = localize('create', "Create");
+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");
+
+
diff --git a/extensions/notebook/src/common/utils.ts b/extensions/notebook/src/common/utils.ts
index 7cd5e04d32..707f19fb58 100644
--- a/extensions/notebook/src/common/utils.ts
+++ b/extensions/notebook/src/common/utils.ts
@@ -11,6 +11,8 @@ import * as vscode from 'vscode';
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';
const localize = nls.loadMessageBundle();
@@ -390,3 +392,12 @@ export interface IBookNotebook {
bookPath?: string;
notebookPath: string;
}
+
+//Confirmation message dialog
+export async function confirmReplace(prompter: IPrompter): Promise {
+ return await prompter.promptSingle({
+ type: QuestionTypes.confirm,
+ message: loc.confirmReplace,
+ default: false
+ });
+}
diff --git a/extensions/notebook/src/dialog/createBookDialog.ts b/extensions/notebook/src/dialog/createBookDialog.ts
new file mode 100644
index 0000000000..fc56b59c6e
--- /dev/null
+++ b/extensions/notebook/src/dialog/createBookDialog.ts
@@ -0,0 +1,171 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+import * as vscode from 'vscode';
+import * as azdata from 'azdata';
+import * as path from 'path';
+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 { IPrompter } from '../prompts/question';
+import CodeAdapter from '../prompts/adapter';
+
+export class CreateBookDialog {
+
+ private dialog: azdata.window.Dialog;
+ public view: azdata.ModelView;
+ private formModel: azdata.FormContainer;
+ private bookNameInputBox: azdata.InputBoxComponent;
+ private saveLocationInputBox: azdata.InputBoxComponent;
+ private contentFolderInputBox: azdata.InputBoxComponent;
+ private prompter: IPrompter;
+
+ constructor(private tocManager: IBookTocManager) {
+ this.prompter = new CodeAdapter();
+ }
+
+ protected createHorizontalContainer(view: azdata.ModelView, items: azdata.Component[]): azdata.FlexContainer {
+ return view.modelBuilder.flexContainer().withItems(items, { CSSStyles: { 'margin-right': '10px', 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row' }).component();
+ }
+
+ public async selectFolder(): Promise {
+ const allFilesFilter = loc.allFiles;
+ let filter: any = {};
+ filter[allFilesFilter] = '*';
+ let uris = await vscode.window.showOpenDialog({
+ filters: filter,
+ canSelectFiles: false,
+ canSelectMany: false,
+ canSelectFolders: true,
+ openLabel: loc.labelSelectFolder
+ });
+ if (uris && uris.length > 0) {
+ return uris[0].fsPath;
+ }
+ return undefined;
+ }
+
+ public async validatePath(folderPath: string): Promise {
+ const destinationUri = path.join(folderPath, path.basename(this.bookNameInputBox.value));
+ if (await pathExists(destinationUri)) {
+ const doReplace = await confirmReplace(this.prompter);
+ if (doReplace) {
+ //remove folder if exists
+ await remove(destinationUri);
+ return true;
+ }
+ return false;
+ }
+ return await pathExists(folderPath);
+ }
+
+ public async createDialog(): Promise {
+ this.dialog = azdata.window.createModelViewDialog(loc.newGroup);
+ this.dialog.registerContent(async view => {
+ this.view = view;
+
+ const groupLabel = this.view.modelBuilder.text()
+ .withProperties({
+ value: loc.groupDescription,
+ CSSStyles: { 'margin-bottom': '0px', 'margin-top': '0px', 'font-size': 'small' }
+ }).component();
+
+ this.bookNameInputBox = this.view.modelBuilder.inputBox()
+ .withProperties({
+ values: [],
+ value: '',
+ enabled: true
+ }).component();
+
+ this.saveLocationInputBox = this.view.modelBuilder.inputBox().withProperties({
+ values: [],
+ value: '',
+ placeHolder: loc.locationBrowser,
+ width: '400px'
+ }).component();
+
+ this.contentFolderInputBox = this.view.modelBuilder.inputBox().withProperties({
+ values: [],
+ value: '',
+ placeHolder: loc.selectContentFolder,
+ width: '400px'
+ }).component();
+
+ const browseFolderButton = view.modelBuilder.button().withProperties({
+ ariaLabel: loc.browse,
+ iconPath: IconPathHelper.folder,
+ width: '18px',
+ height: '20px',
+ }).component();
+
+ const browseContentFolderButton = view.modelBuilder.button().withProperties({
+ ariaLabel: loc.browse,
+ iconPath: IconPathHelper.folder,
+ width: '18px',
+ height: '20px',
+ }).component();
+
+ browseFolderButton.onDidClick(async () => {
+ this.saveLocationInputBox.value = await this.selectFolder();
+ });
+
+ browseContentFolderButton.onDidClick(async () => {
+ this.contentFolderInputBox.value = await this.selectFolder();
+ });
+
+ this.formModel = this.view.modelBuilder.formContainer()
+ .withFormItems([{
+ components: [
+ {
+ component: groupLabel,
+ required: false
+ },
+ {
+ component: this.bookNameInputBox,
+ title: loc.name,
+ required: true
+ },
+ {
+ title: loc.saveLocation,
+ required: true,
+ component: this.createHorizontalContainer(view, [this.saveLocationInputBox, browseFolderButton])
+ },
+ {
+ title: loc.contentFolder,
+ required: false,
+ component: this.createHorizontalContainer(view, [this.contentFolderInputBox, browseContentFolderButton])
+ },
+ ],
+ title: ''
+ }]).withLayout({ width: '100%' }).component();
+ await this.view.initializeModel(this.formModel);
+ });
+ this.dialog.okButton.label = loc.create;
+ this.dialog.registerCloseValidator(async () => await this.create());
+ azdata.window.openDialog(this.dialog);
+ }
+
+ private async create(): Promise {
+ try {
+ const isValid = await this.validatePath(this.saveLocationInputBox.value);
+ if (!isValid) {
+ throw (new Error(loc.msgSaveFolderError));
+ }
+ if (this.contentFolderInputBox.value !== '' && !await pathExists(this.contentFolderInputBox.value)) {
+ throw (new Error(loc.msgContentFolderError));
+ }
+ const bookPath = path.join(this.saveLocationInputBox.value, this.bookNameInputBox.value);
+ await this.tocManager.createBook(bookPath, this.contentFolderInputBox.value);
+ return true;
+ } catch (error) {
+ this.dialog.message = {
+ text: error.message,
+ level: azdata.window.MessageLevel.Error
+ };
+ return false;
+ }
+ }
+}