From 042f980b89fe9a04a70ea5abed165f3722fe9a6d Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Fri, 30 Jul 2021 10:27:27 -0700 Subject: [PATCH] Replace window.confirm and window.alert with modal dialog (#965) --- src/Explorer/Controls/Dialog.tsx | 55 +++++++++++++++- .../QueriesGridComponent.tsx | 66 +++++++++++-------- .../DataSamples/DataSamplesUtil.test.ts | 1 - src/Explorer/DataSamples/DataSamplesUtil.ts | 5 +- src/Explorer/Explorer.tsx | 58 +++------------- .../Notebook/NotebookComponent/epics.ts | 35 +++++----- src/Explorer/Notebook/NotebookManager.tsx | 41 ++++++------ .../Tables/DataTable/TableCommands.ts | 34 ++++++---- src/Explorer/Tabs/ConflictsTab.ts | 29 +++++--- src/Explorer/Tabs/DocumentsTab.ts | 48 +++++++++----- src/Explorer/Tabs/MongoDocumentsTab.ts | 5 +- src/Explorer/Tabs/NotebookV2Tab.ts | 19 +++--- src/Explorer/Tree/ConflictId.ts | 28 ++++---- src/Explorer/Tree/DocumentId.ts | 15 ++++- src/Explorer/Tree/ResourceTree.tsx | 37 ++++++----- src/Explorer/Tree/ResourceTreeAdapter.tsx | 37 ++++++----- src/Explorer/Tree/StoredProcedure.ts | 22 ++++--- src/Explorer/Tree/Trigger.ts | 22 ++++--- src/Explorer/Tree/UserDefinedFunction.ts | 26 +++++--- src/Utils/GalleryUtils.test.ts | 17 ++--- src/Utils/GalleryUtils.ts | 6 +- tsconfig.strict.json | 1 - 22 files changed, 353 insertions(+), 254 deletions(-) diff --git a/src/Explorer/Controls/Dialog.tsx b/src/Explorer/Controls/Dialog.tsx index ad9805348..d5e308d69 100644 --- a/src/Explorer/Controls/Dialog.tsx +++ b/src/Explorer/Controls/Dialog.tsx @@ -23,13 +23,66 @@ export interface DialogState { dialogProps?: DialogProps; openDialog: (props: DialogProps) => void; closeDialog: () => void; + showOkCancelModalDialog: ( + title: string, + subText: string, + okLabel: string, + onOk: () => void, + cancelLabel: string, + onCancel: () => void, + choiceGroupProps?: IChoiceGroupProps, + textFieldProps?: TextFieldProps, + primaryButtonDisabled?: boolean + ) => void; + showOkModalDialog: (title: string, subText: string) => void; } -export const useDialog: UseStore = create((set) => ({ +export const useDialog: UseStore = create((set, get) => ({ visible: false, openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })), closeDialog: () => set((state) => ({ visible: false, openDialog: state.openDialog, closeDialog: state.closeDialog }), true), + showOkCancelModalDialog: ( + title: string, + subText: string, + okLabel: string, + onOk: () => void, + cancelLabel: string, + onCancel: () => void, + choiceGroupProps?: IChoiceGroupProps, + textFieldProps?: TextFieldProps, + primaryButtonDisabled?: boolean + ): void => + get().openDialog({ + isModal: true, + title, + subText, + primaryButtonText: okLabel, + secondaryButtonText: cancelLabel, + onPrimaryButtonClick: () => { + get().closeDialog(); + onOk && onOk(); + }, + onSecondaryButtonClick: () => { + get().closeDialog(); + onCancel && onCancel(); + }, + choiceGroupProps, + textFieldProps, + primaryButtonDisabled, + }), + showOkModalDialog: (title: string, subText: string): void => + get().openDialog({ + isModal: true, + title, + subText, + primaryButtonText: "Close", + secondaryButtonText: undefined, + onPrimaryButtonClick: () => { + get().closeDialog(); + }, + onSecondaryButtonClick: undefined, + }), })); export interface TextFieldProps extends ITextFieldProps { diff --git a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx index 3c3ef24b7..236dea5c3 100644 --- a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx +++ b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx @@ -29,6 +29,7 @@ import { QueriesClient } from "../../../Common/QueriesClient"; import * as DataModels from "../../../Contracts/DataModels"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; +import { useDialog } from "../Dialog"; const title = "Open Saved Queries"; @@ -222,35 +223,42 @@ export class QueriesGridComponent extends React.Component { - if (window.confirm("Are you sure you want to delete this query?")) { - const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, { - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: title, - }); - try { - await this.props.queriesClient.deleteQuery(query); - TelemetryProcessor.traceSuccess( - Action.DeleteSavedQuery, - { - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: title, - }, - startKey - ); - } catch (error) { - TelemetryProcessor.traceFailure( - Action.DeleteSavedQuery, - { - dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: title, - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - startKey - ); - } - await this.fetchSavedQueries(); // get latest state - } + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + "Are you sure you want to delete this query?", + "Delete", + async () => { + const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, { + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: title, + }); + try { + await this.props.queriesClient.deleteQuery(query); + TelemetryProcessor.traceSuccess( + Action.DeleteSavedQuery, + { + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: title, + }, + startKey + ); + } catch (error) { + TelemetryProcessor.traceFailure( + Action.DeleteSavedQuery, + { + dataExplorerArea: Constants.Areas.ContextualPane, + paneTitle: title, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); + } + await this.fetchSavedQueries(); // get latest state + }, + "Cancel", + undefined + ); }, }, ], diff --git a/src/Explorer/DataSamples/DataSamplesUtil.test.ts b/src/Explorer/DataSamples/DataSamplesUtil.test.ts index f8ff6f8e5..c8ae2e66a 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.test.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.test.ts @@ -17,7 +17,6 @@ describe("DataSampleUtils", () => { collections: ko.observableArray([collection]), } as Database; const explorer = {} as Explorer; - explorer.showOkModalDialog = () => {}; useDatabases.getState().addDatabases([database]); const dataSamplesUtil = new DataSamplesUtil(explorer); diff --git a/src/Explorer/DataSamples/DataSamplesUtil.ts b/src/Explorer/DataSamples/DataSamplesUtil.ts index 4007608c0..d28ef0426 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.ts @@ -1,6 +1,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { userContext } from "../../UserContext"; import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { useDatabases } from "../useDatabases"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; @@ -20,7 +21,7 @@ export class DataSamplesUtil { const containerName = generator.getCollectionId(); if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) { const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`; - this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); + useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg); logConsoleError(msg); return; } @@ -29,7 +30,7 @@ export class DataSamplesUtil { .createSampleContainerAsync() .catch((error) => logConsoleError(`Error creating sample container: ${error}`)); const msg = `The sample ${containerName} in database ${databaseName} has been successfully created.`; - this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); + useDialog.getState().showOkModalDialog(DataSamplesUtil.DialogTitle, msg); logConsoleInfo(msg); } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 79f6fec11..a4cd403f0 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1,4 +1,3 @@ -import { IChoiceGroupProps } from "@fluentui/react"; import * as ko from "knockout"; import React from "react"; import _ from "underscore"; @@ -35,7 +34,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import "./ComponentRegisterer"; -import { DialogProps, TextFieldProps, useDialog } from "./Controls/Dialog"; +import { DialogProps, useDialog } from "./Controls/Dialog"; import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; import * as FileSystemUtil from "./Notebook/FileSystemUtil"; @@ -548,7 +547,7 @@ export default class Explorer { const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent); promise .then(() => this.resourceTree.triggerRender()) - .catch((reason) => this.showOkModalDialog("Unable to upload file", reason)); + .catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", reason)); return promise; } @@ -614,51 +613,6 @@ export default class Explorer { this.notebookManager?.openCopyNotebookPane(name, content); } - public showOkModalDialog(title: string, msg: string): void { - useDialog.getState().openDialog({ - isModal: true, - title, - subText: msg, - primaryButtonText: "Close", - secondaryButtonText: undefined, - onPrimaryButtonClick: () => { - useDialog.getState().closeDialog(); - }, - onSecondaryButtonClick: undefined, - }); - } - - public showOkCancelModalDialog( - title: string, - msg: string, - okLabel: string, - onOk: () => void, - cancelLabel: string, - onCancel: () => void, - choiceGroupProps?: IChoiceGroupProps, - textFieldProps?: TextFieldProps, - isPrimaryButtonDisabled?: boolean - ): void { - useDialog.getState().openDialog({ - isModal: true, - title, - subText: msg, - primaryButtonText: okLabel, - secondaryButtonText: cancelLabel, - onPrimaryButtonClick: () => { - useDialog.getState().closeDialog(); - onOk && onOk(); - }, - onSecondaryButtonClick: () => { - useDialog.getState().closeDialog(); - onCancel && onCancel(); - }, - choiceGroupProps, - textFieldProps, - primaryButtonDisabled: isPrimaryButtonDisabled, - }); - } - /** * Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree. * Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder. @@ -732,7 +686,9 @@ export default class Explorer { return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path); }); if (openedNotebookTabs.length > 0) { - this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); + useDialog + .getState() + .showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); } else { useSidePanel.getState().openSidePanel( "Rename Notebook", @@ -862,7 +818,9 @@ export default class Explorer { return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path); }); if (openedNotebookTabs.length > 0) { - this.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); + useDialog + .getState() + .showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again."); return Promise.reject(); } diff --git a/src/Explorer/Notebook/NotebookComponent/epics.ts b/src/Explorer/Notebook/NotebookComponent/epics.ts index 718028dab..9e6616a06 100644 --- a/src/Explorer/Notebook/NotebookComponent/epics.ts +++ b/src/Explorer/Notebook/NotebookComponent/epics.ts @@ -38,6 +38,7 @@ import { useTabs } from "../../../hooks/useTabs"; import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationConsoleUtils"; +import { useDialog } from "../../Controls/Dialog"; import * as FileSystemUtil from "../FileSystemUtil"; import * as cdbActions from "../NotebookComponent/actions"; import { NotebookUtil } from "../NotebookUtil"; @@ -686,10 +687,8 @@ const handleKernelConnectionLostEpic = ( logConsoleError(msg); logFailureToTelemetry(state, "Kernel restart error", msg); - const explorer = window.dataExplorer; - if (explorer) { - explorer.showOkModalDialog("kernel restarts", msg); - } + useDialog.getState().showOkModalDialog("kernel restarts", msg); + return of(EMPTY); } @@ -773,8 +772,7 @@ const closeUnsupportedMimetypesEpic = ( ofType(actions.FETCH_CONTENT_FULFILLED), mergeMap((action) => { const mimetype = action.payload.model.mimetype; - const explorer = window.dataExplorer; - if (explorer && !TextFile.handles(mimetype)) { + if (!TextFile.handles(mimetype)) { const filepath = action.payload.filepath; // Close tab and show error message useTabs @@ -783,7 +781,7 @@ const closeUnsupportedMimetypesEpic = ( (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) ); const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`; - explorer.showOkModalDialog("File cannot be rendered", msg); + useDialog.getState().showOkModalDialog("File cannot be rendered", msg); logConsoleError(msg); } return EMPTY; @@ -803,19 +801,16 @@ const closeContentFailedToFetchEpic = ( return action$.pipe( ofType(actions.FETCH_CONTENT_FAILED), mergeMap((action) => { - const explorer = window.dataExplorer; - if (explorer) { - const filepath = action.payload.filepath; - // Close tab and show error message - useTabs - .getState() - .closeTabsByComparator( - (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) - ); - const msg = `Failed to load file: ${filepath}.`; - explorer.showOkModalDialog("Failure to load", msg); - logConsoleError(msg); - } + const filepath = action.payload.filepath; + // Close tab and show error message + useTabs + .getState() + .closeTabsByComparator( + (tab: any) => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath) + ); + const msg = `Failed to load file: ${filepath}.`; + useDialog.getState().showOkModalDialog("Failure to load", msg); + logConsoleError(msg); return EMPTY; }) ); diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 4d14abf6a..ed66caab1 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -18,6 +18,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { getFullName } from "../../Utils/UserUtils"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; @@ -172,31 +173,33 @@ export default class NotebookManager { if (error.status === HttpStatusCodes.Unauthorized) { this.gitHubOAuthService.resetToken(); - this.params.container.showOkCancelModalDialog( - undefined, - "Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.", - "Connect to GitHub", - () => - useSidePanel - .getState() - .openSidePanel( - "Connect to GitHub", - - ), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + undefined, + "Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.", + "Connect to GitHub", + () => + useSidePanel + .getState() + .openSidePanel( + "Connect to GitHub", + + ), + "Cancel", + undefined + ); } }; private promptForCommitMsg = (title: string, primaryButtonLabel: string) => { return new Promise((resolve, reject) => { let commitMsg = "Committed from Azure Cosmos DB Notebooks"; - this.params.container.showOkCancelModalDialog( + useDialog.getState().showOkCancelModalDialog( title || "Commit", undefined, primaryButtonLabel || "Commit", diff --git a/src/Explorer/Tables/DataTable/TableCommands.ts b/src/Explorer/Tables/DataTable/TableCommands.ts index 9da9746ee..c5a80f65e 100644 --- a/src/Explorer/Tables/DataTable/TableCommands.ts +++ b/src/Explorer/Tables/DataTable/TableCommands.ts @@ -1,5 +1,6 @@ import Q from "q"; import { userContext } from "../../../UserContext"; +import { useDialog } from "../../Controls/Dialog"; import Explorer from "../../Explorer"; import * as Entities from "../Entities"; import * as DataTableUtilities from "./DataTableUtilities"; @@ -69,19 +70,28 @@ export default class TableCommands { return null; // Error } var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected(); - let deleteMessage: string = "Are you sure you want to delete the selected entities?"; - if (userContext.apiType === "Cassandra") { - deleteMessage = "Are you sure you want to delete the selected rows?"; - } - if (window.confirm(deleteMessage)) { - viewModel.queryTablesTab.container.tableDataClient - .deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete) - .then((results: any) => { - return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => { - viewModel.redrawTableThrottled(); + const deleteMessage: string = + userContext.apiType === "Cassandra" + ? "Are you sure you want to delete the selected rows?" + : "Are you sure you want to delete the selected entities?"; + + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + deleteMessage, + "Delete", + () => { + viewModel.queryTablesTab.container.tableDataClient + .deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete) + .then((results: any) => { + return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => { + viewModel.redrawTableThrottled(); + }); }); - }); - } + }, + "Cancel", + undefined + ); + return null; } diff --git a/src/Explorer/Tabs/ConflictsTab.ts b/src/Explorer/Tabs/ConflictsTab.ts index 6d95b01eb..fb51394e2 100644 --- a/src/Explorer/Tabs/ConflictsTab.ts +++ b/src/Explorer/Tabs/ConflictsTab.ts @@ -21,6 +21,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; import ConflictId from "../Tree/ConflictId"; @@ -228,7 +229,7 @@ export default class ConflictsTab extends TabsBase { this._documentsIterator = this.createIterator(); await this.loadNextPage(); } catch (error) { - window.alert(getErrorMessage(error)); + useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); } } @@ -252,10 +253,23 @@ export default class ConflictsTab extends TabsBase { } public onAcceptChangesClick = async (): Promise => { - if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { - return; + if (this.isEditorDirty()) { + useDialog + .getState() + .showOkCancelModalDialog( + "Unsaved changes", + "Changes will be lost. Do you want to continue?", + "OK", + async () => await this.resolveConflict(), + "Cancel", + undefined + ); + } else { + await this.resolveConflict(); } + }; + private resolveConflict = async (): Promise => { this.isExecutionError(false); this.isExecuting(true); @@ -318,7 +332,7 @@ export default class ConflictsTab extends TabsBase { } catch (error) { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Resolve conflict failed", errorMessage); TelemetryProcessor.traceFailure( Action.ResolveConflict, { @@ -372,7 +386,7 @@ export default class ConflictsTab extends TabsBase { } catch (error) { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Delete conflict failed", errorMessage); TelemetryProcessor.traceFailure( Action.DeleteConflict, { @@ -662,11 +676,6 @@ export default class ConflictsTab extends TabsBase { return jsonObject; } - private _isIgnoreDirtyEditor = (): boolean => { - var msg: string = "Changes will be lost. Do you want to continue?"; - return window.confirm(msg); - }; - private _getPartitionKeyPropertyHeader(): string { return ( (this.partitionKey && diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 2968a3511..504797a68 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -25,6 +25,7 @@ import { userContext } from "../../UserContext"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import * as QueryUtils from "../../Utils/QueryUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; import DocumentId from "../Tree/DocumentId"; @@ -378,7 +379,7 @@ export default class DocumentsTab extends TabsBase { this.isFilterExpanded(false); document.getElementById("errorStatusIcon")?.focus(); } catch (error) { - window.alert(getErrorMessage(error)); + useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); } } @@ -401,18 +402,29 @@ export default class DocumentsTab extends TabsBase { return Q(); } - public onNewDocumentClick = (): Q.Promise => { - if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { - return Q(); + public onNewDocumentClick = (): void => { + if (this.isEditorDirty()) { + useDialog + .getState() + .showOkCancelModalDialog( + "Unsaved changes", + "Changes will be lost. Do you want to continue?", + "OK", + () => this.initializeNewDocument(), + "Cancel", + undefined + ); + } else { + this.initializeNewDocument(); } - this.selectedDocumentId(null); + }; + private initializeNewDocument = (): void => { + this.selectedDocumentId(null); const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4); this.initialDocumentContent(defaultDocument); this.selectedDocumentContent.setBaseline(defaultDocument); this.editorState(ViewModels.DocumentExplorerState.newDocumentValid); - - return Q(); }; public onSaveNewDocumentClick = (): Promise => { @@ -453,7 +465,7 @@ export default class DocumentsTab extends TabsBase { (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Create document failed", errorMessage); TelemetryProcessor.traceFailure( Action.CreateDocument, { @@ -516,7 +528,7 @@ export default class DocumentsTab extends TabsBase { (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Update document failed", errorMessage); TelemetryProcessor.traceFailure( Action.UpdateDocument, { @@ -546,9 +558,16 @@ export default class DocumentsTab extends TabsBase { ? "Are you sure you want to delete the selected item ?" : "Are you sure you want to delete the selected document ?"; - if (window.confirm(msg)) { - await this._deleteDocument(selectedDocumentId); - } + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + msg, + "Delete", + async () => await this._deleteDocument(selectedDocumentId), + "Cancel", + undefined + ); }; public onValidDocumentEdit(): Q.Promise { @@ -617,11 +636,6 @@ export default class DocumentsTab extends TabsBase { } } - private _isIgnoreDirtyEditor = (): boolean => { - var msg: string = "Changes will be lost. Do you want to continue?"; - return window.confirm(msg); - }; - protected __deleteDocument(documentId: DocumentId): Promise { return deleteDocument(this.collection, documentId); } diff --git a/src/Explorer/Tabs/MongoDocumentsTab.ts b/src/Explorer/Tabs/MongoDocumentsTab.ts index 3b3866b9e..fff7cb042 100644 --- a/src/Explorer/Tabs/MongoDocumentsTab.ts +++ b/src/Explorer/Tabs/MongoDocumentsTab.ts @@ -16,6 +16,7 @@ import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { useDialog } from "../Controls/Dialog"; import DocumentId from "../Tree/DocumentId"; import ObjectId from "../Tree/ObjectId"; import DocumentsTab from "./DocumentsTab"; @@ -111,7 +112,7 @@ export default class MongoDocumentsTab extends DocumentsTab { (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Create document failed", errorMessage); TelemetryProcessor.traceFailure( Action.CreateDocument, { @@ -169,7 +170,7 @@ export default class MongoDocumentsTab extends DocumentsTab { (error) => { this.isExecutionError(true); const errorMessage = getErrorMessage(error); - window.alert(errorMessage); + useDialog.getState().showOkModalDialog("Update document failed", errorMessage); TelemetryProcessor.traceFailure( Action.UpdateDocument, { diff --git a/src/Explorer/Tabs/NotebookV2Tab.ts b/src/Explorer/Tabs/NotebookV2Tab.ts index 012406760..01aab3c6a 100644 --- a/src/Explorer/Tabs/NotebookV2Tab.ts +++ b/src/Explorer/Tabs/NotebookV2Tab.ts @@ -17,6 +17,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { useDialog } from "../Controls/Dialog"; import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory"; import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2"; import * as CdbActions from "../Notebook/NotebookComponent/actions"; @@ -59,14 +60,16 @@ export default class NotebookTabV2 extends NotebookTabBase { }; if (this.notebookComponentAdapter.isContentDirty()) { - this.container.showOkCancelModalDialog( - "Close without saving?", - `File has unsaved changes, close without saving?`, - "Close", - cleanup, - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Close without saving?", + `File has unsaved changes, close without saving?`, + "Close", + cleanup, + "Cancel", + undefined + ); return Q.resolve(null); } else { cleanup(); diff --git a/src/Explorer/Tree/ConflictId.ts b/src/Explorer/Tree/ConflictId.ts index c9cf1a98a..89185b80a 100644 --- a/src/Explorer/Tree/ConflictId.ts +++ b/src/Explorer/Tree/ConflictId.ts @@ -1,12 +1,11 @@ -import Q from "q"; +import { extractPartitionKey } from "@azure/cosmos"; import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; -import DocumentId from "./DocumentId"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { extractPartitionKey } from "@azure/cosmos"; -import ConflictsTab from "../Tabs/ConflictsTab"; import { readDocument } from "../../Common/dataAccess/readDocument"; +import * as DataModels from "../../Contracts/DataModels"; +import { useDialog } from "../Controls/Dialog"; +import ConflictsTab from "../Tabs/ConflictsTab"; +import DocumentId from "./DocumentId"; export default class ConflictId { public container: ConflictsTab; @@ -50,13 +49,20 @@ export default class ConflictId { } public click() { - if ( - !this.container.isEditorDirty() || - window.confirm("Your unsaved changes will be lost. Do you want to continue?") - ) { + if (this.container.isEditorDirty()) { + useDialog + .getState() + .showOkCancelModalDialog( + "Unsaved changes", + "Your unsaved changes will be lost. Do you want to continue?", + "OK", + () => this.loadConflict(), + "Cancel", + undefined + ); + } else { this.loadConflict(); } - return; } public async loadConflict(): Promise { diff --git a/src/Explorer/Tree/DocumentId.ts b/src/Explorer/Tree/DocumentId.ts index a30c0b534..af2d0250c 100644 --- a/src/Explorer/Tree/DocumentId.ts +++ b/src/Explorer/Tree/DocumentId.ts @@ -1,5 +1,6 @@ import * as ko from "knockout"; import * as DataModels from "../../Contracts/DataModels"; +import { useDialog } from "../Controls/Dialog"; import DocumentsTab from "../Tabs/DocumentsTab"; export default class DocumentId { @@ -28,10 +29,20 @@ export default class DocumentId { } public click() { - if (!this.container.isEditorDirty() || window.confirm("Your unsaved changes will be lost.")) { + if (this.container.isEditorDirty()) { + useDialog + .getState() + .showOkCancelModalDialog( + "Unsaved changes", + "Your unsaved changes will be lost. Do you want to continue?", + "OK", + () => this.loadDocument(), + "Cancel", + undefined + ); + } else { this.loadDocument(); } - return; } public partitionKeyHeader(): Object { diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index b5b6dfe92..2130c01bb 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -24,6 +24,7 @@ import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; +import { useDialog } from "../Controls/Dialog"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; @@ -254,14 +255,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc label: "Delete", iconSrc: DeleteIcon, onClick: () => { - container.showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}"`, - "Delete", - () => container.deleteNotebookFile(item), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}"`, + "Delete", + () => container.deleteNotebookFile(item), + "Cancel", + undefined + ); }, }, { @@ -319,14 +322,16 @@ export const ResourceTree: React.FC = ({ container }: Resourc label: "Delete", iconSrc: DeleteIcon, onClick: () => { - container.showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}?"`, - "Delete", - () => container.deleteNotebookFile(item), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}?"`, + "Delete", + () => container.deleteNotebookFile(item), + "Cancel", + undefined + ); }, }, { diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 6a00e92e9..6a9352db2 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -27,6 +27,7 @@ import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent"; +import { useDialog } from "../Controls/Dialog"; import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; @@ -712,14 +713,16 @@ export class ResourceTreeAdapter implements ReactAdapter { label: "Delete", iconSrc: DeleteIcon, onClick: () => { - this.container.showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}"`, - "Delete", - () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}"`, + "Delete", + () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), + "Cancel", + undefined + ); }, }, { @@ -777,14 +780,16 @@ export class ResourceTreeAdapter implements ReactAdapter { label: "Delete", iconSrc: DeleteIcon, onClick: () => { - this.container.showOkCancelModalDialog( - "Confirm delete", - `Are you sure you want to delete "${item.name}?"`, - "Delete", - () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), - "Cancel", - undefined - ); + useDialog + .getState() + .showOkCancelModalDialog( + "Confirm delete", + `Are you sure you want to delete "${item.name}?"`, + "Delete", + () => this.container.deleteNotebookFile(item).then(() => this.triggerRender()), + "Cancel", + undefined + ); }, }, { diff --git a/src/Explorer/Tree/StoredProcedure.ts b/src/Explorer/Tree/StoredProcedure.ts index 8e6f77ece..215035ffe 100644 --- a/src/Explorer/Tree/StoredProcedure.ts +++ b/src/Explorer/Tree/StoredProcedure.ts @@ -8,6 +8,7 @@ import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import { getErrorMessage } from "../Tables/Utilities"; import { NewStoredProcedureTab } from "../Tabs/StoredProcedureTab/StoredProcedureTab"; @@ -138,16 +139,21 @@ export default class StoredProcedure { } }; public delete() { - if (!window.confirm("Are you sure you want to delete the stored procedure?")) { - return; - } - - deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + "Are you sure you want to delete the stored procedure?", + "Delete", () => { - useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); - this.collection.children.remove(this); + deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then( + () => { + useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); + this.collection.children.remove(this); + }, + (reason) => {} + ); }, - (reason) => {} + "Cancel", + undefined ); } diff --git a/src/Explorer/Tree/Trigger.ts b/src/Explorer/Tree/Trigger.ts index d90428b65..59d05de60 100644 --- a/src/Explorer/Tree/Trigger.ts +++ b/src/Explorer/Tree/Trigger.ts @@ -6,6 +6,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import TriggerTab from "../Tabs/TriggerTab"; import { useSelectedNode } from "../useSelectedNode"; @@ -99,16 +100,21 @@ export default class Trigger { }; public delete() { - if (!window.confirm("Are you sure you want to delete the trigger?")) { - return; - } - - deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + "Are you sure you want to delete the trigger?", + "Delete", () => { - useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); - this.collection.children.remove(this); + deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then( + () => { + useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); + this.collection.children.remove(this); + }, + (reason) => {} + ); }, - (reason) => {} + "Cancel", + undefined ); } } diff --git a/src/Explorer/Tree/UserDefinedFunction.ts b/src/Explorer/Tree/UserDefinedFunction.ts index 4264c0954..555f547bb 100644 --- a/src/Explorer/Tree/UserDefinedFunction.ts +++ b/src/Explorer/Tree/UserDefinedFunction.ts @@ -6,6 +6,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { useTabs } from "../../hooks/useTabs"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab"; import { useSelectedNode } from "../useSelectedNode"; @@ -95,18 +96,23 @@ export default class UserDefinedFunction { } public delete() { - if (!window.confirm("Are you sure you want to delete the user defined function?")) { - return; - } - - deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( + useDialog.getState().showOkCancelModalDialog( + "Confirm delete", + "Are you sure you want to delete the user defined function?", + "Delete", () => { - useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); - this.collection.children.remove(this); + deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then( + () => { + useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid); + this.collection.children.remove(this); + }, + () => { + /**/ + } + ); }, - () => { - /**/ - } + "Cancel", + undefined ); } } diff --git a/src/Utils/GalleryUtils.test.ts b/src/Utils/GalleryUtils.test.ts index b911fa352..4df3651cd 100644 --- a/src/Utils/GalleryUtils.test.ts +++ b/src/Utils/GalleryUtils.test.ts @@ -1,8 +1,9 @@ -import * as GalleryUtils from "./GalleryUtils"; -import { JunoClient, IGalleryItem } from "../Juno/JunoClient"; import { HttpStatusCodes } from "../Common/Constants"; +import { useDialog } from "../Explorer/Controls/Dialog"; import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import Explorer from "../Explorer/Explorer"; +import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; +import * as GalleryUtils from "./GalleryUtils"; const galleryItem: IGalleryItem = { id: "id", @@ -29,11 +30,11 @@ describe("GalleryUtils", () => { it("downloadItem shows dialog in data explorer", () => { const container = {} as Explorer; - container.showOkCancelModalDialog = jest.fn().mockImplementation(); - GalleryUtils.downloadItem(container, undefined, galleryItem, undefined); - expect(container.showOkCancelModalDialog).toBeCalled(); + expect(useDialog.getState().visible).toBe(true); + expect(useDialog.getState().dialogProps).toBeDefined(); + expect(useDialog.getState().dialogProps.title).toBe("Download to My Notebooks"); }); it("favoriteItem favorites item", async () => { @@ -66,11 +67,11 @@ describe("GalleryUtils", () => { it("deleteItem shows dialog in data explorer", () => { const container = {} as Explorer; - container.showOkCancelModalDialog = jest.fn().mockImplementation(); - GalleryUtils.deleteItem(container, undefined, galleryItem, undefined); - expect(container.showOkCancelModalDialog).toBeCalled(); + expect(useDialog.getState().visible).toBe(true); + expect(useDialog.getState().dialogProps).toBeDefined(); + expect(useDialog.getState().dialogProps.title).toBe("Remove published notebook"); }); it("getGalleryViewerProps gets gallery viewer props correctly", () => { diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index d9a52d348..16a41c6d3 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -3,7 +3,7 @@ import { Notebook } from "@nteract/commutable"; import { NotebookV4 } from "@nteract/commutable/lib/v4"; import { HttpStatusCodes } from "../Common/Constants"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; -import { TextFieldProps } from "../Explorer/Controls/Dialog"; +import { TextFieldProps, useDialog } from "../Explorer/Controls/Dialog"; import { GalleryTab, GalleryViewerComponent, @@ -222,7 +222,7 @@ export function downloadItem( }); const name = data.name; - container.showOkCancelModalDialog( + useDialog.getState().showOkCancelModalDialog( "Download to My Notebooks", `Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`, "Download", @@ -388,7 +388,7 @@ export function deleteItem( if (container) { trace(Action.NotebooksGalleryClickDelete, ActionModifiers.Mark, { notebookId: data.id }); - container.showOkCancelModalDialog( + useDialog.getState().showOkCancelModalDialog( "Remove published notebook", `Would you like to remove ${data.name} from the gallery?`, "Remove", diff --git a/tsconfig.strict.json b/tsconfig.strict.json index d48357cda..ee037db87 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -37,7 +37,6 @@ "./src/Contracts/SelfServeContracts.ts", "./src/Contracts/SubscriptionType.ts", "./src/Contracts/Versions.ts", - "./src/Explorer/Controls/Dialog.tsx", "./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts", "./src/Explorer/Controls/SmartUi/InputUtils.ts", "./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts",