UNPKG

@jupyterlab/docmanager

Version:
323 lines (287 loc) 8.96 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { Dialog, showDialog, showErrorMessage } from '@jupyterlab/apputils'; import { DocumentRegistry } from '@jupyterlab/docregistry'; import { PathExt } from '@jupyterlab/coreutils'; import { Contents } from '@jupyterlab/services'; import { ITranslator, nullTranslator } from '@jupyterlab/translation'; import { JSONObject } from '@lumino/coreutils'; import { Widget } from '@lumino/widgets'; import { IDocumentManager, IDocumentManagerDialogs } from './'; /** * The class name added to file dialogs. */ const FILE_DIALOG_CLASS = 'jp-FileDialog'; /** * The class name added for the new name label in the rename dialog */ const RENAME_NEW_NAME_TITLE_CLASS = 'jp-new-name-title'; /** * A stripped-down interface for a file container. */ export interface IFileContainer extends JSONObject { /** * The list of item names in the current working directory. */ items: string[]; /** * The current working directory of the file container. */ path: string; } /** * Namespace for DocumentManagerDialogs. */ export namespace DocumentManagerDialogs { export interface IOptions { translator?: ITranslator; } } /** * Default implementation of IDocumentManagerDialogs. */ export class DocumentManagerDialogs implements IDocumentManagerDialogs { constructor(options: DocumentManagerDialogs.IOptions = {}) { this._translator = options.translator || nullTranslator; } /** * Show a dialog to rename a file. */ async rename(context: DocumentRegistry.Context): Promise<void | null> { const trans = this._translator.load('jupyterlab'); const localPath = context.localPath.split('/'); const fileName = localPath.pop() || context.localPath; const handler = new RenameHandler(fileName, this._translator); const result = await showDialog({ title: trans.__('Rename File'), body: handler, buttons: [ Dialog.cancelButton(), Dialog.okButton({ label: trans.__('Rename'), ariaLabel: trans.__('Rename File') }) ], focusNodeSelector: 'input' }); if (!result.button.accept) { return null; } const newPath = result.value; if (!newPath || newPath === fileName) { return null; } if (!isValidFileName(newPath)) { void showErrorMessage( trans.__('Rename Error'), Error( trans.__( '"%1" is not a valid name for a file. Names must have nonzero length, and cannot include "/", "\\", or ":"', result.value ) ) ); return null; } return context.rename(newPath); } /** * Show a dialog asking whether to close a document. */ async confirmClose( options: IDocumentManagerDialogs.ConfirmClose.IOptions ): Promise<IDocumentManagerDialogs.ConfirmClose.IResult> { const trans = this._translator.load('jupyterlab'); const { fileName, isDirty } = options; const buttons = [ Dialog.cancelButton(), Dialog.okButton({ label: isDirty ? trans.__('Close and save') : trans.__('Close'), ariaLabel: isDirty ? trans.__('Close and save Document') : trans.__('Close Document') }) ]; if (isDirty) { buttons.splice( 1, 0, Dialog.warnButton({ label: trans.__('Close without saving'), ariaLabel: trans.__('Close Document without saving') }) ); } const confirm = await showDialog({ title: trans.__('Confirmation'), body: trans.__('Please confirm you want to close "%1".', fileName), checkbox: isDirty ? null : { label: trans.__('Do not ask me again.'), caption: trans.__( 'If checked, no confirmation to close a document will be asked in the future.' ) }, buttons }); const shouldClose = confirm.button.accept; const ignoreSave = isDirty ? confirm.button.displayType === 'warn' : true; const doNotAskAgain = confirm.isChecked === true; return { shouldClose, ignoreSave, doNotAskAgain }; } /** * Show a dialog asking whether to save before closing a dirty document. */ async saveBeforeClose( options: IDocumentManagerDialogs.SaveBeforeClose.IOptions ): Promise<IDocumentManagerDialogs.SaveBeforeClose.IResult> { const trans = this._translator.load('jupyterlab'); const { fileName, writable } = options; const saveLabel = writable ? trans.__('Save') : trans.__('Save as'); const result = await showDialog({ title: trans.__('Save your work'), body: trans.__('Save changes in "%1" before closing?', fileName), buttons: [ Dialog.cancelButton(), Dialog.warnButton({ label: trans.__('Discard'), ariaLabel: trans.__('Discard changes to file') }), Dialog.okButton({ label: saveLabel }) ] }); const shouldClose = result.button.accept; const ignoreSave = result.button.displayType === 'warn'; return { shouldClose, ignoreSave }; } private _translator: ITranslator; } /** * Rename a file with a dialog. * * @deprecated You should use {@link DocumentManagerDialogs.rename} */ export function renameDialog( manager: IDocumentManager, context: DocumentRegistry.Context, translator?: ITranslator, dialogs?: IDocumentManagerDialogs ): Promise<void | null> { if (dialogs) { return dialogs.rename(context); } const newDialogs = new DocumentManagerDialogs({ translator: translator }); return newDialogs.rename(context); } /** * Rename a file, asking for confirmation if it is overwriting another. */ export function renameFile( manager: IDocumentManager, oldPath: string, newPath: string ): Promise<Contents.IModel | null> { return manager.rename(oldPath, newPath).catch(error => { if (error.response.status !== 409) { // if it's not caused by an already existing file, rethrow throw error; } // otherwise, ask for confirmation return shouldOverwrite(newPath).then((value: boolean) => { if (value) { return manager.overwrite(oldPath, newPath); } return Promise.reject('File not renamed'); }); }); } /** * Ask the user whether to overwrite a file. */ export function shouldOverwrite( path: string, translator?: ITranslator ): Promise<boolean> { translator = translator || nullTranslator; const trans = translator.load('jupyterlab'); const options = { title: trans.__('Overwrite file?'), body: trans.__('"%1" already exists, overwrite?', path), buttons: [ Dialog.cancelButton(), Dialog.warnButton({ label: trans.__('Overwrite'), ariaLabel: trans.__('Overwrite Existing File') }) ] }; return showDialog(options).then(result => { return Promise.resolve(result.button.accept); }); } /** * Test whether a name is a valid file name * * Disallows "/", "\", and ":" in file names, as well as names with zero length. */ export function isValidFileName(name: string): boolean { const validNameExp = /[\/\\:]/; return name.length > 0 && !validNameExp.test(name); } /** * A widget used to rename a file. */ class RenameHandler extends Widget { /** * Construct a new "rename" dialog. */ constructor(oldPath: string, translator?: ITranslator) { super({ node: Private.createRenameNode(oldPath, translator) }); this.addClass(FILE_DIALOG_CLASS); const ext = PathExt.extname(oldPath); const value = (this.inputNode.value = PathExt.basename(oldPath)); this.inputNode.setSelectionRange(0, value.length - ext.length); } /** * Get the input text node. */ get inputNode(): HTMLInputElement { return this.node.getElementsByTagName('input')[0] as HTMLInputElement; } /** * Get the value of the widget. */ getValue(): string { return this.inputNode.value; } } /** * A namespace for private data. */ namespace Private { /** * Create the node for a rename handler. */ export function createRenameNode( oldPath: string, translator?: ITranslator ): HTMLElement { translator = translator || nullTranslator; const trans = translator.load('jupyterlab'); const body = document.createElement('div'); const existingLabel = document.createElement('label'); existingLabel.textContent = trans.__('File Path'); const existingPath = document.createElement('span'); existingPath.textContent = oldPath; const nameTitle = document.createElement('label'); nameTitle.textContent = trans.__('New Name'); nameTitle.className = RENAME_NEW_NAME_TITLE_CLASS; const name = document.createElement('input'); body.appendChild(existingLabel); body.appendChild(existingPath); body.appendChild(nameTitle); body.appendChild(name); return body; } }