UNPKG

5.45 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3
4import { Dialog, showDialog, showErrorMessage } from '@jupyterlab/apputils';
5import { DocumentRegistry } from '@jupyterlab/docregistry';
6import { PathExt } from '@jupyterlab/coreutils';
7import { Contents } from '@jupyterlab/services';
8import { ITranslator, nullTranslator } from '@jupyterlab/translation';
9import { JSONObject } from '@lumino/coreutils';
10import { Widget } from '@lumino/widgets';
11import { IDocumentManager } from './';
12
13/**
14 * The class name added to file dialogs.
15 */
16const FILE_DIALOG_CLASS = 'jp-FileDialog';
17
18/**
19 * The class name added for the new name label in the rename dialog
20 */
21const RENAME_NEW_NAME_TITLE_CLASS = 'jp-new-name-title';
22
23/**
24 * A stripped-down interface for a file container.
25 */
26export interface IFileContainer extends JSONObject {
27 /**
28 * The list of item names in the current working directory.
29 */
30 items: string[];
31 /**
32 * The current working directory of the file container.
33 */
34 path: string;
35}
36
37/**
38 * Rename a file with a dialog.
39 */
40export function renameDialog(
41 manager: IDocumentManager,
42 context: DocumentRegistry.Context,
43 translator?: ITranslator
44): Promise<void | null> {
45 translator = translator || nullTranslator;
46 const trans = translator.load('jupyterlab');
47
48 const localPath = context.localPath.split('/');
49 const fileName = localPath.pop() || context.localPath;
50
51 return showDialog({
52 title: trans.__('Rename File'),
53 body: new RenameHandler(fileName),
54 focusNodeSelector: 'input',
55 buttons: [
56 Dialog.cancelButton(),
57 Dialog.okButton({
58 label: trans.__('Rename'),
59 ariaLabel: trans.__('Rename File')
60 })
61 ]
62 }).then(result => {
63 if (!result.value) {
64 return null;
65 }
66 if (!isValidFileName(result.value)) {
67 void showErrorMessage(
68 trans.__('Rename Error'),
69 Error(
70 trans.__(
71 '"%1" is not a valid name for a file. Names must have nonzero length, and cannot include "/", "\\", or ":"',
72 result.value
73 )
74 )
75 );
76 return null;
77 }
78 return context.rename(result.value);
79 });
80}
81
82/**
83 * Rename a file, asking for confirmation if it is overwriting another.
84 */
85export function renameFile(
86 manager: IDocumentManager,
87 oldPath: string,
88 newPath: string
89): Promise<Contents.IModel | null> {
90 return manager.rename(oldPath, newPath).catch(error => {
91 if (error.response.status !== 409) {
92 // if it's not caused by an already existing file, rethrow
93 throw error;
94 }
95
96 // otherwise, ask for confirmation
97 return shouldOverwrite(newPath).then((value: boolean) => {
98 if (value) {
99 return manager.overwrite(oldPath, newPath);
100 }
101 return Promise.reject('File not renamed');
102 });
103 });
104}
105
106/**
107 * Ask the user whether to overwrite a file.
108 */
109export function shouldOverwrite(
110 path: string,
111 translator?: ITranslator
112): Promise<boolean> {
113 translator = translator || nullTranslator;
114 const trans = translator.load('jupyterlab');
115
116 const options = {
117 title: trans.__('Overwrite file?'),
118 body: trans.__('"%1" already exists, overwrite?', path),
119 buttons: [
120 Dialog.cancelButton(),
121 Dialog.warnButton({
122 label: trans.__('Overwrite'),
123 ariaLabel: trans.__('Overwrite Existing File')
124 })
125 ]
126 };
127 return showDialog(options).then(result => {
128 return Promise.resolve(result.button.accept);
129 });
130}
131
132/**
133 * Test whether a name is a valid file name
134 *
135 * Disallows "/", "\", and ":" in file names, as well as names with zero length.
136 */
137export function isValidFileName(name: string): boolean {
138 const validNameExp = /[\/\\:]/;
139 return name.length > 0 && !validNameExp.test(name);
140}
141
142/**
143 * A widget used to rename a file.
144 */
145class RenameHandler extends Widget {
146 /**
147 * Construct a new "rename" dialog.
148 */
149 constructor(oldPath: string) {
150 super({ node: Private.createRenameNode(oldPath) });
151 this.addClass(FILE_DIALOG_CLASS);
152 const ext = PathExt.extname(oldPath);
153 const value = (this.inputNode.value = PathExt.basename(oldPath));
154 this.inputNode.setSelectionRange(0, value.length - ext.length);
155 }
156
157 /**
158 * Get the input text node.
159 */
160 get inputNode(): HTMLInputElement {
161 return this.node.getElementsByTagName('input')[0] as HTMLInputElement;
162 }
163
164 /**
165 * Get the value of the widget.
166 */
167 getValue(): string {
168 return this.inputNode.value;
169 }
170}
171
172/**
173 * A namespace for private data.
174 */
175namespace Private {
176 /**
177 * Create the node for a rename handler.
178 */
179 export function createRenameNode(
180 oldPath: string,
181 translator?: ITranslator
182 ): HTMLElement {
183 translator = translator || nullTranslator;
184 const trans = translator.load('jupyterlab');
185
186 const body = document.createElement('div');
187 const existingLabel = document.createElement('label');
188 existingLabel.textContent = trans.__('File Path');
189 const existingPath = document.createElement('span');
190 existingPath.textContent = oldPath;
191
192 const nameTitle = document.createElement('label');
193 nameTitle.textContent = trans.__('New Name');
194 nameTitle.className = RENAME_NEW_NAME_TITLE_CLASS;
195 const name = document.createElement('input');
196
197 body.appendChild(existingLabel);
198 body.appendChild(existingPath);
199 body.appendChild(nameTitle);
200 body.appendChild(name);
201 return body;
202 }
203}