From c8e866f46c7412960a8a85a11150ca71a7099d7e Mon Sep 17 00:00:00 2001 From: Edmundo Ruiz Ghanem Date: Fri, 8 Apr 2022 15:45:39 -0400 Subject: [PATCH] Add Confimration Modal and Service and update form blocking to use modal --- airbyte-webapp/src/App.tsx | 5 +- .../ConfirmationModal/ConfirmationModal.tsx | 50 +++++++++++++ .../src/components/ConfirmationModal/index.ts | 4 + airbyte-webapp/src/hooks/router/usePrompt.ts | 35 --------- .../ConfirmationModalService.tsx | 74 +++++++++++++++++++ .../hooks/services/ConfirmationModal/index.ts | 4 + .../services/ConfirmationModal/reducer.ts | 31 ++++++++ .../hooks/services/ConfirmationModal/types.ts | 18 +++++ .../src/hooks/useFormNavigationBlocking.ts | 23 ------ .../src/hooks/useFormNavigationBlocking.tsx | 42 +++++++++++ airbyte-webapp/src/packages/cloud/App.tsx | 17 +++-- 11 files changed, 237 insertions(+), 66 deletions(-) create mode 100644 airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx create mode 100644 airbyte-webapp/src/components/ConfirmationModal/index.ts delete mode 100644 airbyte-webapp/src/hooks/router/usePrompt.ts create mode 100644 airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx create mode 100644 airbyte-webapp/src/hooks/services/ConfirmationModal/index.ts create mode 100644 airbyte-webapp/src/hooks/services/ConfirmationModal/reducer.ts create mode 100644 airbyte-webapp/src/hooks/services/ConfirmationModal/types.ts delete mode 100644 airbyte-webapp/src/hooks/useFormNavigationBlocking.ts create mode 100644 airbyte-webapp/src/hooks/useFormNavigationBlocking.tsx diff --git a/airbyte-webapp/src/App.tsx b/airbyte-webapp/src/App.tsx index 4c008aebbe30..8bb82a9b5dad 100644 --- a/airbyte-webapp/src/App.tsx +++ b/airbyte-webapp/src/App.tsx @@ -9,6 +9,7 @@ import { FeatureService } from "hooks/services/Feature"; import { ServicesProvider } from "core/servicesProvider"; import { ApiServices } from "core/ApiServices"; import { StoreProvider } from "views/common/StoreProvider"; +import ConfirmationModalServiceProvider from "hooks/services/ConfirmationModal"; import en from "./locales/en.json"; import GlobalStyle from "./global-styles"; @@ -53,7 +54,9 @@ const Services: React.FC = ({ children }) => ( - {children} + + {children} + diff --git a/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx b/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx new file mode 100644 index 000000000000..183b0d9244a2 --- /dev/null +++ b/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import styled from "styled-components"; +import { FormattedMessage } from "react-intl"; + +import Modal from "components/Modal"; +import { Button } from "components/base/Button"; + +const Content = styled.div` + width: 585px; + font-size: 14px; + line-height: 28px; + padding: 25px; + white-space: pre-line; +`; + +const ButtonContent = styled.div` + padding-top: 28px; + display: flex; + justify-content: flex-end; +`; + +const ButtonWithMargin = styled(Button)` + margin-right: 12px; +`; + +interface Props { + onClose: () => void; + title: React.ReactNode; + text: React.ReactNode; + submitButtonText: React.ReactNode; + onSubmit: () => void; +} + +const ConfirmationModal: React.FC = ({ onClose, title, text, onSubmit, submitButtonText }) => ( + + + {text} + + + + + + + + +); + +export default ConfirmationModal; diff --git a/airbyte-webapp/src/components/ConfirmationModal/index.ts b/airbyte-webapp/src/components/ConfirmationModal/index.ts new file mode 100644 index 000000000000..8b0ae872acbd --- /dev/null +++ b/airbyte-webapp/src/components/ConfirmationModal/index.ts @@ -0,0 +1,4 @@ +import ConfirmationModal from "./ConfirmationModal"; + +export default ConfirmationModal; +export { ConfirmationModal }; diff --git a/airbyte-webapp/src/hooks/router/usePrompt.ts b/airbyte-webapp/src/hooks/router/usePrompt.ts deleted file mode 100644 index e2640dc8336b..000000000000 --- a/airbyte-webapp/src/hooks/router/usePrompt.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Transition } from "history"; - -import { useCallback } from "react"; - -import { useBlocker } from "./useBlocker"; - -/** - * @source https://github.com/remix-run/react-router/issues/8139#issuecomment-1021457943 - */ -export const usePrompt = ( - message: string | ((location: Transition["location"], action: Transition["action"]) => string), - when = true, - onConfirm?: () => void -) => { - const blocker = useCallback( - (tx: Transition) => { - let response; - if (typeof message === "function") { - response = message(tx.location, tx.action); - if (typeof response === "string") { - response = window.confirm(response); - } - } else { - response = window.confirm(message); - } - if (response) { - onConfirm?.(); - tx.retry(); - } - }, - [message, onConfirm] - ); - - return useBlocker(blocker, when); -}; diff --git a/airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx b/airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx new file mode 100644 index 000000000000..bb3521310da0 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx @@ -0,0 +1,74 @@ +import React, { useContext, useEffect, useMemo } from "react"; + +import ConfirmationModalComponent from "components/ConfirmationModal"; + +import useTypesafeReducer from "hooks/useTypesafeReducer"; + +import { ConfirmationModal, ConfirmationModalServiceApi, ConfirmationModalState } from "./types"; +import { actions, initialState, confirmationModalServiceReducer } from "./reducer"; + +const confirmationModalServiceContext = React.createContext(undefined); + +export const useConfirmationModalService: ( + confirmationModal?: ConfirmationModal, + dependencies?: [] +) => { + openConfirmationModal: (confirmationModal: ConfirmationModal) => void; + closeConfirmationModal: () => void; +} = (confirmationModal, dependencies) => { + const confirmationModalService = useContext(confirmationModalServiceContext); + if (!confirmationModalService) { + throw new Error("useConfirmationModalService must be used within a ConfirmationModalService."); + } + + useEffect(() => { + if (confirmationModal) { + confirmationModalService.openConfirmationModal(confirmationModal); + } + return () => { + if (confirmationModal) { + confirmationModalService.closeConfirmationModal(); + } + }; + // eslint-disable-next-line + }, [confirmationModal, confirmationModalService, ...(dependencies ?? [])]); + + return { + openConfirmationModal: confirmationModalService.openConfirmationModal, + closeConfirmationModal: confirmationModalService.closeConfirmationModal, + }; +}; + +const ConfirmationModalService = ({ children }: { children: React.ReactNode }) => { + const [state, { openConfirmationModal, closeConfirmationModal }] = useTypesafeReducer< + ConfirmationModalState, + typeof actions + >(confirmationModalServiceReducer, initialState, actions); + + const confirmationModalService: ConfirmationModalServiceApi = useMemo( + () => ({ + openConfirmationModal, + closeConfirmationModal, + }), + [closeConfirmationModal, openConfirmationModal] + ); + + return ( + <> + + {children} + + {state.isOpen && state.confirmationModal ? ( + + ) : null} + + ); +}; + +export default React.memo(ConfirmationModalService); diff --git a/airbyte-webapp/src/hooks/services/ConfirmationModal/index.ts b/airbyte-webapp/src/hooks/services/ConfirmationModal/index.ts new file mode 100644 index 000000000000..e89bc0bbf953 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/ConfirmationModal/index.ts @@ -0,0 +1,4 @@ +import ConfirmationModalService from "./ConfirmationModalService"; + +export default ConfirmationModalService; +export { ConfirmationModalService }; diff --git a/airbyte-webapp/src/hooks/services/ConfirmationModal/reducer.ts b/airbyte-webapp/src/hooks/services/ConfirmationModal/reducer.ts new file mode 100644 index 000000000000..ad1ea4576e83 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/ConfirmationModal/reducer.ts @@ -0,0 +1,31 @@ +import { ActionType, createAction, createReducer } from "typesafe-actions"; + +import { ConfirmationModal, ConfirmationModalState } from "./types"; + +export const actions = { + openConfirmationModal: createAction("OPEN_CONFIRMATION_MODAL")(), + closeConfirmationModal: createAction("CLOSE_CONFIRMATION_MODAL")(), +}; + +type Actions = ActionType; + +export const initialState: ConfirmationModalState = { + isOpen: false, + confirmationModal: null, +}; + +export const confirmationModalServiceReducer = createReducer(initialState) + .handleAction(actions.openConfirmationModal, (state, action): ConfirmationModalState => { + return { + ...state, + isOpen: true, + confirmationModal: action.payload, + }; + }) + .handleAction(actions.closeConfirmationModal, (state): ConfirmationModalState => { + return { + ...state, + isOpen: false, + confirmationModal: null, + }; + }); diff --git a/airbyte-webapp/src/hooks/services/ConfirmationModal/types.ts b/airbyte-webapp/src/hooks/services/ConfirmationModal/types.ts new file mode 100644 index 000000000000..ab578144e9eb --- /dev/null +++ b/airbyte-webapp/src/hooks/services/ConfirmationModal/types.ts @@ -0,0 +1,18 @@ +import React from "react"; + +export interface ConfirmationModal { + submitButtonText: React.ReactNode; + title: React.ReactNode; + text: React.ReactNode; + onSubmit: () => Promise; +} + +export interface ConfirmationModalServiceApi { + openConfirmationModal: (confirmationModal: ConfirmationModal) => void; + closeConfirmationModal: () => void; +} + +export interface ConfirmationModalState { + isOpen: boolean; + confirmationModal: ConfirmationModal | null; +} diff --git a/airbyte-webapp/src/hooks/useFormNavigationBlocking.ts b/airbyte-webapp/src/hooks/useFormNavigationBlocking.ts deleted file mode 100644 index 233457b7f2ab..000000000000 --- a/airbyte-webapp/src/hooks/useFormNavigationBlocking.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useCallback, useMemo } from "react"; -import { createGlobalState } from "react-use"; - -import { usePrompt } from "./router/usePrompt"; - -export const useBlockingFormsById = createGlobalState>({}); - -const useFormNavigationBlockingPrompt = () => { - const [blockingFormsById, setBlockingFormsById] = useBlockingFormsById(); - - const isFormBlocking = useMemo( - () => Object.values(blockingFormsById ?? {}).reduce((acc, value) => acc || value, false), - [blockingFormsById] - ); - - const onConfirm = useCallback(() => { - setBlockingFormsById({}); - }, [setBlockingFormsById]); - - usePrompt("Navigate to another page? Changes you made will not be saved.", isFormBlocking, onConfirm); -}; - -export default useFormNavigationBlockingPrompt; diff --git a/airbyte-webapp/src/hooks/useFormNavigationBlocking.tsx b/airbyte-webapp/src/hooks/useFormNavigationBlocking.tsx new file mode 100644 index 000000000000..bb629f6ae225 --- /dev/null +++ b/airbyte-webapp/src/hooks/useFormNavigationBlocking.tsx @@ -0,0 +1,42 @@ +import type { Transition } from "history"; + +import { useCallback, useMemo } from "react"; +import { createGlobalState } from "react-use"; + +import { useBlocker } from "./router/useBlocker"; +import { useConfirmationModalService } from "./services/ConfirmationModal/ConfirmationModalService"; +import { ConfirmationModal } from "./services/ConfirmationModal/types"; + +export const useBlockingFormsById = createGlobalState>({}); + +const useFormNavigationBlocking = () => { + const [blockingFormsById, setBlockingFormsById] = useBlockingFormsById(); + const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); + + const blocker = useCallback( + (tx: Transition) => { + const modalData: ConfirmationModal = { + title: "Discard changes", + text: "There are unsaved changes. Are you sure you want to discard your changes?", + submitButtonText: "Discard changes", + onSubmit: async () => { + setBlockingFormsById({}); + closeConfirmationModal(); + tx.retry(); + }, + }; + + openConfirmationModal(modalData); + }, + [closeConfirmationModal, openConfirmationModal, setBlockingFormsById] + ); + + const isFormBlocking = useMemo( + () => Object.values(blockingFormsById ?? {}).reduce((acc, value) => acc || value, false), + [blockingFormsById] + ); + + useBlocker(blocker, isFormBlocking); +}; + +export default useFormNavigationBlocking; diff --git a/airbyte-webapp/src/packages/cloud/App.tsx b/airbyte-webapp/src/packages/cloud/App.tsx index 08d86ac2a36c..78cea2b9a48a 100644 --- a/airbyte-webapp/src/packages/cloud/App.tsx +++ b/airbyte-webapp/src/packages/cloud/App.tsx @@ -16,6 +16,7 @@ import { AnalyticsProvider } from "views/common/AnalyticsProvider"; import { FeatureService } from "hooks/services/Feature"; import { AuthenticationProvider } from "packages/cloud/services/auth/AuthService"; import { StoreProvider } from "views/common/StoreProvider"; +import ConfirmationModalServiceProvider from "hooks/services/ConfirmationModal"; import { AppServicesProvider } from "./services/AppServicesProvider"; import { IntercomProvider } from "./services/thirdParty/intercom/IntercomProvider"; @@ -46,13 +47,15 @@ const Services: React.FC = ({ children }) => ( - - - - {children} - - - + + + + + {children} + + + +