From fdd527cc784a2a2865cab17bb43b18b49107909d Mon Sep 17 00:00:00 2001 From: Samuel Klutse Date: Tue, 25 Jun 2024 11:20:48 -0400 Subject: [PATCH] Add Ancillary functionality to TOC Tree --- client/specs/book-tocs.spec.ts | 13 ++++- client/src/book-tocs.ts | 2 +- client/src/extension.ts | 7 ++- client/src/panel-toc-editor.ts | 6 +-- client/src/tocs-event-handler.ts | 84 ++++++++++++++++++++++++++++++-- client/static/xsd/cnxml.xsd | 3 ++ common/src/toc.ts | 32 +++++++++--- package.json | 53 ++++++++++++++++++-- server/src/book-toc-utils.ts | 2 +- server/src/model-manager.ts | 52 ++++++++++++-------- server/src/model/_cli.ts | 2 +- server/src/model/utils.ts | 6 ++- server/src/server.ts | 2 + 13 files changed, 220 insertions(+), 44 deletions(-) diff --git a/client/specs/book-tocs.spec.ts b/client/specs/book-tocs.spec.ts index b143e9dd..c9fa8be6 100644 --- a/client/specs/book-tocs.spec.ts +++ b/client/specs/book-tocs.spec.ts @@ -12,10 +12,19 @@ const testTocPage: ClientTocNode = { fileId: 'fileId' } } +const testTocAncillary: ClientTocNode = { + type: TocNodeKind.Ancillary, + value: { + absPath: '/path/to/ancillary', + token: 'token', + title: 'title', + fileId: 'fileId' + } +} const testTocSubbook: ClientTocNode = { type: TocNodeKind.Subbook, value: { token: 'token', title: 'title' }, - children: [testTocPage] + children: [testTocPage, testTocAncillary] } const testToc: BookToc = { type: BookRootNode.Singleton, @@ -27,13 +36,13 @@ const testToc: BookToc = { licenseUrl: 'licenseUrl', tocTree: [testTocSubbook] } - describe('Toc Provider', () => { const p = new TocsTreeProvider() it('returns tree items for children', () => { expect(p.getTreeItem(testToc)).toMatchSnapshot() expect(p.getTreeItem(testTocSubbook)).toMatchSnapshot() expect(p.getTreeItem(testTocPage)).toMatchSnapshot() + expect(p.getTreeItem(testTocAncillary)).toMatchSnapshot() }) it('filters fileids when filtering is set', () => { const p = new TocsTreeProvider() diff --git a/client/src/book-tocs.ts b/client/src/book-tocs.ts index 96ac08e1..5dd982b4 100644 --- a/client/src/book-tocs.ts +++ b/client/src/book-tocs.ts @@ -87,7 +87,7 @@ export class TocsTreeProvider implements TreeDataProvider { return [...this.bookTocs, ...this.orphans] } else if (node.type === BookRootNode.Singleton) { kids = node.tocTree - } else if (node.type === TocNodeKind.Page) { + } else if (node.type === TocNodeKind.Page || node.type === TocNodeKind.Ancillary) { kids = [] } else { kids = node.children diff --git a/client/src/extension.ts b/client/src/extension.ts index 61be1b1b..24a84bb9 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -14,6 +14,7 @@ import { type BookOrTocNode, TocsTreeProvider } from './book-tocs' import { type BooksAndOrphans, EMPTY_BOOKS_AND_ORPHANS, ExtensionServerNotification } from '../../common/src/requests' import { readmeGenerator } from './generate-readme' import { TocsEventHandler } from './tocs-event-handler' +import { TocNodeKind } from '../../common/src/toc' let tocTreesView: vscode.TreeView let tocTreesProvider: TocsTreeProvider @@ -109,8 +110,12 @@ function doRest(client: LanguageClient): ExtensionExports { vscode.commands.registerCommand('openstax.generateReadme', ensureCatch(readmeGenerator(hostContext))) tocTreesView = vscode.window.createTreeView('tocTrees', { treeDataProvider: tocTreesProvider, showCollapseAll: true, dragAndDropController: tocEventHandler }) vscode.commands.registerCommand('openstax.toggleTocTreesFiltering', ensureCatch(toggleTocTreesFilteringHandler(tocTreesView, tocTreesProvider))) + vscode.commands.registerCommand('openstax.addAncillaryToToc', ensureCatch(async (node: BookOrTocNode) => { await tocEventHandler.addNode(TocNodeKind.Ancillary, node, 'test') })) + vscode.commands.registerCommand('openstax.addPageToToc', ensureCatch(async (node: BookOrTocNode) => { await tocEventHandler.addNode(TocNodeKind.Page, node, 'test') })) + vscode.commands.registerCommand('openstax.addSubBookToToc', ensureCatch(async (node: BookOrTocNode) => { await tocEventHandler.addNode(TocNodeKind.Subbook, node, 'test') })) vscode.commands.registerCommand('openstax.validateContent', ensureCatch(validateContent)) - vscode.commands.registerCommand('openstax.test', ensureCatch(async (node: BookOrTocNode) => { await tocEventHandler.removeNode(node) })) + vscode.commands.registerCommand('openstax.removeNode', ensureCatch(async (node: BookOrTocNode) => { await tocEventHandler.removeNode(node) })) + vscode.commands.registerCommand('openstax.renameNode', ensureCatch(async (node: BookOrTocNode) => { await tocEventHandler.renameNode(node) })) void ensureCatchPromise(setDefaultGitConfig()) void ensureCatchPromise(initPrivateSubmodule(hostContext)) diff --git a/client/src/panel-toc-editor.ts b/client/src/panel-toc-editor.ts index 8ffa3534..698cd613 100644 --- a/client/src/panel-toc-editor.ts +++ b/client/src/panel-toc-editor.ts @@ -29,7 +29,7 @@ export type PanelIncomingMessage = ( ) export type TreeItemWithToken = TreeItemUI & ({ - type: TocNodeKind.Page + type: TocNodeKind.Page | TocNodeKind.Ancillary token: string title: string | undefined fileId: string @@ -51,7 +51,7 @@ export interface PanelState { } function toTreeItem(n: ClientTocNode): TreeItemWithToken { - if (n.type === TocNodeKind.Page) { + if (n.type === TocNodeKind.Page || n.type === TocNodeKind.Ancillary) { return { type: n.type, token: n.value.token, @@ -162,7 +162,7 @@ export class TocEditorPanel extends Panel() function recAddModules(n: ClientTocNode) { - if (n.type === TocNodeKind.Page) { + if (n.type === TocNodeKind.Page || n.type === TocNodeKind.Ancillary) { allModules.add(n.value) } else { n.children.forEach(recAddModules) diff --git a/client/src/tocs-event-handler.ts b/client/src/tocs-event-handler.ts index 59bb0a02..4824401f 100644 --- a/client/src/tocs-event-handler.ts +++ b/client/src/tocs-event-handler.ts @@ -1,12 +1,12 @@ import vscode from 'vscode' import { type TocsTreeProvider, type BookOrTocNode } from './book-tocs' -import { type TocModification, TocModificationKind, type TocModificationParams, TocNodeKind, BookRootNode } from '../../common/src/toc' +import { type TocModification, TocModificationKind, type TocModificationParams, TocNodeKind, BookRootNode, type CreatePageEvent, type CreateSubbookEvent, type CreateAncillaryEvent } from '../../common/src/toc' import { ExtensionServerRequest } from '../../common/src/requests' import { expect, getRootPathUri } from './utils' import { type ExtensionHostContext } from './panel' const getNodeToken = (node: BookOrTocNode) => { - return node.type === TocNodeKind.Page || node.type === TocNodeKind.Subbook + return node.type === TocNodeKind.Page || node.type === TocNodeKind.Subbook || node.type === TocNodeKind.Ancillary ? node.value.token : undefined } @@ -26,7 +26,7 @@ export class TocsEventHandler implements vscode.TreeDragAndDropController { + return await vscode.window.showInputBox({ + prompt: 'Please enter the title', + value: title ?? '', + validateInput: text => { + return text.trim().length === 0 ? 'Title cannot be empty' : null + } + }) + } + + async addNode(nodeType: TocNodeKind, node: BookOrTocNode, slug: string | undefined) { + const title = await this.askTitle() + if (title === undefined) { return } + const bookIndex = this.tocTreesProvider.getParentBookIndex(node) ?? 0 + if (nodeType === TocNodeKind.Page) { + const event: CreatePageEvent = { + type: TocNodeKind.Page, + title, + bookIndex + } + await this.fireEvent(event) + } else if (nodeType === TocNodeKind.Subbook) { + const event: CreateSubbookEvent = { + type: TocNodeKind.Subbook, + title, + slug, + bookIndex + } + await this.fireEvent(event) + } else if (nodeType === TocNodeKind.Ancillary) { + const event: CreateAncillaryEvent = { + type: TocNodeKind.Ancillary, + title, + slug, + bookIndex + } + await this.fireEvent(event) + } + } + + async renameNode(node: BookOrTocNode) { + // TODO Implement the rename functionality using inline editing (wait for the API to be available) + // https://github.com/microsoft/vscode/issues/97190 + // https://stackoverflow.com/questions/70594061/change-an-existing-label-name-in-tree-view-vscode-extension + const newTitle = await this.askTitle(('title' in node) ? node.title : '') + const nodeToken = expect( + getNodeToken(node), + 'BUG: Could not get token of renamed node' + ) + if (newTitle === undefined) { return } + const bookIndex = this.tocTreesProvider.getParentBookIndex(node) ?? 0 + if (node.type === TocNodeKind.Subbook) { + const event: TocModification = { + type: TocModificationKind.SubbookRename, + newTitle, + nodeToken, + bookIndex + } + await this.fireEvent(event) + } else if (node.type === TocNodeKind.Page) { + const event: TocModification = { + type: TocModificationKind.PageRename, + newTitle, + nodeToken, + bookIndex + } + await this.fireEvent(event) + } else if (node.type === TocNodeKind.Ancillary) { + const event: TocModification = { + type: TocModificationKind.AncillaryRename, + newTitle, + nodeToken, + bookIndex + } + await this.fireEvent(event) + } + } + handleDrag(source: readonly BookOrTocNode[], dataTransfer: vscode.DataTransfer): void { dataTransfer.set(XFER_ITEM_ID, new vscode.DataTransferItem(source[0])) } diff --git a/client/static/xsd/cnxml.xsd b/client/static/xsd/cnxml.xsd index e5baab8e..8ce1b4c7 100644 --- a/client/static/xsd/cnxml.xsd +++ b/client/static/xsd/cnxml.xsd @@ -49,6 +49,9 @@ + + + diff --git a/common/src/toc.ts b/common/src/toc.ts index 71dd99cf..61d89bdc 100644 --- a/common/src/toc.ts +++ b/common/src/toc.ts @@ -2,7 +2,8 @@ // This enum is also hardcoded in the toc-editor Webview export enum TocNodeKind { Subbook = 'TocNodeKind.Subbook', - Page = 'TocNodeKind.Page' + Page = 'TocNodeKind.Page', + Ancillary = 'TocNodeKind.Ancillary' } export enum BookRootNode { @@ -10,20 +11,24 @@ export enum BookRootNode { } export enum TocModificationKind { + Add = 'TocModificationKind.Add', Move = 'TocModificationKind.Move', Remove = 'TocModificationKind.Remove', PageRename = 'TocModificationKind.PageRename', SubbookRename = 'TocModificationKind.SubbookRename', + AncillaryRename = 'TocModificationKind.AncillaryRename' } -type TocNode = TocSubbook | TocPage +type TocNode = TocSubbook | TocPage | TocAncillary export interface TocSubbook { readonly type: TocNodeKind.Subbook, children: Array>, value: I } export interface TocPage { readonly type: TocNodeKind.Page, value: L } +export interface TocAncillary { readonly type: TocNodeKind.Ancillary, value: L } export type Token = string export interface ClientPageish { token: Token, title: string | undefined, fileId: string, absPath: string } +export interface ClientAncillaryish { token: Token, title: string | undefined, fileId: string, absPath: string } export interface ClientSubbookish { token: Token, title: string } -export type ClientTocNode = TocNode +export type ClientTocNode = TocNode export interface BookToc { readonly type: BookRootNode.Singleton @@ -38,9 +43,9 @@ export interface BookToc { export interface TocModificationParams { workspaceUri: string - event: TocModification | CreateSubbookEvent | CreatePageEvent + event: TocModification | CreateSubbookEvent | CreatePageEvent | CreateAncillaryEvent } -export type TocModification = (TocMoveEvent | TocRemoveEvent | PageRenameEvent | SubbookRenameEvent) +export type TocModification = (TocMoveEvent | TocRemoveEvent | PageRenameEvent | SubbookRenameEvent | AncillaryRenameEvent) export interface TocMoveEvent { readonly type: TocModificationKind.Move readonly nodeToken: Token @@ -67,12 +72,27 @@ export interface SubbookRenameEvent { readonly bookIndex: number } +export interface AncillaryRenameEvent { + readonly type: TocModificationKind.AncillaryRename + readonly newTitle: string + readonly nodeToken: Token + readonly bookIndex: number +} + export interface CreateSubbookEvent { readonly type: TocNodeKind.Subbook readonly title: string - readonly slug: string + readonly slug: string | undefined readonly bookIndex: number } + +export interface CreateAncillaryEvent { + readonly type: TocNodeKind.Ancillary + readonly title: string + readonly slug: string | undefined + readonly bookIndex: number +} + export interface CreatePageEvent { readonly type: TocNodeKind.Page readonly title: string diff --git a/package.json b/package.json index b55e662b..c6aa9d47 100644 --- a/package.json +++ b/package.json @@ -67,11 +67,29 @@ "title": "Generate README", "category": "Openstax" }, + { + "command": "openstax.addAncillaryToToc", + "title": "Add Ancillary", + "category": "Openstax", + "icon": "$(person-add)" + }, + { + "command": "openstax.addPageToToc", + "title": "Add Page", + "category": "Openstax", + "icon": "$(file-add)" + }, + { + "command": "openstax.addSubBookToToc", + "title": "Add Sub Book", + "category": "Openstax", + "icon": "$(file-directory-create)" + }, { "command": "openstax.toggleTocTreesFiltering", "title": "Toggle ToC Filtering", "category": "Openstax", - "icon": "$(filter)" + "icon": "$(expand-all)" }, { "command": "openstax.validateContent", @@ -79,8 +97,14 @@ "category": "Openstax" }, { - "command": "openstax.test", - "title": "Validate Content", + "command": "openstax.renameNode", + "title": "Rename Node", + "category": "Openstax", + "icon": "$(pencil)" + }, + { + "command": "openstax.removeNode", + "title": "Remove Node", "category": "Openstax", "icon": "$(trash)" } @@ -136,7 +160,7 @@ ], "commandPalette": [ { - "command": "openstax.showPreviewToSide", + "command": "openstax.showPreviewToSide", "when": "editorLangId == xml && !notebookEditorFocused", "group": "navigation" } @@ -146,11 +170,30 @@ "command": "openstax.toggleTocTreesFiltering", "when": "view == tocTrees", "group": "navigation" + }, + { + "command": "openstax.addAncillaryToToc", + "when": "view == tocTrees", + "group": "navigation" + }, + { + "command": "openstax.addSubBookToToc", + "when": "view == tocTrees", + "group": "navigation" + }, + { + "command": "openstax.addPageToToc", + "when": "view == tocTrees", + "group": "navigation" } ], "view/item/context": [ { - "command": "openstax.test", + "command": "openstax.renameNode", + "when": "view == tocTrees && viewItem =~ /,?rename,?/", + "group": "inline" + },{ + "command": "openstax.removeNode", "when": "view == tocTrees && viewItem =~ /,?delete,?/", "group": "inline" } diff --git a/server/src/book-toc-utils.ts b/server/src/book-toc-utils.ts index a5b87301..2097a900 100644 --- a/server/src/book-toc-utils.ts +++ b/server/src/book-toc-utils.ts @@ -99,7 +99,7 @@ function recTree(tocIdMap: IdMap, parent } function recBuild(doc: Document, node: ClientTocNode): Element { - if (node.type === TocNodeKind.Page) { + if (node.type === TocNodeKind.Page || node.type === TocNodeKind.Ancillary) { const ret = doc.createElementNS(NS_COLLECTION, 'col:module') ret.setAttribute('document', node.value.fileId) return ret diff --git a/server/src/model-manager.ts b/server/src/model-manager.ts index 6ad71911..d531c405 100644 --- a/server/src/model-manager.ts +++ b/server/src/model-manager.ts @@ -597,7 +597,7 @@ export class ModelManager { if (nodeAndParent !== undefined) { // We are manipulating an item in a Book ToC const { node, parent } = nodeAndParent - if (evt.type === TocModificationKind.PageRename || evt.type === TocModificationKind.SubbookRename) { + if (evt.type === TocModificationKind.PageRename || evt.type === TocModificationKind.SubbookRename || evt.type === TocModificationKind.AncillaryRename) { if (node.type === TocNodeKind.Page) { const page = expectValue(this.bundle.allPages.get(node.value.absPath), `BUG: This node should exist: ${node.value.absPath}`) const fsPath = URI.parse(node.value.absPath).fsPath @@ -680,20 +680,7 @@ export class ModelManager { } } - public async createPage(bookIndex: number, title: string) { - const template = (): string => { - return ` - - - <metadata xmlns:md="http://cnx.rice.edu/mdml"> - <md:title/> - <md:content-id/> - <md:uuid/> - </metadata> - <content> - </content> -</document>`.trim() - } + public async createDocument(bookIndex: number, title: string, documentType: string, template: string) { const workspaceRootUri = URI.parse(this.bundle.workspaceRootUri) const pageDirUri = Utils.joinPath(workspaceRootUri, 'modules') let moduleNumber = 0 @@ -708,9 +695,8 @@ export class ModelManager { const pageUri = Utils.joinPath(pageDirUri, newModuleId, 'index.cnxml') const page = this.bundle.allPages.getOrAdd(pageUri.fsPath) // fsPath works for tests and gets converted to file:// for real - const doc = new DOMParser().parseFromString(template(), 'text/xml') + const doc = new DOMParser().parseFromString(template, 'text/xml') selectOne('/cnxml:document/cnxml:title', doc).textContent = title - selectOne('/cnxml:document/cnxml:metadata/md:title', doc).textContent = title selectOne('/cnxml:document/cnxml:metadata/md:content-id', doc).textContent = newModuleId selectOne('/cnxml:document/cnxml:metadata/md:uuid', doc).textContent = uuid4() const xmlStr = new XMLSerializer().serializeToString(doc) @@ -718,7 +704,7 @@ export class ModelManager { page.load(xmlStr) await mkdirp(Utils.joinPath(pageDirUri, newModuleId).fsPath) await fs.promises.writeFile(pageUri.fsPath, xmlStr) - ModelManager.debug(`[NEW_PAGE] Created: ${pageUri.fsPath}`) + ModelManager.debug(`[NEW_${documentType.toUpperCase()}] Created: ${pageUri.fsPath}`) const bookToc = this.bookTocs[bookIndex] const book = expectValue(this.bundle.allBooks.get(bookToc.absPath), 'BUG: Book no longer exists') @@ -727,13 +713,41 @@ export class ModelManager { value: { token: 'unused-when-writing', title: undefined, fileId: newModuleId, absPath: page.absPath } }) await writeBookToc(book, bookToc) - ModelManager.debug(`[CREATE_PAGE] Prepended to Book: ${pageUri.fsPath}`) + ModelManager.debug(`[CREATE_${documentType.toUpperCase()}] Prepended to Book: ${pageUri.fsPath}`) return { page, id: newModuleId } } /* istanbul ignore next */ throw new Error('Error: Too many page directories already exist') } + public async createPage(bookIndex: number, title: string) { + return await this.createDocument(bookIndex, title, 'page', ` +<document xmlns="http://cnx.rice.edu/cnxml"> + <title/> + <metadata xmlns:md="http://cnx.rice.edu/mdml"> + <md:title/> + <md:content-id/> + <md:uuid/> + </metadata> + <content> + </content> +</document>`.trim()) + } + + public async createAncillary(bookIndex: number, title: string) { + return await this.createDocument(bookIndex, title, 'ancilliary', ` +<document xmlns="http://cnx.rice.edu/cnxml" class="super" resource="" document-link="" ancillary-type=""> + <title/> + <metadata xmlns:md="http://cnx.rice.edu/mdml"> + <md:title/> + <md:content-id/> + <md:uuid/> + </metadata> + <content class="super"> + </content> +</document>`.trim()) + } + public async createSubbook(bookIndex: number, title: string) { ModelManager.debug(`[CREATE_SUBBOOK] Creating: ${title}`) const bookToc = this.bookTocs[bookIndex] diff --git a/server/src/model/_cli.ts b/server/src/model/_cli.ts index b7615668..7846717b 100644 --- a/server/src/model/_cli.ts +++ b/server/src/model/_cli.ts @@ -228,7 +228,7 @@ function traverse(node: BookNode | TocNodeWithRange, indexes: number[]): TocPage /* Returns true if this node has nothing worth keeping (trimMe) */ function trimNodes(node: ClientTocNode | BookToc, keepPages: Set<PageNode>): boolean { - if (node.type === TocNodeKind.Page) { + if (node.type === TocNodeKind.Page || node.type === TocNodeKind.Ancillary) { const hasKeeper = [...keepPages].find(n => n.absPath === node.value.absPath) !== undefined return !hasKeeper } else { diff --git a/server/src/model/utils.ts b/server/src/model/utils.ts index 6620a63c..2afe2d9d 100644 --- a/server/src/model/utils.ts +++ b/server/src/model/utils.ts @@ -61,11 +61,13 @@ export function textWithRange(el: Element, attr?: string): WithRange<string> { // This also exists in ../common/ export enum TocNodeKind { Subbook = 'TocNodeKind.Subbook', - Page = 'TocNodeKind.Page' + Page = 'TocNodeKind.Page', + Ancillary = 'TocNodeKind.Ancillary' } -export type TocNode<T> = TocSubbook<T> | TocPage<T> +export type TocNode<T> = TocSubbook<T> | TocPage<T> | TocAncillary<T> export interface TocSubbook<T> { type: TocNodeKind.Subbook, readonly title: string, readonly children: Array<TocNode<T>> } export interface TocPage<T> { type: TocNodeKind.Page, readonly page: T } +export interface TocAncillary<T> { type: TocNodeKind.Ancillary, readonly page: T } export interface Paths { booksRoot: string diff --git a/server/src/server.ts b/server/src/server.ts index 1bc32f37..3ace3753 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -147,6 +147,8 @@ connection.onRequest(ExtensionServerRequest.TocModification, async (params: TocM await manager.createPage(event.bookIndex, event.title) } else if (event.type === TocNodeKind.Subbook) { await manager.createSubbook(event.bookIndex, event.title) + } else if (event.type === TocNodeKind.Ancillary) { + await manager.createAncillary(event.bookIndex, event.title) } else { await manager.modifyToc(event) }