mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-01 09:35:41 -05:00
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:
1
extensions/notebook/resources/dark/folder_inverse.svg
Normal file
1
extensions/notebook/resources/dark/folder_inverse.svg
Normal 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 |
1
extensions/notebook/resources/light/folder.svg
Normal file
1
extensions/notebook/resources/light/folder.svg
Normal 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 |
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
171
extensions/notebook/src/dialog/createBookDialog.ts
Normal file
171
extensions/notebook/src/dialog/createBookDialog.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user