diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 9d9abf6d..502cbdae 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -107,6 +107,8 @@ type SubstepArgument { source: SubstepArgumentSource additionalFields: SubstepArgumentAdditionalFields dependsOn: [String] + fields: [SubstepArgument] + value: JSONObject } type SubstepArgumentOption { diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 6cd0dae2..7b3df670 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -145,7 +145,18 @@ export interface IFieldText { dependsOn?: string[]; } -export type IField = IFieldDropdown | IFieldText; +export interface IFieldDynamic { + key: string; + label: string; + type: 'dynamic'; + required?: boolean; + readOnly?: boolean; + description?: string; + value?: Record[]; + fields: (IFieldDropdown | IFieldText)[]; +} + +export type IField = IFieldDropdown | IFieldText | IFieldDynamic; export interface IAuthenticationStepField { name: string; diff --git a/packages/web/src/components/DynamicField/index.tsx b/packages/web/src/components/DynamicField/index.tsx new file mode 100644 index 00000000..ab2daf32 --- /dev/null +++ b/packages/web/src/components/DynamicField/index.tsx @@ -0,0 +1,126 @@ +import * as React from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import { useFormContext, useWatch } from 'react-hook-form'; +import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import RemoveIcon from '@mui/icons-material/Remove'; +import AddIcon from '@mui/icons-material/Add'; + +import { IFieldDynamic } from '@automatisch/types'; +import InputCreator from 'components/InputCreator'; +import { EditorContext } from 'contexts/Editor'; + +interface DynamicFieldProps { + onChange?: (value: string) => void; + onBlur?: (value: string) => void; + defaultValue?: Record[]; + name: string; + label: string; + type?: string; + required?: boolean; + readOnly?: boolean; + description?: string; + docUrl?: string; + clickToCopy?: boolean; + disabled?: boolean; + fields: IFieldDynamic["fields"]; + shouldUnregister?: boolean; +} + +function DynamicField( + props: DynamicFieldProps +): React.ReactElement { + const { label, description, fields, name, defaultValue } = props; + const { control, setValue, getValues } = useFormContext(); + const fieldsValue = useWatch({ control, name }) as Record[]; + const editorContext = React.useContext(EditorContext); + + const createEmptyItem = React.useCallback(() => { + return fields.reduce((previousValue, field) => { + return { + ...previousValue, + [field.key]: '', + __id: uuidv4(), + } + }, {}); + }, [fields]); + + const addItem = React.useCallback(() => { + const values = getValues(name); + + if (!values) { + setValue(name, [createEmptyItem()]); + } else { + setValue(name, values.concat(createEmptyItem())); + } + }, [getValues, createEmptyItem]); + + const removeItem = React.useCallback((index) => { + if (fieldsValue.length === 1) return; + + const newFieldsValue = fieldsValue.filter((fieldValue, fieldIndex) => fieldIndex !== index); + + setValue(name, newFieldsValue); + }, [fieldsValue]); + + React.useEffect(function addInitialGroupWhenEmpty() { + const fieldValues = getValues(name); + + if (!fieldValues && defaultValue) { + setValue(name, defaultValue); + } else if (!fieldValues) { + setValue(name, [createEmptyItem()]); + } + }, [createEmptyItem, defaultValue]); + + return ( + + {label} + + {fieldsValue?.map((field, index) => ( + + + {fields.map((fieldSchema, fieldSchemaIndex) => ( + + + + ))} + + + removeItem(index)} + sx={{ width: 61, height: 61 }} + > + + + + ))} + + + + + + + + + + {description} + + ); +} + +export default DynamicField; diff --git a/packages/web/src/components/FlowStep/index.tsx b/packages/web/src/components/FlowStep/index.tsx index c38c4c5a..2d81c12f 100644 --- a/packages/web/src/components/FlowStep/index.tsx +++ b/packages/web/src/components/FlowStep/index.tsx @@ -63,14 +63,14 @@ function generateValidationSchema(substeps: ISubstep[]) { const substepArgumentValidations: Record = {}; for (const arg of args) { - const { key, required, dependsOn } = arg; + const { key, required } = arg; // base validation for the field if not exists if (!substepArgumentValidations[key]) { substepArgumentValidations[key] = yup.mixed(); } - if (typeof substepArgumentValidations[key] === 'object') { + if (typeof substepArgumentValidations[key] === 'object' && (arg.type === 'string' || arg.type === 'dropdown')) { // if the field is required, add the required validation if (required) { substepArgumentValidations[key] = substepArgumentValidations[ @@ -79,8 +79,8 @@ function generateValidationSchema(substeps: ISubstep[]) { } // if the field depends on another field, add the dependsOn required validation - if (Array.isArray(dependsOn) && dependsOn.length > 0) { - for (const dependsOnKey of dependsOn) { + if (Array.isArray(arg.dependsOn) && arg.dependsOn.length > 0) { + for (const dependsOnKey of arg.dependsOn) { const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`; // TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported. diff --git a/packages/web/src/components/FlowSubstep/FilterConditions/index.tsx b/packages/web/src/components/FlowSubstep/FilterConditions/index.tsx index b358f1cd..f87804a2 100644 --- a/packages/web/src/components/FlowSubstep/FilterConditions/index.tsx +++ b/packages/web/src/components/FlowSubstep/FilterConditions/index.tsx @@ -144,7 +144,7 @@ function FilterConditions(props: FilterConditionsProps): React.ReactElement { {group?.and?.map((groupItem: TGroupItem, groupItemIndex: number) => ( - + + ); + } + if (type === 'dropdown') { const preparedOptions = schema.options || optionGenerator(data); return ( {(additionalFieldsLoading && !additionalFields?.length) &&
@@ -92,6 +110,7 @@ export default function InputCreator( stepId={stepId} disabled={disabled} showOptionValue={true} + shouldUnregister={shouldUnregister} /> ))} @@ -99,15 +118,17 @@ export default function InputCreator( } if (type === 'string') { - if (variables) { + if (schema.variables) { return ( {(additionalFieldsLoading && !additionalFields?.length) &&
@@ -122,6 +143,7 @@ export default function InputCreator( stepId={stepId} disabled={disabled} showOptionValue={true} + shouldUnregister={shouldUnregister} /> ))} @@ -131,6 +153,7 @@ export default function InputCreator( return ( {(additionalFieldsLoading && !additionalFields?.length) &&
@@ -156,6 +180,7 @@ export default function InputCreator( stepId={stepId} disabled={disabled} showOptionValue={true} + shouldUnregister={shouldUnregister} /> ))} diff --git a/packages/web/src/components/PowerInput/index.tsx b/packages/web/src/components/PowerInput/index.tsx index 2bdfa6a4..4c2b3fbf 100644 --- a/packages/web/src/components/PowerInput/index.tsx +++ b/packages/web/src/components/PowerInput/index.tsx @@ -5,7 +5,7 @@ import Popper from '@mui/material/Popper'; import InputLabel from '@mui/material/InputLabel'; import FormHelperText from '@mui/material/FormHelperText'; import { Controller, useFormContext } from 'react-hook-form'; -import { Editor, Transforms, Range, createEditor } from 'slate'; +import { createEditor } from 'slate'; import { Slate, Editable, useSelected, useFocused } from 'slate-react'; import { @@ -17,7 +17,7 @@ import { import Suggestions from './Suggestions'; import { StepExecutionsContext } from 'contexts/StepExecutions'; -import { FakeInput, InputLabelWrapper } from './style'; +import { FakeInput, InputLabelWrapper, ChildrenWrapper } from './style'; import { VariableElement } from './types'; import { processStepWithExecutions } from './data'; @@ -34,6 +34,7 @@ type PowerInputProps = { docUrl?: string; clickToCopy?: boolean; disabled?: boolean; + shouldUnregister?: boolean; }; const PowerInput = (props: PowerInputProps) => { @@ -46,6 +47,7 @@ const PowerInput = (props: PowerInputProps) => { required, description, disabled, + shouldUnregister, } = props; const priorStepsWithExecutions = React.useContext(StepExecutionsContext); const editorRef = React.useRef(null); @@ -81,7 +83,7 @@ const PowerInput = (props: PowerInputProps) => { name={name} control={control} defaultValue={defaultValue} - shouldUnregister={true} + shouldUnregister={shouldUnregister ?? true} render={({ field: { value, @@ -103,7 +105,7 @@ const PowerInput = (props: PowerInputProps) => { }} > {/* ref-able single child for ClickAwayListener */} -
+ { data={stepsWithVariables} onSuggestionClick={handleVariableSuggestionClick} /> -
+ )} diff --git a/packages/web/src/components/PowerInput/style.ts b/packages/web/src/components/PowerInput/style.ts index 52373108..f918aece 100644 --- a/packages/web/src/components/PowerInput/style.ts +++ b/packages/web/src/components/PowerInput/style.ts @@ -1,5 +1,12 @@ import { styled } from '@mui/material/styles'; +export const ChildrenWrapper = styled('div')` + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; + hyphens: auto; +`; + export const InputLabelWrapper = styled('div')` position: absolute; left: ${({ theme }) => theme.spacing(1.75)}; @@ -9,7 +16,7 @@ export const InputLabelWrapper = styled('div')` export const FakeInput = styled('div', { shouldForwardProp: (prop) => prop !== 'disabled', -})<{ disabled?: boolean }>` +}) <{ disabled?: boolean }>` border: 1px solid #eee; min-height: 52px; width: 100%; diff --git a/packages/web/src/graphql/queries/get-apps.ts b/packages/web/src/graphql/queries/get-apps.ts index 821ba1c5..f358ae7d 100644 --- a/packages/web/src/graphql/queries/get-apps.ts +++ b/packages/web/src/graphql/queries/get-apps.ts @@ -110,6 +110,7 @@ export const GET_APPS = gql` description variables dependsOn + value options { label value @@ -130,6 +131,36 @@ export const GET_APPS = gql` value } } + fields { + label + key + type + required + description + variables + value + dependsOn + options { + label + value + } + source { + type + name + arguments { + name + value + } + } + additionalFields { + type + name + arguments { + name + value + } + } + } } } }