-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add the ability to block navigation when forms have not been saved (#…
…11831) * Add navigation blocking when forms are dirty and not saved * Add hooks to block and display prompt on navigation * Add components to indicate which form is blocking * Update connection replication and transformation components to block on edit * Update main page component to show prompt * Add Confimration Modal and Service and update form blocking to use modal * FormNavigationBlocker -> FormChangesTracker useFormNavigationBlocking -> useDiscardFormChangesConfirmation Update exports from default to module * Add confirmation modal text to i18n file * Used formatted title in ConfirmationModal component * Move Form change tracking to a service pattern Fix forms in replications and transformations not resetting the data Add types to import sorting and add Formik helper type for onSubmit functions * Clear all form data after submission in ConnectionForm component Update ConnectionForm submission to handle post submission result from callback * ConnectionForm now clears its own form tracking Add hook to generate unique form id Update form tracker component to use new hook if no id is specified * Adjust margins on ConfirmationModal to more closely match design * Update useUniqueFormId hook id generation to exclude trailing slash * Cleanup code based on pull request review * Remove uneeded dependency spread in useConfirmationModalService hook * Remove data-id from confirmation modal component Update FormChangeTracker to useEffect over useLayoutEffect Memoize useConfirmationModalService hook Remove memoization of ConfirmationModalService provider Simplify useUniqueFormId * Update FormTrackerService check from reduce to some Co-authored-by: Tim Roes <[email protected]> Co-authored-by: Tim Roes <[email protected]>
- Loading branch information
Showing
25 changed files
with
418 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
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` | ||
margin-top: 26px; | ||
display: flex; | ||
justify-content: flex-end; | ||
`; | ||
|
||
const ButtonWithMargin = styled(Button)` | ||
margin-right: 12px; | ||
`; | ||
|
||
export interface ConfirmationModalProps { | ||
onClose: () => void; | ||
title: string; | ||
text: string; | ||
submitButtonText: string; | ||
onSubmit: () => void; | ||
} | ||
|
||
export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({ | ||
onClose, | ||
title, | ||
text, | ||
onSubmit, | ||
submitButtonText, | ||
}) => ( | ||
<Modal onClose={onClose} title={<FormattedMessage id={title} />}> | ||
<Content> | ||
<FormattedMessage id={text} /> | ||
<ButtonContent> | ||
<ButtonWithMargin onClick={onClose} type="button" secondary> | ||
<FormattedMessage id="form.cancel" /> | ||
</ButtonWithMargin> | ||
<Button type="button" danger onClick={onSubmit}> | ||
<FormattedMessage id={submitButtonText} /> | ||
</Button> | ||
</ButtonContent> | ||
</Content> | ||
</Modal> | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { ConfirmationModal } from "./ConfirmationModal"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
airbyte-webapp/src/components/FormChangeTracker/FormChangeTracker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { useEffect } from "react"; | ||
import { usePrevious } from "react-use"; | ||
|
||
import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; | ||
|
||
interface Props { | ||
changed: boolean; | ||
formId?: string; | ||
} | ||
|
||
export const FormChangeTracker: React.FC<Props> = ({ changed, formId }) => { | ||
const id = useUniqueFormId(formId); | ||
const prevChanged = usePrevious(changed); | ||
|
||
const { trackFormChange } = useFormChangeTrackerService(); | ||
|
||
useEffect(() => { | ||
if (changed !== prevChanged) { | ||
trackFormChange(id, changed); | ||
} | ||
}, [id, changed, trackFormChange, prevChanged]); | ||
|
||
return null; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { FormChangeTracker } from "./FormChangeTracker"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import type { Blocker, History, Transition } from "history"; | ||
|
||
import { ContextType, useContext, useEffect } from "react"; | ||
import { Navigator as BaseNavigator, UNSAFE_NavigationContext as NavigationContext } from "react-router-dom"; | ||
|
||
interface Navigator extends BaseNavigator { | ||
block: History["block"]; | ||
} | ||
|
||
type NavigationContextWithBlock = ContextType<typeof NavigationContext> & { navigator: Navigator }; | ||
|
||
/** | ||
* @source https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874 | ||
*/ | ||
export const useBlocker = (blocker: Blocker, block = true) => { | ||
const { navigator } = useContext(NavigationContext) as NavigationContextWithBlock; | ||
|
||
useEffect(() => { | ||
if (!block) { | ||
return; | ||
} | ||
|
||
const unblock = navigator.block((tx: Transition) => { | ||
const autoUnblockingTx = { | ||
...tx, | ||
retry() { | ||
// Automatically unblock the transition so it can play all the way | ||
// through before retrying it. TODO: Figure out how to re-enable | ||
// this block if the transition is cancelled for some reason. | ||
unblock(); | ||
tx.retry(); | ||
}, | ||
}; | ||
|
||
blocker(autoUnblockingTx); | ||
}); | ||
|
||
return unblock; | ||
}, [navigator, blocker, block]); | ||
}; |
71 changes: 71 additions & 0 deletions
71
airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import React, { useContext, useEffect, useMemo } from "react"; | ||
|
||
import { ConfirmationModal } from "components/ConfirmationModal"; | ||
|
||
import useTypesafeReducer from "hooks/useTypesafeReducer"; | ||
|
||
import { ConfirmationModalOptions, ConfirmationModalServiceApi, ConfirmationModalState } from "./types"; | ||
import { actions, initialState, confirmationModalServiceReducer } from "./reducer"; | ||
|
||
const ConfirmationModalServiceContext = React.createContext<ConfirmationModalServiceApi | undefined>(undefined); | ||
|
||
export const useConfirmationModalService: (confirmationModal?: ConfirmationModalOptions) => { | ||
openConfirmationModal: (confirmationModal: ConfirmationModalOptions) => void; | ||
closeConfirmationModal: () => void; | ||
} = (confirmationModal) => { | ||
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(); | ||
} | ||
}; | ||
}, [confirmationModal, confirmationModalService]); | ||
|
||
return useMemo( | ||
() => ({ | ||
openConfirmationModal: confirmationModalService.openConfirmationModal, | ||
closeConfirmationModal: confirmationModalService.closeConfirmationModal, | ||
}), | ||
[confirmationModalService] | ||
); | ||
}; | ||
|
||
export 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 ( | ||
<> | ||
<ConfirmationModalServiceContext.Provider value={confirmationModalService}> | ||
{children} | ||
</ConfirmationModalServiceContext.Provider> | ||
{state.isOpen && state.confirmationModal ? ( | ||
<ConfirmationModal | ||
onClose={closeConfirmationModal} | ||
title={state.confirmationModal.title} | ||
text={state.confirmationModal.text} | ||
onSubmit={state.confirmationModal.onSubmit} | ||
submitButtonText={state.confirmationModal.submitButtonText} | ||
/> | ||
) : null} | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./ConfirmationModalService"; |
31 changes: 31 additions & 0 deletions
31
airbyte-webapp/src/hooks/services/ConfirmationModal/reducer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { ActionType, createAction, createReducer } from "typesafe-actions"; | ||
|
||
import { ConfirmationModalOptions, ConfirmationModalState } from "./types"; | ||
|
||
export const actions = { | ||
openConfirmationModal: createAction("OPEN_CONFIRMATION_MODAL")<ConfirmationModalOptions>(), | ||
closeConfirmationModal: createAction("CLOSE_CONFIRMATION_MODAL")(), | ||
}; | ||
|
||
type Actions = ActionType<typeof actions>; | ||
|
||
export const initialState: ConfirmationModalState = { | ||
isOpen: false, | ||
confirmationModal: null, | ||
}; | ||
|
||
export const confirmationModalServiceReducer = createReducer<ConfirmationModalState, Actions>(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, | ||
}; | ||
}); |
13 changes: 13 additions & 0 deletions
13
airbyte-webapp/src/hooks/services/ConfirmationModal/types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { ConfirmationModalProps } from "components/ConfirmationModal/ConfirmationModal"; | ||
|
||
export type ConfirmationModalOptions = Omit<ConfirmationModalProps, "onClose">; | ||
|
||
export interface ConfirmationModalServiceApi { | ||
openConfirmationModal: (confirmationModal: ConfirmationModalOptions) => void; | ||
closeConfirmationModal: () => void; | ||
} | ||
|
||
export interface ConfirmationModalState { | ||
isOpen: boolean; | ||
confirmationModal: ConfirmationModalOptions | null; | ||
} |
38 changes: 38 additions & 0 deletions
38
airbyte-webapp/src/hooks/services/FormChangeTracker/FormChangeTrackerService.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import type { Transition } from "history"; | ||
|
||
import React, { useCallback, useMemo } from "react"; | ||
|
||
import { useBlocker } from "hooks/router/useBlocker"; | ||
|
||
import { useConfirmationModalService } from "../ConfirmationModal"; | ||
import { useChangedFormsById } from "./hooks"; | ||
|
||
export const FormChangeTrackerService: React.FC = ({ children }) => { | ||
const [changedFormsById, setChangedFormsById] = useChangedFormsById(); | ||
const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); | ||
|
||
const blocker = useCallback( | ||
(tx: Transition) => { | ||
openConfirmationModal({ | ||
title: "form.discardChanges", | ||
text: "form.discardChangesConfirmation", | ||
submitButtonText: "form.discardChanges", | ||
onSubmit: () => { | ||
setChangedFormsById({}); | ||
closeConfirmationModal(); | ||
tx.retry(); | ||
}, | ||
}); | ||
}, | ||
[closeConfirmationModal, openConfirmationModal, setChangedFormsById] | ||
); | ||
|
||
const formsChanged = useMemo( | ||
() => Object.values(changedFormsById ?? {}).some((formChanged) => formChanged), | ||
[changedFormsById] | ||
); | ||
|
||
useBlocker(blocker, formsChanged); | ||
|
||
return <>{children}</>; | ||
}; |
39 changes: 39 additions & 0 deletions
39
airbyte-webapp/src/hooks/services/FormChangeTracker/hooks.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { useCallback, useMemo } from "react"; | ||
import { createGlobalState } from "react-use"; | ||
import { uniqueId } from "lodash"; | ||
|
||
import { FormChangeTrackerServiceApi } from "./types"; | ||
|
||
export const useChangedFormsById = createGlobalState<Record<string, boolean>>({}); | ||
|
||
export const useUniqueFormId = (formId?: string) => useMemo(() => formId ?? uniqueId("form_"), [formId]); | ||
|
||
export const useFormChangeTrackerService = (): FormChangeTrackerServiceApi => { | ||
const [changedFormsById, setChangedFormsById] = useChangedFormsById(); | ||
|
||
const clearAllFormChanges = useCallback(() => { | ||
setChangedFormsById({}); | ||
}, [setChangedFormsById]); | ||
|
||
const clearFormChange = useCallback( | ||
(id: string) => { | ||
setChangedFormsById({ ...changedFormsById, [id]: false }); | ||
}, | ||
[changedFormsById, setChangedFormsById] | ||
); | ||
|
||
const trackFormChange = useCallback( | ||
(id: string, changed: boolean) => { | ||
if (Boolean(changedFormsById?.[id]) !== changed) { | ||
setChangedFormsById({ ...changedFormsById, [id]: changed }); | ||
} | ||
}, | ||
[changedFormsById, setChangedFormsById] | ||
); | ||
|
||
return { | ||
trackFormChange, | ||
clearFormChange, | ||
clearAllFormChanges, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./FormChangeTrackerService"; | ||
export * from "./hooks"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export interface FormChangeTrackerServiceApi { | ||
trackFormChange: (id: string, changed: boolean) => void; | ||
clearFormChange: (id: string) => void; | ||
clearAllFormChanges: () => void; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.