dialogs.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. ClientSession, Dialog, showDialog
  5. } from '@jupyterlab/apputils';
  6. import {
  7. PathExt, uuid
  8. } from '@jupyterlab/coreutils';
  9. import {
  10. Contents, IServiceManager, Kernel
  11. } from '@jupyterlab/services';
  12. import {
  13. each, IIterator
  14. } from '@phosphor/algorithm';
  15. import {
  16. Widget
  17. } from '@phosphor/widgets';
  18. import {
  19. DocumentManager
  20. } from './';
  21. /**
  22. * The class name added to file dialogs.
  23. */
  24. const FILE_DIALOG_CLASS = 'jp-FileDialog';
  25. /**
  26. * The class name added for a file conflict.
  27. */
  28. const FILE_CONFLICT_CLASS = 'jp-mod-conflict';
  29. /**
  30. * A stripped-down interface for a file container.
  31. */
  32. export
  33. interface IFileContainer {
  34. /**
  35. * Returns an iterator over the container's items.
  36. */
  37. items(): IIterator<Contents.IModel>;
  38. /**
  39. * The current working directory of the file container.
  40. */
  41. path: string;
  42. }
  43. /**
  44. * Create a file using a file creator.
  45. */
  46. export
  47. function createFromDialog(container: IFileContainer, manager: DocumentManager, creatorName: string): Promise<Widget> {
  48. let handler = new CreateFromHandler(container, manager, creatorName);
  49. return manager.services.ready.then(() => {
  50. return handler.populate();
  51. }).then(() => {
  52. return handler.showDialog();
  53. });
  54. }
  55. /**
  56. * Create a new untitled file.
  57. */
  58. export
  59. function newUntitled(manager: DocumentManager, options: Contents.ICreateOptions): Promise<Contents.IModel> {
  60. if (options.type === 'file') {
  61. options.ext = options.ext || '.txt';
  62. }
  63. return manager.services.contents.newUntitled(options);
  64. }
  65. /**
  66. * Rename a file with optional dialog.
  67. */
  68. export
  69. function renameFile(manager: DocumentManager, oldPath: string, newPath: string, basePath = ''): Promise<Contents.IModel> {
  70. let { services } = manager;
  71. let rename = Private.rename(services, oldPath, newPath, basePath);
  72. return rename.catch(error => {
  73. if (error.xhr) {
  74. error.message = `${error.xhr.statusText} ${error.xhr.status}`;
  75. }
  76. let overwriteBtn = Dialog.warnButton({ label: 'OVERWRITE' });
  77. if (error.message.indexOf('409') !== -1) {
  78. let options = {
  79. title: 'Overwrite file?',
  80. body: `"${newPath}" already exists, overwrite?`,
  81. buttons: [Dialog.cancelButton(), overwriteBtn]
  82. };
  83. return showDialog(options).then(button => {
  84. if (button.accept) {
  85. return Private.overwrite(services, oldPath, newPath, basePath);
  86. }
  87. });
  88. } else {
  89. throw error;
  90. }
  91. });
  92. }
  93. /**
  94. * An error message dialog to show in the filebrowser widget.
  95. */
  96. export
  97. function showErrorMessage(title: string, error: Error): Promise<void> {
  98. console.error(error);
  99. let options = {
  100. title: title,
  101. body: error.message || `File ${title}`,
  102. buttons: [Dialog.okButton()],
  103. okText: 'DISMISS'
  104. };
  105. return showDialog(options).then(() => { /* no-op */ });
  106. }
  107. /**
  108. * A widget used to create a file using a creator.
  109. */
  110. class CreateFromHandler extends Widget {
  111. /**
  112. * Construct a new "create from" dialog.
  113. */
  114. constructor(container: IFileContainer, manager: DocumentManager, creatorName: string) {
  115. super({ node: Private.createCreateFromNode() });
  116. this.addClass(FILE_DIALOG_CLASS);
  117. this._container = container;
  118. this._manager = manager;
  119. this._creatorName = creatorName;
  120. // Check for name conflicts when the inputNode changes.
  121. this.inputNode.addEventListener('input', () => {
  122. let value = this.inputNode.value;
  123. if (value !== this._orig) {
  124. each(this._container.items(), item => {
  125. if (item.name === value) {
  126. this.addClass(FILE_CONFLICT_CLASS);
  127. return;
  128. }
  129. });
  130. }
  131. this.removeClass(FILE_CONFLICT_CLASS);
  132. });
  133. }
  134. /**
  135. * Dispose of the resources used by the widget.
  136. */
  137. dispose(): void {
  138. this._container = null;
  139. this._manager = null;
  140. super.dispose();
  141. }
  142. /**
  143. * Get the input text node.
  144. */
  145. get inputNode(): HTMLInputElement {
  146. return this.node.getElementsByTagName('input')[0] as HTMLInputElement;
  147. }
  148. /**
  149. * Get the kernel dropdown node.
  150. */
  151. get kernelDropdownNode(): HTMLSelectElement {
  152. return this.node.getElementsByTagName('select')[0] as HTMLSelectElement;
  153. }
  154. /**
  155. * Show the createNew dialog.
  156. */
  157. showDialog(): Promise<Widget> {
  158. return showDialog({
  159. title: `Create New ${this._creatorName}`,
  160. body: this.node,
  161. primaryElement: this.inputNode,
  162. buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'CREATE' })]
  163. }).then(result => {
  164. if (result.accept) {
  165. return this._open().then(widget => {
  166. if (!widget) {
  167. return this.showDialog();
  168. }
  169. return widget;
  170. });
  171. }
  172. // this._model.deleteFile('/' + this._orig.path);
  173. return null;
  174. });
  175. }
  176. /**
  177. * Populate the create from widget.
  178. */
  179. populate(): Promise<void> {
  180. let container = this._container;
  181. let manager = this._manager;
  182. let registry = manager.registry;
  183. let creator = registry.getCreator(this._creatorName);
  184. if (!creator) {
  185. return Promise.reject(`Creator not registered: ${this._creatorName}`);
  186. }
  187. let { fileType, widgetName, kernelName } = creator;
  188. let fType = registry.getFileType(fileType);
  189. let ext = '.txt';
  190. let type: Contents.ContentType = 'file';
  191. if (fType) {
  192. ext = fType.extension;
  193. type = fType.contentType || 'file';
  194. }
  195. if (!widgetName || widgetName === 'default') {
  196. this._widgetName = widgetName = registry.defaultWidgetFactory(ext).name;
  197. }
  198. // Handle the kernel preferences.
  199. let preference = registry.getKernelPreference(
  200. ext, widgetName, { name: kernelName }
  201. );
  202. if (!preference.canStart) {
  203. this.node.removeChild(this.kernelDropdownNode.previousSibling);
  204. this.node.removeChild(this.kernelDropdownNode);
  205. } else {
  206. let services = this._manager.services;
  207. ClientSession.populateKernelSelect(this.kernelDropdownNode, {
  208. specs: services.specs,
  209. sessions: services.sessions.running(),
  210. preference
  211. });
  212. }
  213. let path = container.path;
  214. return newUntitled(manager, { ext, path, type }).then(contents => {
  215. let value = this.inputNode.value = contents.name;
  216. this.inputNode.setSelectionRange(0, value.length - ext.length);
  217. this._orig = contents;
  218. });
  219. }
  220. /**
  221. * Open the file and return the document widget.
  222. */
  223. private _open(): Promise<Widget> {
  224. let oldPath = this._orig.name;
  225. let file = this.inputNode.value;
  226. let widgetName = this._widgetName;
  227. let kernelValue = this.kernelDropdownNode ? this.kernelDropdownNode.value
  228. : 'null';
  229. let kernelId: Kernel.IModel;
  230. if (kernelValue !== 'null') {
  231. kernelId = JSON.parse(kernelValue) as Kernel.IModel;
  232. }
  233. if (file !== oldPath) {
  234. let basePath = this._container.path;
  235. let promise = renameFile(this._manager, oldPath, file, basePath);
  236. return promise.then((contents: Contents.IModel) => {
  237. if (!contents) {
  238. return null;
  239. }
  240. return this._manager.open(contents.path, widgetName, kernelId);
  241. });
  242. }
  243. let path = this._orig.path;
  244. return Promise.resolve(this._manager.createNew(path, widgetName, kernelId));
  245. }
  246. private _container: IFileContainer = null;
  247. private _creatorName: string;
  248. private _manager: DocumentManager;
  249. private _orig: Contents.IModel = null;
  250. private _widgetName: string;
  251. }
  252. /**
  253. * A namespace for private data.
  254. */
  255. namespace Private {
  256. /**
  257. * Create the node for a create from handler.
  258. */
  259. export
  260. function createCreateFromNode(): HTMLElement {
  261. let body = document.createElement('div');
  262. let nameTitle = document.createElement('label');
  263. nameTitle.textContent = 'File Name';
  264. let name = document.createElement('input');
  265. let kernelTitle = document.createElement('label');
  266. kernelTitle.textContent = 'Kernel';
  267. let kernelDropdownNode = document.createElement('select');
  268. body.appendChild(nameTitle);
  269. body.appendChild(name);
  270. body.appendChild(kernelTitle);
  271. body.appendChild(kernelDropdownNode);
  272. return body;
  273. }
  274. /**
  275. * Delete a file.
  276. *
  277. * @param manager - The service manager used to delete.
  278. *
  279. * @param: path - The path to the file to be deleted.
  280. *
  281. * @param basePath - The base path to resolve against, defaults to ''.
  282. *
  283. * @returns A promise which resolves when the file is deleted.
  284. *
  285. * #### Notes
  286. * If there is a running session associated with the file and no other
  287. * sessions are using the kernel, the session will be shut down.
  288. */
  289. function deleteFile(manager: IServiceManager, path: string, basePath = ''): Promise<void> {
  290. path = PathExt.resolve(basePath, path);
  291. return this.stopIfNeeded(path).then(() => {
  292. return this._manager.contents.delete(path);
  293. });
  294. }
  295. /**
  296. * Overwrite a file.
  297. *
  298. * @param manager - The service manager used to overwrite.
  299. *
  300. * @param oldPath - The path to the original file.
  301. *
  302. * @param newPath - The path to the new file.
  303. *
  304. * @param basePath - The base path to resolve against, defaults to ''.
  305. *
  306. * @returns A promise containing the new file contents model.
  307. */
  308. export
  309. function overwrite(manager: IServiceManager, oldPath: string, newPath: string, basePath = ''): Promise<Contents.IModel> {
  310. // Cleanly overwrite the file by moving it, making sure the original
  311. // does not exist, and then renaming to the new path.
  312. const tempPath = `${newPath}.${uuid()}`;
  313. const cb = () => rename(manager, tempPath, newPath, basePath);
  314. return rename(manager, oldPath, tempPath, basePath).then(() => {
  315. return deleteFile(manager, newPath);
  316. }).then(cb, cb);
  317. }
  318. /**
  319. * Rename a file or directory.
  320. *
  321. * @param manager - The service manager used to rename.
  322. *
  323. * @param oldPath - The path to the original file.
  324. *
  325. * @param newPath - The path to the new file.
  326. *
  327. * @param basePath - The base path to resolve against, defaults to ''.
  328. *
  329. * @returns A promise containing the new file contents model. The promise
  330. * will reject if the newPath already exists. Use [[overwrite]] to
  331. * overwrite a file.
  332. */
  333. export
  334. function rename(manager: IServiceManager, oldPath: string, newPath: string, basePath = ''): Promise<Contents.IModel> {
  335. // Normalize paths.
  336. oldPath = PathExt.resolve(basePath, oldPath);
  337. newPath = PathExt.resolve(basePath, newPath);
  338. return manager.contents.rename(oldPath, newPath);
  339. }
  340. }