feat: add dropdown field with dynamic data in flow
This commit is contained in:

committed by
Ömer Faruk Aydın

parent
5e1fc510ff
commit
932145e3a1
@@ -122,10 +122,6 @@
|
|||||||
"type": "query",
|
"type": "query",
|
||||||
"name": "getData",
|
"name": "getData",
|
||||||
"arguments": [
|
"arguments": [
|
||||||
{
|
|
||||||
"name": "stepId",
|
|
||||||
"value": "{step.id}"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "key",
|
"name": "key",
|
||||||
"value": "listChannels"
|
"value": "listChannels"
|
||||||
|
@@ -1,9 +1,5 @@
|
|||||||
import App from '../../models/app';
|
import App from '../../models/app';
|
||||||
import Connection from '../../models/connection';
|
|
||||||
import Step from '../../models/step';
|
|
||||||
import { IApp } from '@automatisch/types';
|
|
||||||
import Context from '../../types/express/context';
|
import Context from '../../types/express/context';
|
||||||
import ListData from '../../apps/slack/data/list-channels';
|
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
stepId: string;
|
stepId: string;
|
||||||
|
2
packages/types/index.d.ts
vendored
2
packages/types/index.d.ts
vendored
@@ -96,7 +96,7 @@ export interface IFieldDropdown {
|
|||||||
export interface IFieldText {
|
export interface IFieldText {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: string;
|
type: 'string';
|
||||||
required: boolean;
|
required: boolean;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
value: string;
|
value: string;
|
||||||
|
@@ -3,7 +3,6 @@ import type { ContainerProps } from '@mui/material/Container';
|
|||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||||
import MuiAppBar from '@mui/material/AppBar';
|
import MuiAppBar from '@mui/material/AppBar';
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
@@ -7,7 +7,7 @@ import ListItem from '@mui/material/ListItem';
|
|||||||
import Autocomplete from '@mui/material/Autocomplete';
|
import Autocomplete from '@mui/material/Autocomplete';
|
||||||
|
|
||||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||||
import type { IApp, IConnection, IStep, ISubstep, IJSONObject } from '@automatisch/types';
|
import type { IApp, IConnection, IStep, ISubstep } from '@automatisch/types';
|
||||||
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
|
||||||
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
||||||
|
|
||||||
|
63
packages/web/src/components/ControlledAutocomplete/index.tsx
Normal file
63
packages/web/src/components/ControlledAutocomplete/index.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import Autocomplete, { AutocompleteProps } from '@mui/material/Autocomplete';
|
||||||
|
|
||||||
|
interface ControlledAutocompleteProps extends AutocompleteProps<Option, boolean, boolean, boolean> {
|
||||||
|
shouldUnregister?: boolean;
|
||||||
|
name: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOption = (options: readonly Option[], value: string) => options.find(option => option.value === value);
|
||||||
|
|
||||||
|
function ControlledAutocomplete(props: ControlledAutocompleteProps): React.ReactElement {
|
||||||
|
const { control } = useFormContext();
|
||||||
|
|
||||||
|
const {
|
||||||
|
required = false,
|
||||||
|
name,
|
||||||
|
defaultValue,
|
||||||
|
shouldUnregister,
|
||||||
|
onBlur,
|
||||||
|
onChange,
|
||||||
|
...autocompleteProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (!autocompleteProps.options) return (<React.Fragment />);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
rules={{ required }}
|
||||||
|
name={name}
|
||||||
|
defaultValue={defaultValue || ''}
|
||||||
|
control={control}
|
||||||
|
shouldUnregister={shouldUnregister}
|
||||||
|
render={({ field: { ref, onChange: controllerOnChange, onBlur: controllerOnBlur, ...field } }) => (
|
||||||
|
<Autocomplete
|
||||||
|
{...autocompleteProps}
|
||||||
|
{...field}
|
||||||
|
value={getOption(autocompleteProps.options, field.value)}
|
||||||
|
onChange={(event, selectedOption, reason, details) => {
|
||||||
|
const typedSelectedOption = selectedOption as Option;
|
||||||
|
if (typedSelectedOption?.value) {
|
||||||
|
controllerOnChange(typedSelectedOption.value);
|
||||||
|
} else {
|
||||||
|
controllerOnChange(typedSelectedOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange?.(event, selectedOption, reason, details);
|
||||||
|
}}
|
||||||
|
onBlur={(...args) => { controllerOnBlur(); onBlur?.(...args); }}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ControlledAutocomplete;
|
@@ -3,6 +3,7 @@ import { useFormContext } from 'react-hook-form';
|
|||||||
import Collapse from '@mui/material/Collapse';
|
import Collapse from '@mui/material/Collapse';
|
||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
|
||||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||||
import InputCreator from 'components/InputCreator';
|
import InputCreator from 'components/InputCreator';
|
||||||
@@ -92,13 +93,19 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
|||||||
/>
|
/>
|
||||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||||
<ListItem sx={{ pt: 2, pb: 3, flexDirection: 'column', alignItems: 'flex-start' }}>
|
<ListItem sx={{ pt: 2, pb: 3, flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||||
|
<Stack
|
||||||
|
width='100%'
|
||||||
|
spacing={2}
|
||||||
|
>
|
||||||
{args?.map((argument) => (
|
{args?.map((argument) => (
|
||||||
<InputCreator
|
<InputCreator
|
||||||
key={argument.key}
|
key={argument.key}
|
||||||
schema={argument}
|
schema={argument}
|
||||||
namePrefix="parameters"
|
namePrefix="parameters"
|
||||||
|
stepId={step.id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
|
@@ -1,22 +1,42 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { IField } from '@automatisch/types';
|
import { useLazyQuery } from '@apollo/client';
|
||||||
|
import MuiTextField from '@mui/material/TextField';
|
||||||
|
import type { IField, IFieldDropdown, IJSONObject } from '@automatisch/types';
|
||||||
|
|
||||||
|
import { GET_DATA } from 'graphql/queries/get-data';
|
||||||
import PowerInput from 'components/PowerInput';
|
import PowerInput from 'components/PowerInput';
|
||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
|
import ControlledAutocomplete from 'components/ControlledAutocomplete';
|
||||||
|
|
||||||
type InputCreatorProps = {
|
type InputCreatorProps = {
|
||||||
onChange?: React.ChangeEventHandler;
|
onChange?: React.ChangeEventHandler;
|
||||||
onBlur?: React.FocusEventHandler;
|
onBlur?: React.FocusEventHandler;
|
||||||
schema: IField;
|
schema: IField;
|
||||||
namePrefix?: string;
|
namePrefix?: string;
|
||||||
|
stepId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RawOption = {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Option = {
|
||||||
|
label: string;
|
||||||
|
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);
|
||||||
|
|
||||||
export default function InputCreator(props: InputCreatorProps): React.ReactElement {
|
export default function InputCreator(props: InputCreatorProps): React.ReactElement {
|
||||||
const {
|
const {
|
||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
schema,
|
schema,
|
||||||
namePrefix,
|
namePrefix,
|
||||||
|
stepId,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -28,10 +48,44 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
|
|||||||
description,
|
description,
|
||||||
clickToCopy,
|
clickToCopy,
|
||||||
variables,
|
variables,
|
||||||
|
type,
|
||||||
} = schema;
|
} = 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 computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
||||||
|
|
||||||
|
if (type === 'dropdown') {
|
||||||
|
const options = optionGenerator(data?.getData);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ControlledAutocomplete
|
||||||
|
name={computedName}
|
||||||
|
fullWidth
|
||||||
|
disablePortal
|
||||||
|
disableClearable={true}
|
||||||
|
options={options}
|
||||||
|
renderInput={(params) => <MuiTextField {...params} label={label} />}
|
||||||
|
value={getOption(options, value)}
|
||||||
|
onChange={console.log}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (type === 'string') {
|
||||||
if (variables) {
|
if (variables) {
|
||||||
return (
|
return (
|
||||||
<PowerInput
|
<PowerInput
|
||||||
@@ -39,7 +93,6 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
|
|||||||
description={description}
|
description={description}
|
||||||
name={computedName}
|
name={computedName}
|
||||||
required={required}
|
required={required}
|
||||||
// onBlur={onBlur}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -61,4 +114,7 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
|
|||||||
clickToCopy={clickToCopy}
|
clickToCopy={clickToCopy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<React.Fragment />)
|
||||||
};
|
};
|
||||||
|
@@ -8,7 +8,6 @@ import LoadingButton from '@mui/lab/LoadingButton';
|
|||||||
|
|
||||||
import useAuthentication from 'hooks/useAuthentication';
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import { setItem } from 'helpers/storage';
|
|
||||||
import { LOGIN } from 'graphql/mutations/login';
|
import { LOGIN } from 'graphql/mutations/login';
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
|
@@ -78,6 +78,14 @@ export const GET_APPS = gql`
|
|||||||
required
|
required
|
||||||
description
|
description
|
||||||
variables
|
variables
|
||||||
|
source {
|
||||||
|
type
|
||||||
|
name
|
||||||
|
arguments {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
packages/web/src/graphql/queries/get-data.ts
Normal file
7
packages/web/src/graphql/queries/get-data.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const GET_DATA = gql`
|
||||||
|
query GetData($stepId: String!, $key: String!) {
|
||||||
|
getData(stepId: $stepId, key: $key)
|
||||||
|
}
|
||||||
|
`;
|
Reference in New Issue
Block a user