From c106f0a19110efad7c5e1b136144985819e21100 Mon Sep 17 00:00:00 2001 From: Stephanie Stattel Date: Fri, 3 Nov 2023 02:04:52 -0700 Subject: [PATCH] Add a setting to show full path in breadcrumbs (#14866) * BROKEN first pass at overriding ellipses logic * cleaning up click handler for full path * cleaning up comments and const vars * fixing destination typo * adding setting for toggling full path * fixing config name * adding full path to settings schema * moving and adding comments on new set/get method for full path * Add a unit test for new crumbs option --------- Co-authored-by: Stephanie Stattel Co-authored-by: krassowski <5832902+krassowski@users.noreply.github.com> --- .../filebrowser-extension/schema/browser.json | 6 ++ packages/filebrowser-extension/src/index.ts | 21 +++++- packages/filebrowser/src/browser.ts | 11 +++ packages/filebrowser/src/crumbs.ts | 72 +++++++++++++++---- packages/filebrowser/test/crumbs.spec.ts | 16 +++++ 5 files changed, 110 insertions(+), 16 deletions(-) diff --git a/packages/filebrowser-extension/schema/browser.json b/packages/filebrowser-extension/schema/browser.json index 17e215492910..14cebcb90a0d 100644 --- a/packages/filebrowser-extension/schema/browser.json +++ b/packages/filebrowser-extension/schema/browser.json @@ -225,6 +225,12 @@ "description": "Whether to show checkboxes next to files and folders", "default": false }, + "showFullPath": { + "type": "boolean", + "title": "Show full path in browser bread crumbs", + "description": "Whether to show full path in browser bread crumbs", + "default": false + }, "sortNotebooksFirst": { "type": "boolean", "title": "When sorting by name, group notebooks before other files", diff --git a/packages/filebrowser-extension/src/index.ts b/packages/filebrowser-extension/src/index.ts index 6592e108ce40..0c359ae42068 100644 --- a/packages/filebrowser-extension/src/index.ts +++ b/packages/filebrowser-extension/src/index.ts @@ -135,6 +135,8 @@ namespace CommandIDs { export const toggleLastModified = 'filebrowser:toggle-last-modified'; + export const toggleShowFullPath = 'filebrowser:toggle-show-full-path'; + export const toggleFileSize = 'filebrowser:toggle-file-size'; export const toggleSortNotebooksFirst = @@ -222,7 +224,8 @@ const browser: JupyterFrontEndPlugin = { showFileSizeColumn: false, showHiddenFiles: false, showFileCheckboxes: false, - sortNotebooksFirst: false + sortNotebooksFirst: false, + showFullPath: false }; const fileBrowserModelConfig = { filterDirectories: true @@ -1260,6 +1263,22 @@ function addCommands( } }); + commands.addCommand(CommandIDs.toggleShowFullPath, { + label: trans.__('Show Full Path'), + isToggled: () => browser.showFullPath, + execute: () => { + const value = !browser.showFullPath; + const key = 'showFullPath'; + if (settingRegistry) { + return settingRegistry + .set(FILE_BROWSER_PLUGIN_ID, key, value) + .catch((reason: Error) => { + console.error(`Failed to set ${key} setting`); + }); + } + } + }); + commands.addCommand(CommandIDs.toggleSortNotebooksFirst, { label: trans.__('Sort Notebooks Above Files'), isToggled: () => browser.sortNotebooksFirst, diff --git a/packages/filebrowser/src/browser.ts b/packages/filebrowser/src/browser.ts index d1de8680e164..37ffea0d2a16 100644 --- a/packages/filebrowser/src/browser.ts +++ b/packages/filebrowser/src/browser.ts @@ -126,6 +126,17 @@ export class FileBrowser extends SidePanel { } } + /** + * Whether to show the full path in the breadcrumbs + */ + get showFullPath(): boolean { + return this.crumbs.fullPath; + } + + set showFullPath(value: boolean) { + this.crumbs.fullPath = value; + } + /** * Whether to show the file size column */ diff --git a/packages/filebrowser/src/crumbs.ts b/packages/filebrowser/src/crumbs.ts index 0e6f30550a40..a7b55ab4988a 100644 --- a/packages/filebrowser/src/crumbs.ts +++ b/packages/filebrowser/src/crumbs.ts @@ -70,6 +70,7 @@ export class BreadCrumbs extends Widget { this.translator = options.translator || nullTranslator; this._trans = this.translator.load('jupyterlab'); this._model = options.model; + this._fullPath = options.fullPath || false; this.addClass(BREADCRUMB_CLASS); this._crumbs = Private.createCrumbs(); this._crumbSeps = Private.createCrumbSeparators(); @@ -114,6 +115,17 @@ export class BreadCrumbs extends Widget { } } + /** + * Whether to show the full path in the breadcrumbs + */ + get fullPath(): boolean { + return this._fullPath; + } + + set fullPath(value: boolean) { + this._fullPath = value; + } + /** * A message handler invoked on an `'after-attach'` message. */ @@ -152,7 +164,8 @@ export class BreadCrumbs extends Widget { this._crumbs, this._crumbSeps, localPath, - this._hasPreferred + this._hasPreferred, + this._fullPath ); } @@ -184,12 +197,20 @@ export class BreadCrumbs extends Widget { node.classList.contains(BREADCRUMB_ITEM_CLASS) || node.classList.contains(BREADCRUMB_ROOT_CLASS) ) { - const index = ArrayExt.findFirstIndex( + let index = ArrayExt.findFirstIndex( this._crumbs, value => value === node ); + let destination = BREAD_CRUMB_PATHS[index]; + if ( + this._fullPath && + index < 0 && + !node.classList.contains(BREADCRUMB_ROOT_CLASS) + ) { + destination = node.title; + } this._model - .cd(BREAD_CRUMB_PATHS[index]) + .cd(destination) .catch(error => showErrorMessage(this._trans.__('Open Error'), error) ); @@ -309,6 +330,7 @@ export class BreadCrumbs extends Widget { private _hasPreferred: boolean; private _crumbs: ReadonlyArray; private _crumbSeps: ReadonlyArray; + private _fullPath: boolean; } /** @@ -328,6 +350,11 @@ export namespace BreadCrumbs { * The application language translator. */ translator?: ITranslator; + + /** + * Show the full file browser path in breadcrumbs + */ + fullPath?: boolean; } } @@ -353,7 +380,8 @@ namespace Private { breadcrumbs: ReadonlyArray, separators: ReadonlyArray, path: string, - hasPreferred: boolean + hasPreferred: boolean, + fullPath: boolean ): void { const node = breadcrumbs[0].parentNode as HTMLElement; @@ -371,7 +399,7 @@ namespace Private { } const parts = path.split('/'); - if (parts.length > 2) { + if (!fullPath && parts.length > 2) { node.appendChild(breadcrumbs[Crumb.Ellipsis]); const grandParent = parts.slice(0, parts.length - 2).join('/'); breadcrumbs[Crumb.Ellipsis].title = grandParent; @@ -379,17 +407,31 @@ namespace Private { } if (path) { - if (parts.length >= 2) { - breadcrumbs[Crumb.Parent].textContent = parts[parts.length - 2]; - node.appendChild(breadcrumbs[Crumb.Parent]); - const parent = parts.slice(0, parts.length - 1).join('/'); - breadcrumbs[Crumb.Parent].title = parent; - node.appendChild(separators[2]); + if (!fullPath) { + if (parts.length >= 2) { + breadcrumbs[Crumb.Parent].textContent = parts[parts.length - 2]; + node.appendChild(breadcrumbs[Crumb.Parent]); + const parent = parts.slice(0, parts.length - 1).join('/'); + breadcrumbs[Crumb.Parent].title = parent; + node.appendChild(separators[2]); + } + breadcrumbs[Crumb.Current].textContent = parts[parts.length - 1]; + node.appendChild(breadcrumbs[Crumb.Current]); + breadcrumbs[Crumb.Current].title = path; + node.appendChild(separators[3]); + } else { + for (let i = 0; i < parts.length; i++) { + const elem = document.createElement('span'); + elem.className = BREADCRUMB_ITEM_CLASS; + elem.textContent = parts[i]; + const elemPath = `/${parts.slice(0, i + 1).join('/')}`; + elem.title = elemPath; + node.appendChild(elem); + const separator = document.createElement('span'); + separator.textContent = '/'; + node.appendChild(separator); + } } - breadcrumbs[Crumb.Current].textContent = parts[parts.length - 1]; - node.appendChild(breadcrumbs[Crumb.Current]); - breadcrumbs[Crumb.Current].title = path; - node.appendChild(separators[3]); } } diff --git a/packages/filebrowser/test/crumbs.spec.ts b/packages/filebrowser/test/crumbs.spec.ts index 8e284d5cc17a..6260943fcf66 100644 --- a/packages/filebrowser/test/crumbs.spec.ts +++ b/packages/filebrowser/test/crumbs.spec.ts @@ -192,6 +192,22 @@ describe('filebrowser/model', () => { }); }); + describe('#fullPath', () => { + it('should show/hide full path', async () => { + Widget.attach(crumbs, document.body); + MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest); + expect(crumbs.node.textContent).toMatch( + /\/\/Untitled Folder.*?\/Untitled Folder.*?\// + ); + crumbs.fullPath = true; + MessageLoop.sendMessage(crumbs, Widget.Msg.UpdateRequest); + await framePromise(); + expect(crumbs.node.textContent).toMatch( + /\/Untitled Folder.*?\/Untitled Folder.*?\/Untitled Folder.*?\// + ); + }); + }); + describe('#onUpdateRequest()', () => { it('should be called when the model updates', async () => { const model = new FileBrowserModel({ manager });