diff --git a/airbyte-webapp/.eslintrc b/airbyte-webapp/.eslintrc index 094e6750d3aa..72aa539aa164 100644 --- a/airbyte-webapp/.eslintrc +++ b/airbyte-webapp/.eslintrc @@ -32,7 +32,7 @@ "group": "internal" }, { - "pattern": "+(config|core|hooks|locales|packages|pages|services|utils|views){/**,}", + "pattern": "+(config|core|hooks|locales|packages|pages|services|types|utils|views){/**,}", "group": "internal", "position": "after" } diff --git a/airbyte-webapp/src/App.tsx b/airbyte-webapp/src/App.tsx index 4c008aebbe30..cf6b168d13e7 100644 --- a/airbyte-webapp/src/App.tsx +++ b/airbyte-webapp/src/App.tsx @@ -9,6 +9,8 @@ import { FeatureService } from "hooks/services/Feature"; import { ServicesProvider } from "core/servicesProvider"; import { ApiServices } from "core/ApiServices"; import { StoreProvider } from "views/common/StoreProvider"; +import { ConfirmationModalService } from "hooks/services/ConfirmationModal"; +import { FormChangeTrackerService } from "hooks/services/FormChangeTracker"; import en from "./locales/en.json"; import GlobalStyle from "./global-styles"; @@ -53,7 +55,11 @@ 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..2629efa8b6d3 --- /dev/null +++ b/airbyte-webapp/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -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 = ({ + onClose, + title, + text, + onSubmit, + submitButtonText, +}) => ( + }> + + + + + + + + + + +); diff --git a/airbyte-webapp/src/components/ConfirmationModal/index.ts b/airbyte-webapp/src/components/ConfirmationModal/index.ts new file mode 100644 index 000000000000..13a5a10e7dd6 --- /dev/null +++ b/airbyte-webapp/src/components/ConfirmationModal/index.ts @@ -0,0 +1 @@ +export { ConfirmationModal } from "./ConfirmationModal"; diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx index ff1d12e67b28..9c2389b50ebd 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx @@ -77,9 +77,11 @@ const CreateConnectionContent: React.FC = ({ }, }); - if (afterSubmitConnection) { - afterSubmitConnection(connection); - } + return { + onSubmitComplete: () => { + afterSubmitConnection?.(connection); + }, + }; }; const onSelectFrequency = (item: IDataItem | null) => { diff --git a/airbyte-webapp/src/components/FormChangeTracker/FormChangeTracker.tsx b/airbyte-webapp/src/components/FormChangeTracker/FormChangeTracker.tsx new file mode 100644 index 000000000000..b0be338e5508 --- /dev/null +++ b/airbyte-webapp/src/components/FormChangeTracker/FormChangeTracker.tsx @@ -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 = ({ 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; +}; diff --git a/airbyte-webapp/src/components/FormChangeTracker/index.ts b/airbyte-webapp/src/components/FormChangeTracker/index.ts new file mode 100644 index 000000000000..f77eff774a4d --- /dev/null +++ b/airbyte-webapp/src/components/FormChangeTracker/index.ts @@ -0,0 +1 @@ +export { FormChangeTracker } from "./FormChangeTracker"; diff --git a/airbyte-webapp/src/hooks/router/useBlocker.ts b/airbyte-webapp/src/hooks/router/useBlocker.ts new file mode 100644 index 000000000000..ee09cd6c5d36 --- /dev/null +++ b/airbyte-webapp/src/hooks/router/useBlocker.ts @@ -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 & { 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]); +}; 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..9f74de6c6f0a --- /dev/null +++ b/airbyte-webapp/src/hooks/services/ConfirmationModal/ConfirmationModalService.tsx @@ -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(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 ( + <> + + {children} + + {state.isOpen && state.confirmationModal ? ( + + ) : null} + + ); +}; 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..033818a35953 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/ConfirmationModal/index.ts @@ -0,0 +1 @@ +export * from "./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..19d864a76a68 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/ConfirmationModal/reducer.ts @@ -0,0 +1,31 @@ +import { ActionType, createAction, createReducer } from "typesafe-actions"; + +import { ConfirmationModalOptions, 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..a7d4aa4619d8 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/ConfirmationModal/types.ts @@ -0,0 +1,13 @@ +import { ConfirmationModalProps } from "components/ConfirmationModal/ConfirmationModal"; + +export type ConfirmationModalOptions = Omit; + +export interface ConfirmationModalServiceApi { + openConfirmationModal: (confirmationModal: ConfirmationModalOptions) => void; + closeConfirmationModal: () => void; +} + +export interface ConfirmationModalState { + isOpen: boolean; + confirmationModal: ConfirmationModalOptions | null; +} diff --git a/airbyte-webapp/src/hooks/services/FormChangeTracker/FormChangeTrackerService.tsx b/airbyte-webapp/src/hooks/services/FormChangeTracker/FormChangeTrackerService.tsx new file mode 100644 index 000000000000..44a7af138434 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/FormChangeTracker/FormChangeTrackerService.tsx @@ -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}; +}; diff --git a/airbyte-webapp/src/hooks/services/FormChangeTracker/hooks.ts b/airbyte-webapp/src/hooks/services/FormChangeTracker/hooks.ts new file mode 100644 index 000000000000..1a7d7fbe3145 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/FormChangeTracker/hooks.ts @@ -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>({}); + +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, + }; +}; diff --git a/airbyte-webapp/src/hooks/services/FormChangeTracker/index.ts b/airbyte-webapp/src/hooks/services/FormChangeTracker/index.ts new file mode 100644 index 000000000000..fc530d8fe4a5 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/FormChangeTracker/index.ts @@ -0,0 +1,2 @@ +export * from "./FormChangeTrackerService"; +export * from "./hooks"; diff --git a/airbyte-webapp/src/hooks/services/FormChangeTracker/types.ts b/airbyte-webapp/src/hooks/services/FormChangeTracker/types.ts new file mode 100644 index 000000000000..a55dd8b71156 --- /dev/null +++ b/airbyte-webapp/src/hooks/services/FormChangeTracker/types.ts @@ -0,0 +1,5 @@ +export interface FormChangeTrackerServiceApi { + trackFormChange: (id: string, changed: boolean) => void; + clearFormChange: (id: string) => void; + clearAllFormChanges: () => void; +} diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index f20240ab9533..c6b30edcf7ce 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -62,6 +62,7 @@ "form.sourceRetest": "Retest source", "form.destinationRetest": "Retest destination", "form.discardChanges": "Discard changes", + "form.discardChangesConfirmation": "There are unsaved changes. Are you sure you want to discard your changes?", "form.every": "Every {value}", "form.testingConnection": "Testing connection...", "form.successTests": "All connection tests passed!", diff --git a/airbyte-webapp/src/packages/cloud/App.tsx b/airbyte-webapp/src/packages/cloud/App.tsx index 08d86ac2a36c..22e6a606b889 100644 --- a/airbyte-webapp/src/packages/cloud/App.tsx +++ b/airbyte-webapp/src/packages/cloud/App.tsx @@ -16,6 +16,8 @@ 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 { ConfirmationModalService } from "hooks/services/ConfirmationModal"; +import { FormChangeTrackerService } from "hooks/services/FormChangeTracker"; import { AppServicesProvider } from "./services/AppServicesProvider"; import { IntercomProvider } from "./services/thirdParty/intercom/IntercomProvider"; @@ -46,13 +48,17 @@ const Services: React.FC = ({ children }) => ( - - - - {children} - - - + + + + + + {children} + + + + + diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx index 983d8b805492..ad149ad3b2ac 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/ReplicationView.tsx @@ -4,6 +4,7 @@ import styled from "styled-components"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; import { useAsyncFn } from "react-use"; +import { FormikHelpers } from "formik"; import { Button, Card } from "components"; import ResetDataModal from "components/ResetDataModal"; @@ -73,7 +74,7 @@ const ReplicationView: React.FC = ({ onAfterSaveSchema, connectionId }) const connection = activeUpdatingSchemaMode ? connectionWithRefreshCatalog : initialConnection; - const onSubmit = async (values: ValuesProps) => { + const onSubmit = async (values: ValuesProps, formikHelpers?: FormikHelpers) => { const initialSyncSchema = connection?.syncCatalog; await updateConnection({ @@ -91,6 +92,8 @@ const ReplicationView: React.FC = ({ onAfterSaveSchema, connectionId }) if (activeUpdatingSchemaMode) { setActiveUpdatingSchemaMode(false); } + + formikHelpers?.resetForm({ values }); }; const onSubmitResetModal = async () => { diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView.tsx index aeef16a44c23..df8053476a59 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView.tsx @@ -5,6 +5,7 @@ import { Field, FieldArray } from "formik"; import { ContentCard, H4 } from "components"; +import { FormikOnSubmit } from "types/formik"; import { NormalizationField } from "views/Connection/ConnectionForm/components/NormalizationField"; import { TransformationField } from "views/Connection/ConnectionForm/components/TransformationField"; import { @@ -41,7 +42,7 @@ const NoSupportedTransformationCard = styled(ContentCard)` const CustomTransformationsCard: React.FC<{ operations: Operation[]; - onSubmit: (newValue: { transformations?: Transformation[] }) => void; + onSubmit: FormikOnSubmit<{ transformations?: Transformation[] }>; }> = ({ operations, onSubmit }) => { const defaultTransformation = useDefaultTransformation(); @@ -72,7 +73,7 @@ const CustomTransformationsCard: React.FC<{ const NormalizationCard: React.FC<{ operations: Operation[]; - onSubmit: (newValue: { normalization?: NormalizationType }) => void; + onSubmit: FormikOnSubmit<{ normalization?: NormalizationType }>; }> = ({ operations, onSubmit }) => { const initialValues = useMemo( () => ({ @@ -80,6 +81,7 @@ const NormalizationCard: React.FC<{ }), [operations] ); + return ( = ({ connection }) = const supportsNormalization = definition.supportsNormalization; const supportsDbt = hasFeature(FeatureItem.AllowCustomDBT) && definition.supportsDbt; - const onSubmit = async (values: { transformations?: Transformation[]; normalization?: NormalizationType }) => { + const onSubmit: FormikOnSubmit<{ transformations?: Transformation[]; normalization?: NormalizationType }> = async ( + values, + { resetForm } + ) => { const newOp = mapFormPropsToOperation(values, connection.operations, workspace.workspaceId); const operations = values.transformations @@ -112,7 +117,7 @@ const TransformationView: React.FC = ({ connection }) = .concat(newOp) : newOp.concat(connection.operations.filter((op) => op.operatorConfiguration.operatorType === OperatorType.Dbt)); - return updateConnection({ + await updateConnection({ namespaceDefinition: connection.namespaceDefinition, namespaceFormat: connection.namespaceFormat, prefix: connection.prefix, @@ -122,6 +127,16 @@ const TransformationView: React.FC = ({ connection }) = status: connection.status, operations: operations, }); + + const nextFormValues: typeof values = {}; + if (values.transformations) { + nextFormValues.transformations = getInitialTransformations(operations); + } + if (values.normalization) { + nextFormValues.normalization = getInitialNormalization(operations, true); + } + + resetForm({ values: nextFormValues }); }; return ( diff --git a/airbyte-webapp/src/types/formik.ts b/airbyte-webapp/src/types/formik.ts new file mode 100644 index 000000000000..143f19634e3e --- /dev/null +++ b/airbyte-webapp/src/types/formik.ts @@ -0,0 +1,3 @@ +import { FormikConfig } from "formik"; + +export type FormikOnSubmit = FormikConfig["onSubmit"]; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx index 0e0d63b1c6a1..d94f1cd186bf 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/ConnectionForm.tsx @@ -1,17 +1,19 @@ import React, { useCallback, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import styled from "styled-components"; -import { Field, FieldProps, Form, Formik } from "formik"; +import { Field, FieldProps, Form, Formik, FormikHelpers } from "formik"; import { ControlLabels, DropDown, DropDownRow, H5, Input, Label } from "components"; import ResetDataModal from "components/ResetDataModal"; import { ModalTypes } from "components/ResetDataModal/types"; +import { FormChangeTracker } from "components/FormChangeTracker"; import { equal } from "utils/objects"; import { useCurrentWorkspace } from "services/workspaces/WorkspacesService"; import { createFormErrorMessage } from "utils/errorStatusMessage"; import { Connection, ConnectionNamespaceDefinition, ScheduleProperties } from "core/domain/connection"; import { useGetDestinationDefinitionSpecification } from "services/connector/DestinationDefinitionSpecificationService"; +import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; import { NamespaceDefinitionField } from "./components/NamespaceDefinitionField"; import CreateControls from "./components/CreateControls"; @@ -78,8 +80,12 @@ const FormContainer = styled(Form)` } `; -type ConnectionFormProps = { - onSubmit: (values: ConnectionFormValues) => void; +interface ConnectionFormSubmitResult { + onSubmitComplete: () => void; +} + +interface ConnectionFormProps { + onSubmit: (values: ConnectionFormValues) => Promise; className?: string; additionBottomControls?: React.ReactNode; successMessage?: React.ReactNode; @@ -93,7 +99,7 @@ type ConnectionFormProps = { additionalSchemaControl?: React.ReactNode; connection: Connection | (Partial & Pick); -}; +} const ConnectionForm: React.FC = ({ onSubmit, @@ -109,6 +115,8 @@ const ConnectionForm: React.FC = ({ connection, }) => { const destDefinition = useGetDestinationDefinitionSpecification(connection.destination.destinationDefinitionId); + const { clearFormChange } = useFormChangeTrackerService(); + const formId = useUniqueFormId(); const [modalIsOpen, setResetModalIsOpen] = useState(false); const [submitError, setSubmitError] = useState(null); @@ -119,7 +127,7 @@ const ConnectionForm: React.FC = ({ const workspace = useCurrentWorkspace(); const onFormSubmit = useCallback( - async (values: FormikConnectionFormValues) => { + async (values: FormikConnectionFormValues, formikHelpers: FormikHelpers) => { const formValues: ConnectionFormValues = connectionValidationSchema.cast(values, { context: { isRequest: true }, }) as unknown as ConnectionFormValues; @@ -128,17 +136,31 @@ const ConnectionForm: React.FC = ({ setSubmitError(null); try { - await onSubmit(formValues); + const result = await onSubmit(formValues); + + formikHelpers.resetForm({ values }); + clearFormChange(formId); const requiresReset = isEditMode && !equal(initialValues.syncCatalog, values.syncCatalog) && !editSchemeMode; if (requiresReset) { setResetModalIsOpen(true); } + + result?.onSubmitComplete?.(); } catch (e) { setSubmitError(e); } }, - [editSchemeMode, initialValues.syncCatalog, isEditMode, onSubmit, connection.operations, workspace.workspaceId] + [ + connection.operations, + workspace.workspaceId, + onSubmit, + clearFormChange, + formId, + isEditMode, + initialValues.syncCatalog, + editSchemeMode, + ] ); const errorMessage = submitError ? createFormErrorMessage(submitError) : null; @@ -153,6 +175,7 @@ const ConnectionForm: React.FC = ({ > {({ isSubmitting, setFieldValue, isValid, dirty, resetForm, values }) => ( +
}> {({ field, meta }: FieldProps) => ( diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/TransformationField.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/TransformationField.tsx index 1c3c512a1366..f3e1546cf23d 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/TransformationField.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/TransformationField.tsx @@ -29,6 +29,7 @@ const TransformationField: React.FC< {(editableItem) => ( setEditableItem(null)} onDone={(transformation) => { if (isDefined(editableItemIdx)) { diff --git a/airbyte-webapp/src/views/Connection/FormCard.tsx b/airbyte-webapp/src/views/Connection/FormCard.tsx index 95f3595fcf46..e260d99fda8b 100644 --- a/airbyte-webapp/src/views/Connection/FormCard.tsx +++ b/airbyte-webapp/src/views/Connection/FormCard.tsx @@ -4,6 +4,8 @@ import { useMutation } from "react-query"; import { useIntl } from "react-intl"; import styled from "styled-components"; +import { FormChangeTracker } from "components/FormChangeTracker"; + import EditControls from "views/Connection/ConnectionForm/components/EditControls"; import { CollapsibleCardProps, CollapsibleCard } from "views/Connection/CollapsibleCard"; import { createFormErrorMessage } from "utils/errorStatusMessage"; @@ -32,6 +34,7 @@ export const FormCard: React.FC< {({ resetForm, isSubmitting, dirty, isValid }) => ( + {children}
void; onDone: (tr: Transformation) => void; + isNewTransformation?: boolean; } const validationSchema = yup.object({ @@ -77,7 +79,12 @@ function prepareLabelFields( // enum with only one value for the moment const TransformationTypes = [{ value: "custom", label: "Custom DBT" }]; -const TransformationForm: React.FC = ({ transformation, onCancel, onDone }) => { +const TransformationForm: React.FC = ({ + transformation, + onCancel, + onDone, + isNewTransformation, +}) => { const formatMessage = useIntl().formatMessage; const operationService = useGetService("OperationService"); @@ -89,9 +96,11 @@ const TransformationForm: React.FC = ({ transformation, onC onDone(values); }, }); + const { dirty } = useFormikContext(); return ( <> +