Skip to content

Commit

Permalink
🪟🎉 Connector form: Use proper validation in array section (#20725)
Browse files Browse the repository at this point in the history
* 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 <tim@airbyte.io>

* 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 <tim@airbyte.io>
  • Loading branch information
Joe Reuter and timroes authored Jan 3, 2023
1 parent 254714b commit 3f0e117
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,27 @@ const getItemDescription = (item: Record<string, string>, properties: FormBlock[
};

export const ArraySection: React.FC<ArraySectionProps> = ({ formField, path, disabled }) => {
const [field, , fieldHelper] = useField(path);
const [editIndex, setEditIndex] = useState<number>();
const [field, , fieldHelper] = useField<Array<Record<string, string>>>(path);
const [editIndex, setEditIndex] = useState<number | undefined>();
// keep the previous state of the currently edited item around so it can be restored on cancelling the form
const [originalItem, setOriginalItem] = useState<Record<string, string> | undefined>();

const items = useMemo(() => field.value ?? [], [field.value]);
const items: Array<Record<string, string>> = 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<string, string>
>;
}, [editIndex, originalItem, items]);

const { renderItemName, renderItemDescription } = useMemo(() => {
const { properties } = formField.properties as FormGroupItem;

const details = items.map((item: Record<string, string>) => {
const details = itemsWithOverride.map((item: Record<string, string>) => {
const name = getItemName(item, properties);
const description = getItemDescription(item, properties);
return {
Expand All @@ -63,10 +75,23 @@ export const ArraySection: React.FC<ArraySectionProps> = ({ 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 (
<SectionContainer>
<GroupControls
Expand All @@ -79,10 +104,13 @@ export const ArraySection: React.FC<ArraySectionProps> = ({ formField, path, dis
render={(arrayHelpers) => (
<ArrayOfObjectsEditor
editableItemIndex={editIndex}
onStartEdit={setEditIndex}
onStartEdit={(n) => {
setEditIndex(n);
setOriginalItem(items[n]);
}}
onRemove={arrayHelpers.remove}
onCancel={clearEditIndex}
items={items}
onCancel={onCancel}
items={itemsWithOverride}
renderItemName={renderItemName}
renderItemDescription={renderItemDescription}
disabled={disabled}
Expand All @@ -93,16 +121,8 @@ export const ArraySection: React.FC<ArraySectionProps> = ({ 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}
/>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -29,16 +28,10 @@ export const VariableInputFieldForm: React.FC<VariableInputFormProps> = ({
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<boolean> => yup.reach(validationSchema, path).isValid(field.value),
[field.value, path, validationSchema]
Expand All @@ -63,15 +56,14 @@ export const VariableInputFieldForm: React.FC<VariableInputFormProps> = ({
return (
<>
<ModalBody maxHeight={300}>
<FormSection blocks={formField.properties} path={variableInputFieldPath} disabled={disabled} skipAppend />
<FormSection blocks={formField.properties} path={path} disabled={disabled} skipAppend />
</ModalBody>
<ModalFooter>
<Button
data-testid="cancel-button"
variant="secondary"
onClick={() => {
onCancel();
fieldHelper.setValue(undefined, false);
}}
>
<FormattedMessage id="form.cancel" />
Expand All @@ -81,7 +73,6 @@ export const VariableInputFieldForm: React.FC<VariableInputFormProps> = ({
disabled={disabled || !isValid}
onClick={() => {
onDone(field.value);
fieldHelper.setValue(undefined, false);
}}
>
<FormattedMessage id="form.done" />
Expand Down

0 comments on commit 3f0e117

Please sign in to comment.