Create book UI (#14210)

* Add dialog for creating books

* create empty book

* add localized constants

* add validation to dialog

* reset the create book command to original

* address pr comments

* change error message

* Init book toc manager in bookTreeView
This commit is contained in:
Barbara Valdez
2021-02-22 13:29:09 -08:00
committed by GitHub
parent 551eb76a42
commit 5ece9b968a
8 changed files with 227 additions and 23 deletions

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#231f20;}.cls-2{fill:#fff;}</style></defs><title>folder_inverse_16x16</title><polygon class="cls-1" points="13.59 2.34 13.58 2.35 13.58 2.33 13.59 2.34"/><text></text><path class="cls-2" d="M16,14.13H0v-12a1,1,0,0,1,.08-.39,1,1,0,0,1,.53-.53A1,1,0,0,1,1,1.13H4.75a2.16,2.16,0,0,1,.61.07,2.26,2.26,0,0,1,.45.18,2.14,2.14,0,0,1,.36.24l.32.24a1.8,1.8,0,0,0,.34.18,1.12,1.12,0,0,0,.43.07H15a1,1,0,0,1,.39.08,1,1,0,0,1,.53.53,1,1,0,0,1,.08.39ZM1,2.13v1H4.75a1.36,1.36,0,0,0,.33,0A1,1,0,0,0,5.34,3l.23-.16.25-.21-.25-.21-.23-.16a1,1,0,0,0-.26-.1,1.36,1.36,0,0,0-.33,0Zm14,11v-10H7.25a1.12,1.12,0,0,0-.43.07,1.8,1.8,0,0,0-.34.18l-.32.24a2.14,2.14,0,0,1-.36.24,2.26,2.26,0,0,1-.45.18,2.16,2.16,0,0,1-.61.07H1v9Z"/></svg>

After

Width:  |  Height:  |  Size: 830 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{fill:#F6F6F6;} .icon-vs-fg{fill:#F0EFF1;} .icon-folder{fill:#DCB67A;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 2.5v10c0 .827-.673 1.5-1.5 1.5h-11.996c-.827 0-1.5-.673-1.5-1.5v-8c0-.827.673-1.5 1.5-1.5h2.886l1-2h8.11c.827 0 1.5.673 1.5 1.5z" id="outline"/><path class="icon-folder" d="M14.5 2h-7.492l-1 2h-3.504c-.277 0-.5.224-.5.5v8c0 .276.223.5.5.5h11.996c.275 0 .5-.224.5-.5v-10c0-.276-.225-.5-.5-.5zm-.496 2h-6.496l.5-1h5.996v1z" id="iconBg"/><path class="icon-vs-fg" d="M14 3v1h-6.5l.5-1h6z" id="iconFg"/></svg>

After

Width:  |  Height:  |  Size: 740 B

View File

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

View File

@@ -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<BookTreeIte
this.initialize(workspaceFolders).catch(e => 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<BookTreeIte
}
}
async createBook(bookPath: string, contentPath: string): Promise<void> {
bookPath = path.normalize(bookPath);
contentPath = path.normalize(contentPath);
await this.bookTocManager.createBook(bookPath, contentPath);
async createBook(): Promise<void> {
const dialog = new CreateBookDialog(this.bookTocManager);
dialog.createDialog();
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.CreateBook).send();
}
@@ -502,7 +503,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 this.confirmReplace();
let doReplace = await confirmReplace(this.prompter);
if (!doReplace) {
return undefined;
}
@@ -676,15 +677,6 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
return untitledFileName;
}
//Confirmation message dialog
private async confirmReplace(): Promise<boolean> {
return await this.prompter.promptSingle<boolean>(<IQuestion>{
type: QuestionTypes.confirm,
message: loc.confirmReplace,
default: false
});
}
getNavigation(uri: vscode.Uri): Thenable<azdata.nb.NavigationResult> {
let result: azdata.nb.NavigationResult;
let notebook = this.currentBook?.getNotebook(uri.fsPath);

View File

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

View File

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

View File

@@ -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<boolean> {
return await prompter.promptSingle<boolean>(<IQuestion>{
type: QuestionTypes.confirm,
message: loc.confirmReplace,
default: false
});
}

View File

@@ -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<string | undefined> {
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<boolean> {
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<void> {
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<azdata.ButtonProperties>({
ariaLabel: loc.browse,
iconPath: IconPathHelper.folder,
width: '18px',
height: '20px',
}).component();
const browseContentFolderButton = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
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<boolean> {
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;
}
}
}