From 3f0e117016e4fa8e56138321409aa7ebbefe83bd Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 3 Jan 2023 15:14:01 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=9F=F0=9F=8E=89=20Connector=20form:=20?= =?UTF-8?q?Use=20proper=20validation=20in=20array=20section=20(#20725)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improve some types * improve further * clean up a bit more * refactor loading state * move loading state up * remove isLoading references * remove unused props and make fetch connector error work * remove special component for name * remove top level state for unifinished flows * start removing uiwidget * Update airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.module.scss Co-authored-by: Tim Roes * remove undefined option for selected id * remove unused prop * fix types * remove uiwidget state * clean up * adjust comment * handle errors in a nice way * do not respect default on oneOf fields * rename to formblock * reduce re-renders * pass error to secure inputs * simplify and improve styling * align top * code review * remove comment * review comments * rename file * be strict about boolean values * add example * track form error in error boundary * review comments * handle unexpected cases better * enrich error with connector id * rename prop * use proper validation in array section * fix test * rename variable Co-authored-by: Tim Roes --- .../ConnectorForm/ConnectorForm.test.tsx | 2 +- .../components/Sections/ArraySection.tsx | 56 +++++++++++++------ .../Sections/VariableInputFieldForm.tsx | 13 +---- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.test.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.test.tsx index 27a9e27a60b2f..c39270c4b4932 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.test.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/ConnectorForm.test.tsx @@ -53,7 +53,7 @@ const useAddPriceListItem = (container: HTMLElement) => { const arrayOfObjectsEditModal = getByTestId(document.body, "arrayOfObjects-editModal"); const getPriceListInput = (index: number, key: string) => - arrayOfObjectsEditModal.querySelector(`input[name='__temp__connectionConfiguration_priceList${index}.${key}']`); + arrayOfObjectsEditModal.querySelector(`input[name='connectionConfiguration.priceList\\[${index}\\].${key}']`); // Type items into input const nameInput = getPriceListInput(index, "name"); diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/ArraySection.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/ArraySection.tsx index 655adbbce8836..b3e27ad3c8f58 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/ArraySection.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/ArraySection.tsx @@ -42,15 +42,27 @@ const getItemDescription = (item: Record, properties: FormBlock[ }; export const ArraySection: React.FC = ({ formField, path, disabled }) => { - const [field, , fieldHelper] = useField(path); - const [editIndex, setEditIndex] = useState(); + const [field, , fieldHelper] = useField>>(path); + const [editIndex, setEditIndex] = useState(); + // keep the previous state of the currently edited item around so it can be restored on cancelling the form + const [originalItem, setOriginalItem] = useState | undefined>(); - const items = useMemo(() => field.value ?? [], [field.value]); + const items: Array> = useMemo(() => field.value ?? [], [field.value]); + + // keep the list of rendered items stable as long as editing is in progress + const itemsWithOverride = useMemo(() => { + if (typeof editIndex === "undefined") { + return items; + } + return items.map((item, index) => (index === editIndex ? originalItem : item)).filter(Boolean) as Array< + Record + >; + }, [editIndex, originalItem, items]); const { renderItemName, renderItemDescription } = useMemo(() => { const { properties } = formField.properties as FormGroupItem; - const details = items.map((item: Record) => { + const details = itemsWithOverride.map((item: Record) => { const name = getItemName(item, properties); const description = getItemDescription(item, properties); return { @@ -63,10 +75,23 @@ export const ArraySection: React.FC = ({ formField, path, dis renderItemName: (_: unknown, index: number) => details[index].name, renderItemDescription: (_: unknown, index: number) => details[index].description, }; - }, [items, formField.properties]); + }, [itemsWithOverride, formField.properties]); const clearEditIndex = () => setEditIndex(undefined); + // on cancelling editing, either remove the item if it has been a new one or put back the old value in the form + const onCancel = () => { + const newList = [...field.value]; + if (!originalItem) { + newList.pop(); + } else if (editIndex !== undefined && originalItem) { + newList.splice(editIndex, 1, originalItem); + } + + fieldHelper.setValue(newList); + clearEditIndex(); + }; + return ( = ({ formField, path, dis render={(arrayHelpers) => ( { + setEditIndex(n); + setOriginalItem(items[n]); + }} onRemove={arrayHelpers.remove} - onCancel={clearEditIndex} - items={items} + onCancel={onCancel} + items={itemsWithOverride} renderItemName={renderItemName} renderItemDescription={renderItemDescription} disabled={disabled} @@ -93,16 +121,8 @@ export const ArraySection: React.FC = ({ formField, path, dis path={`${path}[${editIndex ?? 0}]`} disabled={disabled} item={item} - onDone={(updatedItem) => { - const updatedValue = - editIndex !== undefined && editIndex < items.length - ? items.map((item: unknown, index: number) => (index === editIndex ? updatedItem : item)) - : [...items, updatedItem]; - - fieldHelper.setValue(updatedValue); - clearEditIndex(); - }} - onCancel={clearEditIndex} + onDone={clearEditIndex} + onCancel={onCancel} /> )} /> diff --git a/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/VariableInputFieldForm.tsx b/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/VariableInputFieldForm.tsx index a984d31d51acf..fa208c9d6aca8 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/VariableInputFieldForm.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorForm/components/Sections/VariableInputFieldForm.tsx @@ -1,5 +1,4 @@ import { useField } from "formik"; -import { useMemo } from "react"; import { FormattedMessage } from "react-intl"; import { useAsync, useEffectOnce } from "react-use"; import * as yup from "yup"; @@ -29,16 +28,10 @@ export const VariableInputFieldForm: React.FC = ({ onDone, onCancel, }) => { - // This form creates a temporary field for Formik to prevent the field from rendering in - // the service form while it's being created or edited since it reuses the FormSection component. - // The temp field is cleared when this form is done or canceled. - const variableInputFieldPath = useMemo(() => `__temp__${path.replace(/\./g, "_").replace(/\[|\]/g, "")}`, [path]); - const [field, , fieldHelper] = useField(variableInputFieldPath); + const [field, , fieldHelper] = useField(path); const { validationSchema } = useConnectorForm(); // Copy the validation from the original field to ensure that the form has all the required values field out correctly. - // One side effect of this is that validation errors will not be shown in this form because the validationSchema does not - // contain info about the temp field. const { value: isValid } = useAsync( async (): Promise => yup.reach(validationSchema, path).isValid(field.value), [field.value, path, validationSchema] @@ -63,7 +56,7 @@ export const VariableInputFieldForm: React.FC = ({ return ( <> - +