Edit book using drag and drop (#16906)

- Use the onDrop method for moving notebooks/sections in the Notebooks Tree View.
- Allow multi selection in tree view
- Modify notebook commands to only show when a single tree item is selected.
This commit is contained in:
Barbara Valdez
2021-09-02 11:07:03 -07:00
committed by GitHub
parent 3803809223
commit bb3ccb92a4
6 changed files with 133 additions and 82 deletions

View File

@@ -15,7 +15,7 @@ import { TocEntryPathHandler } from './tocEntryPathHandler';
import { FileExtension } from '../common/utils';
export interface IBookTocManager {
updateBook(element: BookTreeItem, book: BookTreeItem, targetSection?: JupyterBookSection): Promise<void>;
updateBook(sources: BookTreeItem[], target: BookTreeItem, targetSection?: JupyterBookSection): Promise<void>;
removeNotebook(element: BookTreeItem): Promise<void>;
createBook(bookContentPath: string, contentFolder: string): Promise<void>;
addNewFile(pathDetails: TocEntryPathHandler, bookItem: BookTreeItem): Promise<void>;
@@ -339,7 +339,7 @@ export class BookTocManager implements IBookTocManager {
this.newSection = sectionTOC;
}
}
this.newSection.title = section.title;
this.newSection.title = section.book.title;
this.newSection.file = path.posix.join(path.parse(uri).dir, fileName);
if (section.sections) {
const files = section.sections as JupyterBookSection[];
@@ -366,8 +366,11 @@ export class BookTocManager implements IBookTocManager {
const filePath = path.parse(file.book.contentPath);
let fileName = undefined;
try {
this.movedFiles.set(file.book.contentPath, path.join(rootPath, filePath.base));
await fs.move(file.book.contentPath, path.join(rootPath, filePath.base), { overwrite: false });
// no op if the notebook is already in the dest location
if (file.book.contentPath !== path.join(rootPath, filePath.base)) {
this.movedFiles.set(file.book.contentPath, path.join(rootPath, filePath.base));
await fs.move(file.book.contentPath, path.join(rootPath, filePath.base), { overwrite: false });
}
} catch (error) {
if (error.code === 'EEXIST') {
fileName = await this.renameFile(file.book.contentPath, path.join(rootPath, filePath.base));
@@ -394,44 +397,47 @@ export class BookTocManager implements IBookTocManager {
/**
* Moves the element to the target book's folder and adds it to the table of contents.
* @param element Notebook, Markdown File, or section that will be added to the book.
* @param targetBook Book that will be modified.
* @param targetSection Book section that'll be modified.
* @param sources The tree items that are been moved.
* @param target Target tree item on which the sources will be added
* @param section (Optional) book section that'll be modified. Not required when using drag and drop.
*/
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 {
// 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();
void vscode.window.showErrorMessage(loc.editBookError(element.book.contentPath, e instanceof Error ? e.message : e));
} finally {
public async updateBook(sources: BookTreeItem[], target: BookTreeItem, section?: JupyterBookSection): Promise<void> {
for (let element of sources) {
try {
await this._targetBook.reinitializeContents();
const targetSection = section ? section : (target.contextValue === 'section' ? { file: target.book.page.file, title: target.book.page.title } : undefined);
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, target);
// remove section from book
await this.updateTOC(element.book.version, element.tableOfContentsPath, findSection, undefined);
// add section to book
await this.updateTOC(target.book.version, target.tableOfContentsPath, targetSection, this.newSection);
}
else {
// the notebook is part of a book so we need to modify its toc as well
const findSection = { file: element.book.page.file, title: element.book.page.title };
await this.moveFile(element, target);
if (element.contextValue === 'savedBookNotebook' || element.contextValue === 'Markdown') {
// 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(target.book.version, target.tableOfContentsPath, targetSection, this.newSection);
}
} catch (e) {
await this.recovery();
void vscode.window.showErrorMessage(loc.editBookError(element.book.contentPath, e instanceof Error ? e.message : e));
} finally {
if (this._sourceBook && this._sourceBook.bookPath !== this._targetBook.bookPath) {
// refresh source book model to pick up latest changes
await this._sourceBook.reinitializeContents();
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();
}
}
}
}

View File

@@ -159,7 +159,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
return dialog.createDialog();
}
async getSelectionQuickPick(movingElement: BookTreeItem): Promise<quickPickResults> {
async bookSectionQuickPick(): Promise<quickPickResults> {
let bookOptions: vscode.QuickPickItem[] = [];
let pickedSection: vscode.QuickPickItem;
this.books.forEach(book => {
@@ -172,7 +172,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
placeHolder: loc.labelBookFolder
});
if (pickedBook && movingElement) {
if (pickedBook) {
const updateBook = this.books.find(book => book.bookPath === pickedBook.detail).bookItems[0];
if (updateBook) {
let bookSections = updateBook.sections;
@@ -194,11 +194,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
break;
}
else if (pickedSection && pickedSection.detail) {
if (updateBook.root === movingElement.root && pickedSection.detail === movingElement.uri) {
pickedSection = undefined;
} else {
bookSections = updateBook.findChildSection(pickedSection.detail).sections;
}
bookSections = updateBook.findChildSection(pickedSection.detail).sections;
}
}
}
@@ -208,17 +204,27 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
return undefined;
}
async editBook(movingElement: BookTreeItem): Promise<void> {
/**
* Move selected tree items (sections/notebooks) using the move option tree item entry point.
* A quick pick menu will show all the possible books and sections for the user to choose
* the target element to add the selected tree items.
* @param treeItems Elements to be moved
*/
async moveTreeItems(treeItems: BookTreeItem[]): Promise<void> {
TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.MoveNotebook);
const selectionResults = await this.getSelectionQuickPick(movingElement);
const selectionResults = await this.bookSectionQuickPick();
if (selectionResults) {
const pickedSection = selectionResults.quickPickSection;
const updateBook = selectionResults.book;
const targetSection = pickedSection.detail !== undefined ? updateBook.findChildSection(pickedSection.detail) : undefined;
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(sourceBook, targetBook);
await this.bookTocManager.updateBook(movingElement, updateBook, targetSection);
let pickedSection = selectionResults.quickPickSection;
// filter target from sources
let movingElements = treeItems.filter(item => item.uri !== pickedSection.detail);
const targetBookItem = selectionResults.book;
const targetSection = pickedSection.detail !== undefined ? targetBookItem.findChildSection(pickedSection.detail) : undefined;
let sourcesByBook = this.groupTreeItemsByBookModel(movingElements);
const targetBookModel = this.books.find(book => book.bookPath === targetBookItem.book.root);
for (let [bookModel, items] of sourcesByBook) {
this.bookTocManager = new BookTocManager(bookModel, targetBookModel);
await this.bookTocManager.updateBook(items, targetBookItem, targetSection);
}
}
}
@@ -717,11 +723,56 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
return Promise.resolve(result);
}
async onDrop(sources: vscode.TreeDataTransfer, target: BookTreeItem): Promise<void> {
let treeItems = JSON.parse(await sources.items.get('text/treeitems')!.asString());
if (treeItems) {
groupTreeItemsByBookModel(treeItems: BookTreeItem[]): Map<BookModel, BookTreeItem[]> {
const sourcesByBook = new Map<BookModel, BookTreeItem[]>();
for (let item of treeItems) {
const book = this.books.find(book => book.bookPath === item.book.root);
if (sourcesByBook.has(book)) {
sourcesByBook.get(book).push(item);
} else {
sourcesByBook.set(book, [item]);
}
}
return sourcesByBook;
}
async onDrop(sources: vscode.TreeDataTransfer, target: BookTreeItem): Promise<void> {
TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.DragAndDrop);
// gets the tree items that are dragged and dropped
let treeItems = JSON.parse(await sources.items.get(this.supportedTypes[0])!.asString()) as BookTreeItem[];
let rootItems = this.getLocalRoots(treeItems);
rootItems = rootItems.filter(item => item.resourceUri !== target.resourceUri);
if (rootItems && target) {
let sourcesByBook = this.groupTreeItemsByBookModel(rootItems);
const targetBook = this.books.find(book => book.bookPath === target.book.root);
for (let [book, items] of sourcesByBook) {
this.bookTocManager = new BookTocManager(book, targetBook);
await this.bookTocManager.updateBook(items, target);
}
}
}
/**
* From the tree items moved find the local roots.
* We don't need to move a child element if the parent element has been selected as well, since every time that an element is moved
* we add its children.
* @param bookItems that have been dragged and dropped
* @returns an array of tree items that do not share a parent element.
*/
public getLocalRoots(bookItems: BookTreeItem[]): BookTreeItem[] {
const localRoots = [];
for (let i = 0; i < bookItems.length; i++) {
const parent = bookItems[i].book.parent;
if (parent) {
const isInList = bookItems.find(item => item.resourceUri.path === parent.resourceUri.path && parent.contextValue !== 'savedBook');
if (isInList === undefined) {
localRoots.push(bookItems[i]);
}
} else {
localRoots.push(bookItems[i]);
}
}
return localRoots;
}
dispose(): void { }