Bläddra i källkod

Merge pull request #8233 from nyu-ossd-s20/exit-dialog

Add save action when exiting unsaved notebook
Steven Silvester 5 år sedan
förälder
incheckning
87b7646455

+ 1 - 0
.gitignore

@@ -59,6 +59,7 @@ MANIFEST
 *.map
 *.swp
 .DS_Store
+.log/
 \#*#
 .#*
 

+ 34 - 21
packages/docmanager/src/widgetmanager.ts

@@ -289,22 +289,30 @@ export class DocumentWidgetManager implements IDisposable {
    *
    * @returns A promise that resolves with whether the widget was closed.
    */
-  protected onClose(widget: Widget): Promise<boolean> {
+  protected async onClose(widget: Widget): Promise<boolean> {
     // Handle dirty state.
-    return this._maybeClose(widget)
-      .then(result => {
-        if (widget.isDisposed) {
+    const [shouldClose, ignoreSave] = await this._maybeClose(widget);
+    if (widget.isDisposed) {
+      return true;
+    }
+    if (shouldClose) {
+      if (!ignoreSave) {
+        const context = Private.contextProperty.get(widget);
+        if (!context) {
           return true;
         }
-        if (result) {
-          widget.dispose();
+        if (context.contentsModel?.writable) {
+          await context.save();
+        } else {
+          await context.saveAs();
         }
-        return result;
-      })
-      .catch(error => {
-        widget.dispose();
-        throw error;
-      });
+      }
+      if (widget.isDisposed) {
+        return true;
+      }
+      widget.dispose();
+    }
+    return shouldClose;
   }
 
   /**
@@ -320,15 +328,15 @@ export class DocumentWidgetManager implements IDisposable {
   /**
    * Ask the user whether to close an unsaved file.
    */
-  private _maybeClose(widget: Widget): Promise<boolean> {
+  private _maybeClose(widget: Widget): Promise<[boolean, boolean]> {
     // Bail if the model is not dirty or other widgets are using the model.)
     const context = Private.contextProperty.get(widget);
     if (!context) {
-      return Promise.resolve(true);
+      return Promise.resolve([true, true]);
     }
     let widgets = Private.widgetsProperty.get(context);
     if (!widgets) {
-      return Promise.resolve(true);
+      return Promise.resolve([true, true]);
     }
     // Filter by whether the factories are read only.
     widgets = toArray(
@@ -342,19 +350,24 @@ export class DocumentWidgetManager implements IDisposable {
     );
     const factory = Private.factoryProperty.get(widget);
     if (!factory) {
-      return Promise.resolve(true);
+      return Promise.resolve([true, true]);
     }
     const model = context.model;
     if (!model.dirty || widgets.length > 1 || factory.readOnly) {
-      return Promise.resolve(true);
+      return Promise.resolve([true, true]);
     }
     const fileName = widget.title.label;
+    const saveLabel = context.contentsModel?.writable ? 'Save' : 'Save as';
     return showDialog({
-      title: 'Close without saving?',
-      body: `File "${fileName}" has unsaved changes, close without saving?`,
-      buttons: [Dialog.cancelButton(), Dialog.warnButton()]
+      title: 'Save your work',
+      body: `Save changes in "${fileName}" before closing?`,
+      buttons: [
+        Dialog.cancelButton(),
+        Dialog.warnButton({ label: 'Discard' }),
+        Dialog.okButton({ label: saveLabel })
+      ]
     }).then(result => {
-      return result.button.accept;
+      return [result.button.accept, result.button.displayType === 'warn'];
     });
   }
 

+ 3 - 3
packages/docmanager/test/widgetmanager.spec.ts

@@ -22,7 +22,7 @@ import { IMessageHandler, Message, MessageLoop } from '@lumino/messaging';
 
 import { Widget } from '@lumino/widgets';
 
-import { acceptDialog, dismissDialog } from '@jupyterlab/testutils';
+import { dangerDialog, dismissDialog } from '@jupyterlab/testutils';
 
 import * as Mock from '@jupyterlab/testutils/lib/mock';
 
@@ -280,7 +280,7 @@ describe('@jupyterlab/docmanager', () => {
         const widget = manager.createWidget(widgetFactory, context);
         const closed = manager.onClose(widget);
 
-        await Promise.all([acceptDialog(), closed]);
+        await Promise.all([dangerDialog(), closed]);
 
         expect(widget.isDisposed).toBe(true);
       });
@@ -315,7 +315,7 @@ describe('@jupyterlab/docmanager', () => {
         const readonly = manager.createWidget(readOnlyFactory, context);
         const closed = manager.onClose(writable);
 
-        await acceptDialog();
+        await dangerDialog();
         await closed;
 
         expect(writable.isDisposed).toBe(true);

+ 16 - 0
testutils/src/common.ts

@@ -332,6 +332,22 @@ export async function acceptDialog(
   }
 }
 
+/**
+ * Click on the warning button in a dialog after it is attached
+ */
+export async function dangerDialog(
+  host: HTMLElement = document.body,
+  timeout: number = 250
+): Promise<void> {
+  await waitForDialog(host, timeout);
+
+  const node = host.getElementsByClassName('jp-mod-warn')[0];
+
+  if (node) {
+    simulate(node as HTMLElement, 'click', { button: 1 });
+  }
+}
+
 /**
  * Dismiss a dialog after it is attached.
  *

+ 1 - 0
testutils/src/index.ts

@@ -22,6 +22,7 @@ export {
   initNotebookContext,
   waitForDialog,
   acceptDialog,
+  dangerDialog,
   dismissDialog
 } from './common';