feat: add new commit trigger in GitHub
This commit is contained in:
@@ -88,6 +88,7 @@ function ChooseAppAndEventSubstep(props: ChooseAppAndEventSubstepProps): React.R
|
||||
...step,
|
||||
key: '',
|
||||
appKey,
|
||||
parameters: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ type Option = {
|
||||
value: string;
|
||||
}
|
||||
|
||||
const getOption = (options: readonly Option[], value: string) => options.find(option => option.value === value);
|
||||
const getOption = (options: readonly Option[], value: string) => options.find(option => option.value === value) || null;
|
||||
|
||||
function ControlledAutocomplete(props: ControlledAutocompleteProps): React.ReactElement {
|
||||
const { control } = useFormContext();
|
||||
@@ -28,11 +28,10 @@ function ControlledAutocomplete(props: ControlledAutocompleteProps): React.React
|
||||
onBlur,
|
||||
onChange,
|
||||
description,
|
||||
options = [],
|
||||
...autocompleteProps
|
||||
} = props;
|
||||
|
||||
if (!autocompleteProps.options) return (<React.Fragment />);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
rules={{ required }}
|
||||
@@ -40,13 +39,14 @@ function ControlledAutocomplete(props: ControlledAutocompleteProps): React.React
|
||||
defaultValue={defaultValue || ''}
|
||||
control={control}
|
||||
shouldUnregister={shouldUnregister}
|
||||
render={({ field: { ref, onChange: controllerOnChange, onBlur: controllerOnBlur, ...field } }) => (
|
||||
render={({ field: { ref, onChange: controllerOnChange, onBlur: controllerOnBlur, ...field }, fieldState }) => (
|
||||
<div>
|
||||
{/* encapsulated with an element such as div to vertical spacing delegated from parent */}
|
||||
<Autocomplete
|
||||
{...autocompleteProps}
|
||||
{...field}
|
||||
value={getOption(autocompleteProps.options, field.value)}
|
||||
options={options}
|
||||
value={getOption(options, field.value)}
|
||||
onChange={(event, selectedOption, reason, details) => {
|
||||
const typedSelectedOption = selectedOption as Option;
|
||||
if (typedSelectedOption?.value) {
|
||||
@@ -63,8 +63,9 @@ function ControlledAutocomplete(props: ControlledAutocompleteProps): React.React
|
||||
|
||||
<FormHelperText
|
||||
variant="outlined"
|
||||
error={Boolean(fieldState.isTouched && fieldState.error)}
|
||||
>
|
||||
{description}
|
||||
{fieldState.isTouched ? fieldState.error?.message || description : description}
|
||||
</FormHelperText>
|
||||
</div>
|
||||
)}
|
||||
|
@@ -10,7 +10,10 @@ import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import type { IApp, IField, IStep } from '@automatisch/types';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as yup from 'yup';
|
||||
import type { BaseSchema } from 'yup';
|
||||
import type { IApp, IField, IStep, ISubstep } from '@automatisch/types';
|
||||
|
||||
import { StepExecutionsProvider } from 'contexts/StepExecutions';
|
||||
import TestSubstep from 'components/TestSubstep';
|
||||
@@ -43,10 +46,57 @@ type FlowStepProps = {
|
||||
const validIcon = <CheckCircleIcon color="success" />;
|
||||
const errorIcon = <ErrorIcon color="error" />;
|
||||
|
||||
function generateValidationSchema(substeps: ISubstep[]) {
|
||||
const fieldValidations = substeps?.reduce((allValidations, { arguments: args }) => {
|
||||
if (!args || !Array.isArray(args)) return allValidations;
|
||||
|
||||
const substepArgumentValidations: Record<string, BaseSchema> = {};
|
||||
|
||||
for (const arg of args) {
|
||||
const { key, required, dependsOn } = arg;
|
||||
|
||||
// base validation for the field if not exists
|
||||
if (!substepArgumentValidations[key]) {
|
||||
substepArgumentValidations[key] = yup.string();
|
||||
}
|
||||
|
||||
if (typeof substepArgumentValidations[key] === 'object') {
|
||||
// if the field is required, add the required validation
|
||||
if (required) {
|
||||
substepArgumentValidations[key] = substepArgumentValidations[key].required(`${key} is required.`);
|
||||
}
|
||||
|
||||
// if the field depends on another field, add the dependsOn required validation
|
||||
if (dependsOn?.length > 0) {
|
||||
for (const dependsOnKey of dependsOn) {
|
||||
// TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported.
|
||||
// So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed.
|
||||
substepArgumentValidations[key] = substepArgumentValidations[key].when(`${dependsOnKey.replace('parameters.', '')}`, {
|
||||
is: (value: string) => Boolean(value) === false,
|
||||
then: (schema) => schema.required(`We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...allValidations,
|
||||
...substepArgumentValidations,
|
||||
}
|
||||
}, {});
|
||||
|
||||
const validationSchema = yup.object({
|
||||
parameters: yup.object(fieldValidations),
|
||||
});
|
||||
|
||||
return yupResolver(validationSchema);
|
||||
};
|
||||
|
||||
export default function FlowStep(
|
||||
props: FlowStepProps
|
||||
): React.ReactElement | null {
|
||||
const { collapsed, index, onChange } = props;
|
||||
const { collapsed, onChange } = props;
|
||||
const contextButtonRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
const step: IStep = props.step;
|
||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
|
||||
@@ -103,6 +153,8 @@ export default function FlowStep(
|
||||
handleChange({ step: val as IStep });
|
||||
};
|
||||
|
||||
const stepValidationSchema = React.useMemo(() => generateValidationSchema(substeps), [substeps]);
|
||||
|
||||
if (!apps) return null;
|
||||
|
||||
const onContextMenuClose = (event: React.SyntheticEvent) => {
|
||||
@@ -171,7 +223,11 @@ export default function FlowStep(
|
||||
stepWithTestExecutionsData?.getStepWithTestExecutions as IStep[]
|
||||
}
|
||||
>
|
||||
<Form defaultValues={step} onSubmit={handleSubmit}>
|
||||
<Form
|
||||
defaultValues={step}
|
||||
onSubmit={handleSubmit}
|
||||
resolver={stepValidationSchema}
|
||||
>
|
||||
<ChooseAppAndEventSubstep
|
||||
expanded={currentSubstep === 0}
|
||||
substep={{ name: 'Choose app & event', arguments: [] }}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { FormProvider, useForm, FieldValues, SubmitHandler, UseFormReturn } from 'react-hook-form';
|
||||
import { FormProvider, useForm, useWatch, FieldValues, SubmitHandler, UseFormReturn } from 'react-hook-form';
|
||||
import type { UseFormProps } from 'react-hook-form';
|
||||
|
||||
type FormProps = {
|
||||
@@ -20,15 +20,26 @@ export default function Form(props: FormProps): React.ReactElement {
|
||||
defaultValues,
|
||||
resolver,
|
||||
render,
|
||||
mode,
|
||||
mode = 'all',
|
||||
...formProps
|
||||
} = props;
|
||||
|
||||
const methods: UseFormReturn = useForm({
|
||||
defaultValues,
|
||||
reValidateMode: 'onBlur',
|
||||
resolver,
|
||||
mode,
|
||||
});
|
||||
|
||||
const form = useWatch({ control: methods.control, });
|
||||
|
||||
/**
|
||||
* For fields having `dependsOn` fields, we need to re-validate the form.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
methods.trigger();
|
||||
}, [methods.trigger, form]);
|
||||
|
||||
React.useEffect(() => {
|
||||
methods.reset(defaultValues);
|
||||
}, [defaultValues]);
|
||||
|
@@ -3,6 +3,7 @@ import { useLazyQuery } from '@apollo/client';
|
||||
import MuiTextField from '@mui/material/TextField';
|
||||
import type { IField, IFieldDropdown, IJSONObject } from '@automatisch/types';
|
||||
|
||||
import useDynamicData from 'hooks/useDynamicData';
|
||||
import { GET_DATA } from 'graphql/queries/get-data';
|
||||
import PowerInput from 'components/PowerInput';
|
||||
import TextField from 'components/TextField';
|
||||
@@ -26,7 +27,6 @@ type Option = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
const computeArguments = (args: IFieldDropdown["source"]["arguments"]): IJSONObject => args.reduce((result, { name, value }) => ({ ...result, [name as string]: value as string }), {});
|
||||
const optionGenerator = (options: RawOption[]): Option[] => options?.map(({ name, value }) => ({ label: name as string, value: value as string }));
|
||||
const getOption = (options: Option[], value: string) => options?.find(option => option.value === value);
|
||||
|
||||
@@ -51,24 +51,11 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
|
||||
type,
|
||||
} = schema;
|
||||
|
||||
const [getData, { called, data }] = useLazyQuery(GET_DATA);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (schema.type === 'dropdown' && stepId && schema.source && !called) {
|
||||
getData({
|
||||
variables: {
|
||||
stepId,
|
||||
...computeArguments(schema.source.arguments),
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [getData, called, stepId, schema]);
|
||||
|
||||
|
||||
const { data, loading } = useDynamicData(stepId, schema);
|
||||
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
||||
|
||||
if (type === 'dropdown') {
|
||||
const options = optionGenerator(data?.getData);
|
||||
const options = optionGenerator(data);
|
||||
|
||||
return (
|
||||
<ControlledAutocomplete
|
||||
@@ -81,6 +68,7 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
|
||||
value={getOption(options, value)}
|
||||
onChange={console.log}
|
||||
description={description}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -63,6 +63,7 @@ export const GET_APPS = gql`
|
||||
required
|
||||
description
|
||||
variables
|
||||
dependsOn
|
||||
source {
|
||||
type
|
||||
name
|
||||
@@ -88,6 +89,7 @@ export const GET_APPS = gql`
|
||||
required
|
||||
description
|
||||
variables
|
||||
dependsOn
|
||||
source {
|
||||
type
|
||||
name
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_DATA = gql`
|
||||
query GetData($stepId: String!, $key: String!) {
|
||||
getData(stepId: $stepId, key: $key)
|
||||
query GetData($stepId: String!, $key: String!, $parameters: JSONObject) {
|
||||
getData(stepId: $stepId, key: $key, parameters: $parameters)
|
||||
}
|
||||
`;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import template from 'lodash.template';
|
||||
import template from 'lodash/template';
|
||||
import type { IAuthenticationStepField, IJSONObject } from '@automatisch/types';
|
||||
|
||||
const interpolate = /{([\s\S]+?)}/g;
|
||||
|
32
packages/web/src/helpers/computeVariables.ts
Normal file
32
packages/web/src/helpers/computeVariables.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import template from 'lodash/template';
|
||||
import type { IAuthenticationStepField, IJSONObject } from '@automatisch/types';
|
||||
|
||||
const interpolate = /{([\s\S]+?)}/g;
|
||||
|
||||
type Variables = {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type IVariable = Omit<IAuthenticationStepField, "properties"> & Partial<Pick<IAuthenticationStepField, "properties">>;
|
||||
|
||||
const computeAuthStepVariables = (variableSchema: IVariable[], aggregatedData: IJSONObject): IJSONObject => {
|
||||
const variables: Variables = {};
|
||||
|
||||
for (const variable of variableSchema) {
|
||||
if (variable.properties) {
|
||||
variables[variable.name] = computeAuthStepVariables(variable.properties, aggregatedData);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variable.value) {
|
||||
const computedVariable = template(variable.value, { interpolate })(aggregatedData);
|
||||
|
||||
variables[variable.name] = computedVariable;
|
||||
}
|
||||
}
|
||||
|
||||
return variables;
|
||||
};
|
||||
|
||||
export default computeAuthStepVariables;
|
101
packages/web/src/hooks/useDynamicData.ts
Normal file
101
packages/web/src/hooks/useDynamicData.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import * as React from 'react';
|
||||
import { useLazyQuery } from '@apollo/client';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import set from 'lodash/set';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import type { IField, IFieldDropdown, IJSONObject } from '@automatisch/types';
|
||||
|
||||
import { GET_DATA } from 'graphql/queries/get-data';
|
||||
|
||||
const variableRegExp = /({.*?})/g;
|
||||
|
||||
function computeArguments(args: IFieldDropdown["source"]["arguments"], getValues: UseFormReturn["getValues"]): IJSONObject {
|
||||
const initialValue = {};
|
||||
return args.reduce(
|
||||
(result, { name, value }) => {
|
||||
const isVariable = variableRegExp.test(value);
|
||||
|
||||
if (isVariable) {
|
||||
const sanitizedFieldPath = value.replace(/{|}/g, '');
|
||||
const computedValue = getValues(sanitizedFieldPath);
|
||||
|
||||
if (!computedValue) throw new Error(`The ${sanitizedFieldPath} field is required.`);
|
||||
|
||||
set(result, name, computedValue);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
set(result, name, value);
|
||||
|
||||
return result;
|
||||
},
|
||||
initialValue
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Fetch the dynamic data for the given step.
|
||||
* This hook must be within a react-hook-form context.
|
||||
*
|
||||
* @param stepId - the id of the step
|
||||
* @param schema - the field that needs the dynamic data
|
||||
*/
|
||||
function useDynamicData(stepId: string | undefined, schema: IField) {
|
||||
const lastComputedVariables = React.useRef({});
|
||||
const [getData, { called, data, loading }] = useLazyQuery(GET_DATA);
|
||||
const { getValues } = useFormContext();
|
||||
const formValues = getValues();
|
||||
|
||||
/**
|
||||
* Return `null` when even a field is missing value.
|
||||
*
|
||||
* This must return the same reference if no computed variable is changed.
|
||||
* Otherwise, it causes redundant network request!
|
||||
*/
|
||||
const computedVariables = React.useMemo(() => {
|
||||
if (schema.type === 'dropdown' && schema.source) {
|
||||
try {
|
||||
const variables = computeArguments(schema.source.arguments, getValues);
|
||||
|
||||
// if computed variables are the same, return the last computed variables.
|
||||
if (isEqual(variables, lastComputedVariables.current)) {
|
||||
return lastComputedVariables.current;
|
||||
}
|
||||
|
||||
lastComputedVariables.current = variables;
|
||||
|
||||
return variables;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
/**
|
||||
* `formValues` is to trigger recomputation when form is updated.
|
||||
* `getValues` is for convenience as it supports paths for fields like `getValues('foo.bar.baz')`.
|
||||
*/
|
||||
}, [schema, formValues, getValues]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (schema.type === 'dropdown' && stepId && schema.source && computedVariables) {
|
||||
getData({
|
||||
variables: {
|
||||
stepId,
|
||||
...computedVariables,
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [getData, stepId, schema, computedVariables]);
|
||||
|
||||
return {
|
||||
called,
|
||||
data: data?.getData,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
||||
export default useDynamicData;
|
@@ -5,7 +5,7 @@ import Grid from '@mui/material/Grid';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as yup from "yup";
|
||||
import * as yup from 'yup';
|
||||
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import Container from 'components/Container';
|
||||
|
Reference in New Issue
Block a user