From 9886059f42365f499f0fa48fb6bfe847f84ed017 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 2 Aug 2023 15:29:20 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=9F=20=F0=9F=94=A7=20[Form=20Migration?= =?UTF-8?q?][Part=201]=20New=20`react-hook-form`=20components=20for=20``=20and=20refactored=20the=20old?= =?UTF-8?q?=20ones=20(#7967)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ArrayOfObjectsEditor.module.scss | 6 +- .../ArrayOfObjectsEditor.tsx | 10 +- .../ArrayOfObjectsHookFormEditor.tsx | 69 ++++++++++++ .../components/EditorHeader.module.scss | 14 --- .../components/EditorHeader.tsx | 33 +++--- .../components/EditorRow.module.scss | 18 +-- .../components/EditorRow.tsx | 21 ++-- .../components/ArrayOfObjectsEditor/index.tsx | 6 +- .../LabeledRadioButton.module.scss | 37 +++---- .../LabeledRadioButton/LabeledRadioButton.tsx | 16 +-- .../components/LabeledRadioButton/index.tsx | 6 +- .../LabeledRadioButtonFormControl.tsx | 39 +++++++ .../NormalizationHookFormField.tsx | 52 +++++++++ .../ConnectionForm/TransformationField.tsx | 2 +- .../TransformationFieldHookForm.tsx | 60 ++++++++++ .../CreateConnectionForm.test.tsx.snap | 25 +++-- .../TransformationHookForm.tsx | 104 ++++++++++++++++++ .../TransformationHookForm/index.tsx | 3 + .../TransformationHookForm/schema.test.ts | 51 +++++++++ .../TransformationHookForm/schema.ts | 26 +++++ .../TransformationHookForm/types.ts | 15 +++ airbyte-webapp/src/locales/en.json | 4 + .../CustomTransformationsCard.tsx | 6 + 23 files changed, 519 insertions(+), 104 deletions(-) create mode 100644 airbyte-webapp/src/components/ArrayOfObjectsEditor/ArrayOfObjectsHookFormEditor.tsx delete mode 100644 airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorHeader.module.scss create mode 100644 airbyte-webapp/src/components/connection/ConnectionForm/LabeledRadioButtonFormControl.tsx create mode 100644 airbyte-webapp/src/components/connection/ConnectionForm/NormalizationHookFormField.tsx create mode 100644 airbyte-webapp/src/components/connection/ConnectionForm/TransformationFieldHookForm.tsx create mode 100644 airbyte-webapp/src/components/connection/TransformationHookForm/TransformationHookForm.tsx create mode 100644 airbyte-webapp/src/components/connection/TransformationHookForm/index.tsx create mode 100644 airbyte-webapp/src/components/connection/TransformationHookForm/schema.test.ts create mode 100644 airbyte-webapp/src/components/connection/TransformationHookForm/schema.ts create mode 100644 airbyte-webapp/src/components/connection/TransformationHookForm/types.ts diff --git a/airbyte-webapp/src/components/ArrayOfObjectsEditor/ArrayOfObjectsEditor.module.scss b/airbyte-webapp/src/components/ArrayOfObjectsEditor/ArrayOfObjectsEditor.module.scss index a8dd2d449d0..d620c469fa4 100644 --- a/airbyte-webapp/src/components/ArrayOfObjectsEditor/ArrayOfObjectsEditor.module.scss +++ b/airbyte-webapp/src/components/ArrayOfObjectsEditor/ArrayOfObjectsEditor.module.scss @@ -1,11 +1,7 @@ @use "scss/colors"; @use "scss/variables"; -.container { - margin-bottom: variables.$spacing-xl; -} - .list { - background-color: colors.$grey-50; border-radius: variables.$border-radius-xs; + overflow: hidden; } diff --git a/airbyte-webapp/src/components/ArrayOfObjectsEditor/ArrayOfObjectsEditor.tsx b/airbyte-webapp/src/components/ArrayOfObjectsEditor/ArrayOfObjectsEditor.tsx index 97cc2587e3b..dafd2e8da89 100644 --- a/airbyte-webapp/src/components/ArrayOfObjectsEditor/ArrayOfObjectsEditor.tsx +++ b/airbyte-webapp/src/components/ArrayOfObjectsEditor/ArrayOfObjectsEditor.tsx @@ -1,6 +1,8 @@ import React from "react"; import { FormattedMessage } from "react-intl"; +import { Box } from "components/ui/Box"; +import { FlexContainer } from "components/ui/Flex"; import { Modal, ModalProps } from "components/ui/Modal"; import { ConnectionFormMode } from "hooks/services/ConnectionForm/ConnectionFormService"; @@ -65,7 +67,7 @@ export const ArrayOfObjectsEditor = ({ return ( <> -
+ ({ disabled={disabled} /> {items.length ? ( -
+ {items.map((item, index) => ( ({ disabled={disabled} /> ))} -
+ ) : null} -
+ {mode !== "readonly" && isEditable && renderEditModal()} ); diff --git a/airbyte-webapp/src/components/ArrayOfObjectsEditor/ArrayOfObjectsHookFormEditor.tsx b/airbyte-webapp/src/components/ArrayOfObjectsEditor/ArrayOfObjectsHookFormEditor.tsx new file mode 100644 index 00000000000..c6834cc6267 --- /dev/null +++ b/airbyte-webapp/src/components/ArrayOfObjectsEditor/ArrayOfObjectsHookFormEditor.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { FieldArrayWithId } from "react-hook-form"; + +import { Box } from "components/ui/Box"; +import { FlexContainer } from "components/ui/Flex"; + +import styles from "./ArrayOfObjectsEditor.module.scss"; +import { EditorHeader } from "./components/EditorHeader"; +import { EditorRow } from "./components/EditorRow"; + +export interface ArrayOfObjectsHookFormEditorProps { + fields: T[]; + mainTitle?: React.ReactNode; + addButtonText?: React.ReactNode; + renderItemName?: (item: T, index: number) => React.ReactNode | undefined; + renderItemDescription?: (item: T, index: number) => React.ReactNode | undefined; + onAddItem: () => void; + onStartEdit: (n: number) => void; + onRemove: (index: number) => void; +} + +/** + * The component is used to render a list of react-hook-form FieldArray items with the ability to add, edit and remove items. + * It's a react-hook-form version of the ArrayOfObjectsEditor component and will replace it in the future. + * @see ArrayOfObjectsEditor + * @param fields + * @param mainTitle + * @param addButtonText + * @param onAddItem + * @param renderItemName + * @param renderItemDescription + * @param onStartEdit + * @param onRemove + * @param mode + * @constructor + */ +export const ArrayOfObjectsHookFormEditor = ({ + fields, + mainTitle, + addButtonText, + onAddItem, + renderItemName, + renderItemDescription, + onStartEdit, + onRemove, +}: ArrayOfObjectsHookFormEditorProps) => ( + + + {fields.length ? ( + + {fields.map((field, index) => ( + + ))} + + ) : null} + +); diff --git a/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorHeader.module.scss b/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorHeader.module.scss deleted file mode 100644 index 885e0e96ce5..00000000000 --- a/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorHeader.module.scss +++ /dev/null @@ -1,14 +0,0 @@ -@use "scss/colors"; -@use "scss/variables"; - -.editorHeader { - display: flex; - justify-content: space-between; - align-items: center; - flex-direction: row; - color: colors.$dark-blue-900; - font-weight: 500; - font-size: variables.$font-size-lg; - line-height: 1.2; - margin: 5px 0 10px; -} diff --git a/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorHeader.tsx b/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorHeader.tsx index f6990cd4570..3979b916e85 100644 --- a/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorHeader.tsx +++ b/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorHeader.tsx @@ -1,22 +1,27 @@ import React from "react"; import { FormattedMessage } from "react-intl"; +import { Box } from "components/ui/Box"; import { Button } from "components/ui/Button"; +import { FlexContainer } from "components/ui/Flex"; +import { Text } from "components/ui/Text"; import { ConnectionFormMode } from "hooks/services/ConnectionForm/ConnectionFormService"; -import styles from "./EditorHeader.module.scss"; - interface EditorHeaderProps { mainTitle?: React.ReactNode; addButtonText?: React.ReactNode; itemsCount: number; onAddItem: () => void; + /** + * seems like "mode" and "disabled" props can be removed since we can control fields enable/disable states on higher levels + * TODO: remove during ArrayOfObjectsEditor refactoring and CreateConnectionForm migration + */ mode?: ConnectionFormMode; disabled?: boolean; } -const EditorHeader: React.FC = ({ +export const EditorHeader: React.FC = ({ itemsCount, onAddItem, mainTitle, @@ -25,15 +30,17 @@ const EditorHeader: React.FC = ({ disabled, }) => { return ( -
- {mainTitle || } - {mode !== "readonly" && ( - - )} -
+ + + + {mainTitle || } + + {mode !== "readonly" && ( + + )} + + ); }; - -export { EditorHeader }; diff --git a/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorRow.module.scss b/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorRow.module.scss index e68c468febe..d195b4a6291 100644 --- a/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorRow.module.scss +++ b/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorRow.module.scss @@ -1,21 +1,14 @@ @use "scss/colors"; @use "scss/variables"; -.container + .container { +.container { border-top: 1px solid colors.$foreground; + background-color: colors.$grey-50; } .body { - display: flex; - justify-content: space-between; - align-items: center; - flex-direction: row; color: colors.$dark-blue; - font-weight: 400; - font-size: variables.$font-size-sm; - line-height: 1.4; - padding: variables.$spacing-xs variables.$spacing-xs variables.$spacing-xs variables.$spacing-md; - gap: variables.$spacing-xs; + padding: variables.$spacing-xs variables.$spacing-md; } .name { @@ -23,8 +16,3 @@ text-overflow: ellipsis; white-space: nowrap; } - -.actions { - display: flex; - flex-direction: row; -} diff --git a/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorRow.tsx b/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorRow.tsx index 2184263b338..b0624e097a3 100644 --- a/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorRow.tsx +++ b/airbyte-webapp/src/components/ArrayOfObjectsEditor/components/EditorRow.tsx @@ -1,9 +1,10 @@ import React from "react"; import { useIntl } from "react-intl"; -import { CrossIcon } from "components/icons/CrossIcon"; -import { PencilIcon } from "components/icons/PencilIcon"; import { Button } from "components/ui/Button"; +import { FlexContainer } from "components/ui/Flex"; +import { Icon } from "components/ui/Icon"; +import { Text } from "components/ui/Text"; import { Tooltip } from "components/ui/Tooltip"; import styles from "./EditorRow.module.scss"; @@ -21,9 +22,11 @@ export const EditorRow: React.FC = ({ name, id, description, onE const { formatMessage } = useIntl(); const body = ( -
-
{name || id}
-
+ + + {name || id} + +
-
+ + ); return ( diff --git a/airbyte-webapp/src/components/ArrayOfObjectsEditor/index.tsx b/airbyte-webapp/src/components/ArrayOfObjectsEditor/index.tsx index b4a645f0160..d8b44d1689c 100644 --- a/airbyte-webapp/src/components/ArrayOfObjectsEditor/index.tsx +++ b/airbyte-webapp/src/components/ArrayOfObjectsEditor/index.tsx @@ -1,4 +1,2 @@ -import { ArrayOfObjectsEditor } from "./ArrayOfObjectsEditor"; - -export default ArrayOfObjectsEditor; -export { ArrayOfObjectsEditor }; +export { ArrayOfObjectsEditor } from "./ArrayOfObjectsEditor"; +export { ArrayOfObjectsHookFormEditor } from "./ArrayOfObjectsHookFormEditor"; diff --git a/airbyte-webapp/src/components/LabeledRadioButton/LabeledRadioButton.module.scss b/airbyte-webapp/src/components/LabeledRadioButton/LabeledRadioButton.module.scss index 18276c775fb..d53e2dfc323 100644 --- a/airbyte-webapp/src/components/LabeledRadioButton/LabeledRadioButton.module.scss +++ b/airbyte-webapp/src/components/LabeledRadioButton/LabeledRadioButton.module.scss @@ -2,28 +2,27 @@ @use "scss/variables"; .container { - margin-bottom: 6px; -} + margin-bottom: variables.$spacing-sm; -.label { - padding-left: variables.$spacing-md; - font-size: variables.$font-size-lg; - color: colors.$dark-blue; - cursor: pointer; -} + .label { + font-size: variables.$font-size-lg; + color: colors.$dark-blue; + cursor: pointer; -.disabled { - color: colors.$grey-300; - cursor: auto; -} + &--disabled { + color: colors.$grey; + cursor: auto; + } + } -.message { - padding-left: variables.$spacing-sm; - color: colors.$grey-300; - font-size: variables.$font-size-md; + .message { + padding-left: variables.$spacing-sm; + color: colors.$grey; + font-size: variables.$font-size-md; - & a { - text-decoration: underline; - color: colors.$blue; + & a { + text-decoration: underline; + color: colors.$blue; + } } } diff --git a/airbyte-webapp/src/components/LabeledRadioButton/LabeledRadioButton.tsx b/airbyte-webapp/src/components/LabeledRadioButton/LabeledRadioButton.tsx index 605b4e5e680..f7f4fc35701 100644 --- a/airbyte-webapp/src/components/LabeledRadioButton/LabeledRadioButton.tsx +++ b/airbyte-webapp/src/components/LabeledRadioButton/LabeledRadioButton.tsx @@ -5,23 +5,23 @@ import { FlexContainer } from "components/ui/Flex"; import { RadioButton } from "components/ui/RadioButton"; import styles from "./LabeledRadioButton.module.scss"; -type IProps = { + +export interface LabeledRadioButtonProps extends React.InputHTMLAttributes { message?: React.ReactNode; label?: React.ReactNode; - className?: string; -} & React.InputHTMLAttributes; +} -const LabeledRadioButton: React.FC = (props) => ( - +export const LabeledRadioButton = React.forwardRef((props, ref) => ( + -); +)); -export default LabeledRadioButton; +LabeledRadioButton.displayName = "LabeledRadioButton"; diff --git a/airbyte-webapp/src/components/LabeledRadioButton/index.tsx b/airbyte-webapp/src/components/LabeledRadioButton/index.tsx index 85f792c69ca..e7619250d92 100644 --- a/airbyte-webapp/src/components/LabeledRadioButton/index.tsx +++ b/airbyte-webapp/src/components/LabeledRadioButton/index.tsx @@ -1,4 +1,2 @@ -import LabeledRadioButton from "./LabeledRadioButton"; - -export default LabeledRadioButton; -export { LabeledRadioButton }; +export { LabeledRadioButton } from "./LabeledRadioButton"; +export type { LabeledRadioButtonProps } from "./LabeledRadioButton"; diff --git a/airbyte-webapp/src/components/connection/ConnectionForm/LabeledRadioButtonFormControl.tsx b/airbyte-webapp/src/components/connection/ConnectionForm/LabeledRadioButtonFormControl.tsx new file mode 100644 index 00000000000..a240040cee0 --- /dev/null +++ b/airbyte-webapp/src/components/connection/ConnectionForm/LabeledRadioButtonFormControl.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { Controller, useFormContext } from "react-hook-form"; + +import { LabeledRadioButton, LabeledRadioButtonProps } from "components"; + +interface LabeledRadioButtonFormControlProps extends LabeledRadioButtonProps { + controlId: string; + name: string; // redeclare name to make it required +} + +export const LabeledRadioButtonFormControl: React.FC = ({ + controlId, + name, + label, + value, + message, + ...restProps +}) => { + const { control } = useFormContext(); + + return ( + ( + + )} + /> + ); +}; diff --git a/airbyte-webapp/src/components/connection/ConnectionForm/NormalizationHookFormField.tsx b/airbyte-webapp/src/components/connection/ConnectionForm/NormalizationHookFormField.tsx new file mode 100644 index 00000000000..4738efb4e5c --- /dev/null +++ b/airbyte-webapp/src/components/connection/ConnectionForm/NormalizationHookFormField.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { Box } from "components/ui/Box"; +import { ExternalLink } from "components/ui/Link"; + +import { NormalizationType } from "core/domain/connection"; +import { links } from "core/utils/links"; +import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; + +import { LabeledRadioButtonFormControl } from "./LabeledRadioButtonFormControl"; + +/** + * react-hook-form field for normalization operation + * ready for migration to + * @see CreateConnectionForm + * old formik form field component: + * @see NormalizationField + */ +export const NormalizationHookFormField: React.FC = () => { + const { formatMessage } = useIntl(); + const { mode } = useConnectionFormService(); + + return ( + + + {lnk}, + }} + /> + ) + } + /> + + ); +}; diff --git a/airbyte-webapp/src/components/connection/ConnectionForm/TransformationField.tsx b/airbyte-webapp/src/components/connection/ConnectionForm/TransformationField.tsx index 759b5475dba..83de45885c0 100644 --- a/airbyte-webapp/src/components/connection/ConnectionForm/TransformationField.tsx +++ b/airbyte-webapp/src/components/connection/ConnectionForm/TransformationField.tsx @@ -2,7 +2,7 @@ import { ArrayHelpers, FormikProps } from "formik"; import React, { useState } from "react"; import { FormattedMessage } from "react-intl"; -import ArrayOfObjectsEditor from "components/ArrayOfObjectsEditor"; +import { ArrayOfObjectsEditor } from "components/ArrayOfObjectsEditor"; import TransformationForm from "components/connection/TransformationForm"; import { OperationRead } from "core/request/AirbyteClient"; diff --git a/airbyte-webapp/src/components/connection/ConnectionForm/TransformationFieldHookForm.tsx b/airbyte-webapp/src/components/connection/ConnectionForm/TransformationFieldHookForm.tsx new file mode 100644 index 00000000000..60edfb038f7 --- /dev/null +++ b/airbyte-webapp/src/components/connection/ConnectionForm/TransformationFieldHookForm.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { useFieldArray } from "react-hook-form"; +import { FormattedMessage } from "react-intl"; + +import { ArrayOfObjectsHookFormEditor } from "components/ArrayOfObjectsEditor"; + +import { isDefined } from "core/utils/common"; +import { useModalService } from "hooks/services/Modal"; +import { CustomTransformationsFormValues } from "pages/connections/ConnectionTransformationPage/CustomTransformationsCard"; + +import { useDefaultTransformation } from "./formConfig"; +import { DbtOperationReadOrCreate, TransformationHookForm } from "../TransformationHookForm"; + +/** + * Custom transformations field for react-hook-form + * will replace TransformationField in the future + * @see TransformationField + * @constructor + */ +export const TransformationFieldHookForm: React.FC = () => { + const { fields, append, remove, update } = useFieldArray({ + name: "transformations", + }); + const { openModal, closeModal } = useModalService(); + const defaultTransformation = useDefaultTransformation(); + + const openEditModal = (transformationItemIndex?: number) => + openModal({ + size: "xl", + title: , + content: () => ( + { + isDefined(transformationItemIndex) + ? update(transformationItemIndex, transformation) + : append(transformation); + closeModal(); + }} + onCancel={closeModal} + /> + ), + }); + + return ( + } + addButtonText={} + renderItemName={(item) => item.name} + onAddItem={() => openEditModal()} + onStartEdit={openEditModal} + onRemove={remove} + /> + ); +}; diff --git a/airbyte-webapp/src/components/connection/CreateConnectionForm/__snapshots__/CreateConnectionForm.test.tsx.snap b/airbyte-webapp/src/components/connection/CreateConnectionForm/__snapshots__/CreateConnectionForm.test.tsx.snap index 60d49b1ead1..47727090e36 100644 --- a/airbyte-webapp/src/components/connection/CreateConnectionForm/__snapshots__/CreateConnectionForm.test.tsx.snap +++ b/airbyte-webapp/src/components/connection/CreateConnectionForm/__snapshots__/CreateConnectionForm.test.tsx.snap @@ -1338,18 +1338,27 @@ exports[`CreateConnectionForm should render 1`] = `
- No custom transformation - + No custom transformation +

+ +
diff --git a/airbyte-webapp/src/components/connection/TransformationHookForm/TransformationHookForm.tsx b/airbyte-webapp/src/components/connection/TransformationHookForm/TransformationHookForm.tsx new file mode 100644 index 00000000000..d010b9d1f39 --- /dev/null +++ b/airbyte-webapp/src/components/connection/TransformationHookForm/TransformationHookForm.tsx @@ -0,0 +1,104 @@ +import React from "react"; +import { useIntl } from "react-intl"; + +import { Form, FormControl } from "components/forms"; +import { ModalFormSubmissionButtons } from "components/forms/ModalFormSubmissionButtons"; +import { FlexContainer, FlexItem } from "components/ui/Flex"; +import { ModalBody, ModalFooter } from "components/ui/Modal"; + +import { useOperationsCheck } from "core/api"; +import { useFormChangeTrackerService, useUniqueFormId } from "hooks/services/FormChangeTracker"; + +import { dbtOperationReadOrCreateSchema } from "./schema"; +import { DbtOperationReadOrCreate } from "./types"; + +interface TransformationHookFormProps { + transformation: DbtOperationReadOrCreate; + onDone: (tr: DbtOperationReadOrCreate) => void; + onCancel: () => void; +} + +/** + * react-hook-form Form for create/update transformation + * old version of TransformationField + * @see TransformationForm + * @param transformation + * @param onDone + * @param onCancel + * @constructor + */ +export const TransformationHookForm: React.FC = ({ transformation, onDone, onCancel }) => { + const { formatMessage } = useIntl(); + const operationCheck = useOperationsCheck(); + const { clearFormChange } = useFormChangeTrackerService(); + const formId = useUniqueFormId(); + + const onSubmit = async (values: DbtOperationReadOrCreate) => { + await operationCheck(values); + clearFormChange(formId); + onDone(values); + }; + + const onFormCancel = () => { + clearFormChange(formId); + onCancel(); + }; + + return ( + + onSubmit={onSubmit} + schema={dbtOperationReadOrCreateSchema} + defaultValues={transformation} + // TODO: uncomment when trackDirtyChanges will be fixed + // trackDirtyChanges + > + + + + + + `<${node}>` } + )} + /> + + + + + + + + + + + + ); +}; diff --git a/airbyte-webapp/src/components/connection/TransformationHookForm/index.tsx b/airbyte-webapp/src/components/connection/TransformationHookForm/index.tsx new file mode 100644 index 00000000000..95f78547439 --- /dev/null +++ b/airbyte-webapp/src/components/connection/TransformationHookForm/index.tsx @@ -0,0 +1,3 @@ +export { TransformationHookForm } from "./TransformationHookForm"; +export { dbtOperationReadOrCreateSchema } from "./schema"; +export { type DbtOperationRead, type DbtOperationReadOrCreate } from "./types"; diff --git a/airbyte-webapp/src/components/connection/TransformationHookForm/schema.test.ts b/airbyte-webapp/src/components/connection/TransformationHookForm/schema.test.ts new file mode 100644 index 00000000000..7272471de60 --- /dev/null +++ b/airbyte-webapp/src/components/connection/TransformationHookForm/schema.test.ts @@ -0,0 +1,51 @@ +import merge from "lodash/merge"; +import { InferType, ValidationError } from "yup"; + +import { dbtOperationReadOrCreateSchema } from "./schema"; + +describe(" - validationSchema", () => { + const customTransformationFields: InferType = { + name: "test name", + workspaceId: "test workspace id", + operationId: undefined, + operatorConfiguration: { + operatorType: "dbt", + dbt: { + gitRepoUrl: "https://github.com/username/example.git", + dockerImage: undefined, + dbtArguments: undefined, + gitRepoBranch: "", + }, + }, + }; + + it("should successfully validate the schema", async () => { + await expect(dbtOperationReadOrCreateSchema.validate(customTransformationFields)).resolves.toBeTruthy(); + }); + + it("should fail if 'name' is empty", async () => { + await expect(async () => { + await dbtOperationReadOrCreateSchema.validateAt("name", { ...customTransformationFields, name: "" }); + }).rejects.toThrow(ValidationError); + }); + + it("should fail if 'gitRepoUrl' is invalid", async () => { + await expect(async () => { + await dbtOperationReadOrCreateSchema.validateAt( + "operatorConfiguration.dbt.gitRepoUrl", + merge(customTransformationFields, { + operatorConfiguration: { dbt: { gitRepoUrl: "" } }, + }) + ); + }).rejects.toThrow(ValidationError); + + await expect(async () => { + await dbtOperationReadOrCreateSchema.validateAt( + "operatorConfiguration.dbt.gitRepoUrl", + merge(customTransformationFields, { + operatorConfiguration: { dbt: { gitRepoUrl: "https://github.com/username/example.git/" } }, + }) + ); + }).rejects.toThrow(ValidationError); + }); +}); diff --git a/airbyte-webapp/src/components/connection/TransformationHookForm/schema.ts b/airbyte-webapp/src/components/connection/TransformationHookForm/schema.ts new file mode 100644 index 00000000000..c625f9a8375 --- /dev/null +++ b/airbyte-webapp/src/components/connection/TransformationHookForm/schema.ts @@ -0,0 +1,26 @@ +import { SchemaOf } from "yup"; +import * as yup from "yup"; + +import { DbtOperationReadOrCreate } from "./types"; + +export const dbtOperationReadOrCreateSchema: SchemaOf = yup.object().shape({ + workspaceId: yup.string().required("form.empty.error"), + operationId: yup.string().optional(), // during creation, this is not required + name: yup.string().required("form.empty.error"), + operatorConfiguration: yup + .object() + .shape({ + operatorType: yup.mixed().oneOf(["dbt"]).default("dbt"), + dbt: yup.object({ + gitRepoUrl: yup + .string() + .trim() + .matches(/((http(s)?)|(git@[\w.]+))(:(\/\/)?)([\w.@:/\-~]+)(\.git)$/, "form.repositoryUrl.invalidUrl") + .required("form.empty.error"), + gitRepoBranch: yup.string().optional(), + dockerImage: yup.string().optional(), + dbtArguments: yup.string().optional(), + }), + }) + .required(), +}); diff --git a/airbyte-webapp/src/components/connection/TransformationHookForm/types.ts b/airbyte-webapp/src/components/connection/TransformationHookForm/types.ts new file mode 100644 index 00000000000..d1691f346df --- /dev/null +++ b/airbyte-webapp/src/components/connection/TransformationHookForm/types.ts @@ -0,0 +1,15 @@ +import { OperationId, OperatorDbt } from "core/request/AirbyteClient"; + +export interface DbtOperationRead { + name: string; + workspaceId: string; + operationId: OperationId; + operatorConfiguration: { + operatorType: "dbt"; + dbt: OperatorDbt; + }; +} + +export interface DbtOperationReadOrCreate extends Omit { + operationId?: OperationId; +} diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 47915c3812a..c2778117ef6 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -559,7 +559,11 @@ "connection.replicationFrequency": "Replication frequency*", "connection.replicationFrequency.subtitle": "Set how often data should sync to the destination", "connection.normalization": "Normalization", + "connection.normalization.successMessage": "Normalization settings were updated successfully!", + "connection.normalization.errorMessage": "There was an error during updating your normalization settings", "connection.customTransformations": "Custom Transformations", + "connection.customTransformations.successMessage": "Custom transformation settings were updated successfully!", + "connection.customTransformations.errorMessage": "There was an error during updating your custom transformation settings", "tables.name": "Name", "tables.connector": "Connector", diff --git a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/CustomTransformationsCard.tsx b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/CustomTransformationsCard.tsx index 88a58210773..ecbb97b3880 100644 --- a/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/CustomTransformationsCard.tsx +++ b/airbyte-webapp/src/pages/connections/ConnectionTransformationPage/CustomTransformationsCard.tsx @@ -6,11 +6,17 @@ import { useToggle } from "react-use"; import { ConnectionEditFormCard } from "components/connection/ConnectionEditFormCard"; import { getInitialTransformations } from "components/connection/ConnectionForm/formConfig"; import { TransformationField } from "components/connection/ConnectionForm/TransformationField"; +import { DbtOperationReadOrCreate } from "components/connection/TransformationHookForm"; import { OperationCreate, OperationRead } from "core/request/AirbyteClient"; import { useConnectionFormService } from "hooks/services/ConnectionForm/ConnectionFormService"; import { FormikOnSubmit } from "types/formik"; +// will be used in 2nd part of migration, TransformationFieldHookForm refers to this interface +export interface CustomTransformationsFormValues { + transformations: DbtOperationReadOrCreate[]; +} + export const CustomTransformationsCard: React.FC<{ operations?: OperationCreate[]; onSubmit: FormikOnSubmit<{ transformations?: OperationRead[] }>;