feat: support scopes for dynamic field entries
This commit is contained in:
@@ -85,8 +85,8 @@ export default defineAction({
|
||||
value: '{parameters.nodeName}',
|
||||
},
|
||||
{
|
||||
name: 'parameters.attributes',
|
||||
value: '{parameters.attributes}',
|
||||
name: 'parameters.attributeKey',
|
||||
value: '{fieldsEntry.key}',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -112,10 +112,6 @@ export default defineAction({
|
||||
name: 'key',
|
||||
value: 'listNodeFields',
|
||||
},
|
||||
{
|
||||
name: 'nodeIndex',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
name: 'parameters.hasChildrenNodes',
|
||||
value: '{parameters.hasChildrenNodes}',
|
||||
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
|
||||
async run($) {
|
||||
const nodeName = $.step.parameters.nodeName;
|
||||
const attributeName = $.step.parameters.attributeName;
|
||||
const attributeKey = $.step.parameters.attributeKey;
|
||||
|
||||
// Node: Conference
|
||||
const conferenceMutedAttributeValues = [
|
||||
@@ -92,17 +92,6 @@ export default {
|
||||
},
|
||||
];
|
||||
|
||||
const conferenceStayAloneAttributeValues = [
|
||||
{
|
||||
name: 'Yes',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'No',
|
||||
value: false,
|
||||
},
|
||||
];
|
||||
|
||||
const conferenceJitterBufferAttributeValues = [
|
||||
{
|
||||
name: 'Off',
|
||||
@@ -126,7 +115,6 @@ export default {
|
||||
waitMethod: conferenceWaitMethodAttributeValues,
|
||||
record: conferenceRecordAttributeValues,
|
||||
trim: conferenceTrimAttributeValues,
|
||||
stayAlone: conferenceStayAloneAttributeValues,
|
||||
jitterBuffer: conferenceJitterBufferAttributeValues,
|
||||
};
|
||||
|
||||
@@ -232,10 +220,10 @@ export default {
|
||||
};
|
||||
|
||||
const allNodeAttributeValues = {
|
||||
conference,
|
||||
say,
|
||||
sip,
|
||||
stream,
|
||||
Conference: conference,
|
||||
Say: say,
|
||||
Sip: sip,
|
||||
Stream: stream,
|
||||
};
|
||||
|
||||
if (!nodeName) return { data: [] };
|
||||
@@ -244,7 +232,7 @@ export default {
|
||||
|
||||
if (!selectedNodeAttributes) return { data: [] };
|
||||
|
||||
const selectedNodeAttributeValues = selectedNodeAttributes[attributeName];
|
||||
const selectedNodeAttributeValues = selectedNodeAttributes[attributeKey];
|
||||
|
||||
if (!selectedNodeAttributeValues) return { data: [] };
|
||||
|
||||
|
||||
@@ -65,16 +65,48 @@ export default {
|
||||
{
|
||||
label: 'Attribute name',
|
||||
key: 'key',
|
||||
type: 'string',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listVoiceXmlNodeAttributes',
|
||||
},
|
||||
{
|
||||
name: 'parameters.nodeName',
|
||||
value: '{outerFieldsEntry.nodeName}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Attribute value',
|
||||
key: 'value',
|
||||
type: 'string',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listVoiceXmlNodeAttributeValues',
|
||||
},
|
||||
{
|
||||
name: 'parameters.nodeName',
|
||||
value: '{outerFieldsEntry.nodeName}',
|
||||
},
|
||||
{
|
||||
name: 'parameters.attributeKey',
|
||||
value: '{fieldsEntry.key}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
import InputCreator from 'components/InputCreator';
|
||||
import { EditorContext } from 'contexts/Editor';
|
||||
import { FieldsPropType } from 'propTypes/propTypes';
|
||||
import { FieldEntryProvider } from 'contexts/FieldEntry';
|
||||
import useFieldEntryContext from 'hooks/useFieldEntryContext';
|
||||
|
||||
function DynamicFieldEntry(props) {
|
||||
const { fields, stepId, namePrefix } = props;
|
||||
const editorContext = React.useContext(EditorContext);
|
||||
const fieldEntryContext = useFieldEntryContext();
|
||||
|
||||
const newFieldEntryPaths = [
|
||||
...(fieldEntryContext?.fieldEntryPaths || []),
|
||||
namePrefix,
|
||||
];
|
||||
|
||||
return (
|
||||
<FieldEntryProvider value={{ fieldEntryPaths: newFieldEntryPaths }}>
|
||||
{fields.map((fieldSchema, fieldSchemaIndex) => (
|
||||
<Stack
|
||||
minWidth={0}
|
||||
flex="1 0 0px"
|
||||
spacing={2}
|
||||
key={`field-${namePrefix}-${fieldSchemaIndex}`}
|
||||
>
|
||||
<InputCreator
|
||||
schema={fieldSchema}
|
||||
namePrefix={namePrefix}
|
||||
disabled={editorContext.readOnly}
|
||||
shouldUnregister={false}
|
||||
stepId={stepId}
|
||||
/>
|
||||
</Stack>
|
||||
))}
|
||||
</FieldEntryProvider>
|
||||
);
|
||||
}
|
||||
|
||||
DynamicFieldEntry.propTypes = {
|
||||
stepId: PropTypes.string,
|
||||
namePrefix: PropTypes.string,
|
||||
index: PropTypes.number,
|
||||
fields: FieldsPropType.isRequired,
|
||||
};
|
||||
|
||||
export default DynamicFieldEntry;
|
||||
@@ -7,15 +7,17 @@ import Stack from '@mui/material/Stack';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import RemoveIcon from '@mui/icons-material/Remove';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import InputCreator from 'components/InputCreator';
|
||||
import { EditorContext } from 'contexts/Editor';
|
||||
|
||||
import { FieldsPropType } from 'propTypes/propTypes';
|
||||
import DynamicFieldEntry from './DynamicFieldEntry';
|
||||
import { FieldEntryProvider } from 'contexts/FieldEntry';
|
||||
import useFieldEntryContext from 'hooks/useFieldEntryContext';
|
||||
|
||||
function DynamicField(props) {
|
||||
const { label, description, fields, name, defaultValue, stepId } = props;
|
||||
const { control, setValue, getValues } = useFormContext();
|
||||
const fieldsValue = useWatch({ control, name });
|
||||
const editorContext = React.useContext(EditorContext);
|
||||
const fieldEntryContext = useFieldEntryContext();
|
||||
|
||||
const createEmptyItem = React.useCallback(() => {
|
||||
return fields.reduce((previousValue, field) => {
|
||||
@@ -60,7 +62,7 @@ function DynamicField(props) {
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FieldEntryProvider value={fieldEntryContext}>
|
||||
<Typography variant="subtitle2">{label}</Typography>
|
||||
{fieldsValue?.map?.((field, index) => (
|
||||
<Stack direction="row" spacing={2} key={`fieldGroup-${field.__id}`}>
|
||||
@@ -76,22 +78,11 @@ function DynamicField(props) {
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{fields.map((fieldSchema, fieldSchemaIndex) => (
|
||||
<Stack
|
||||
minWidth={0}
|
||||
flex="1 0 0px"
|
||||
spacing={2}
|
||||
key={`field-${field.__id}-${fieldSchemaIndex}`}
|
||||
>
|
||||
<InputCreator
|
||||
schema={fieldSchema}
|
||||
namePrefix={`${name}.${index}`}
|
||||
disabled={editorContext.readOnly}
|
||||
shouldUnregister={false}
|
||||
stepId={stepId}
|
||||
/>
|
||||
</Stack>
|
||||
))}
|
||||
<DynamicFieldEntry
|
||||
fields={fields}
|
||||
namePrefix={`${name}.${index}`}
|
||||
stepId={stepId}
|
||||
/>
|
||||
</Stack>
|
||||
<IconButton
|
||||
size="small"
|
||||
@@ -115,7 +106,7 @@ function DynamicField(props) {
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Typography variant="caption">{description}</Typography>
|
||||
</React.Fragment>
|
||||
</FieldEntryProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ function InputCreator(props) {
|
||||
showOptionValue,
|
||||
shouldUnregister,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
key: name,
|
||||
label,
|
||||
@@ -35,9 +36,11 @@ function InputCreator(props) {
|
||||
description,
|
||||
type,
|
||||
} = schema;
|
||||
|
||||
const { data, loading } = useDynamicData(stepId, schema);
|
||||
const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } =
|
||||
useDynamicFields(stepId, schema);
|
||||
|
||||
const additionalFields = additionalFieldsData?.data;
|
||||
|
||||
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
||||
|
||||
@@ -7,6 +7,7 @@ export const EditorContext = React.createContext({
|
||||
|
||||
export const EditorProvider = (props) => {
|
||||
const { children, value } = props;
|
||||
|
||||
return (
|
||||
<EditorContext.Provider value={value}>{children}</EditorContext.Provider>
|
||||
);
|
||||
@@ -14,5 +15,7 @@ export const EditorProvider = (props) => {
|
||||
|
||||
EditorProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
value: PropTypes.shape({ readOnly: PropTypes.bool.isRequired }).isRequired,
|
||||
value: PropTypes.shape({
|
||||
readOnly: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
19
packages/web/src/contexts/FieldEntry.jsx
Normal file
19
packages/web/src/contexts/FieldEntry.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const FieldEntryContext = React.createContext({});
|
||||
|
||||
export const FieldEntryProvider = (props) => {
|
||||
const { children, value } = props;
|
||||
|
||||
return (
|
||||
<FieldEntryContext.Provider value={value}>
|
||||
{children}
|
||||
</FieldEntryContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
FieldEntryProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
value: PropTypes.object,
|
||||
};
|
||||
@@ -6,6 +6,7 @@ export const StepExecutionsContext = React.createContext([]);
|
||||
|
||||
export const StepExecutionsProvider = (props) => {
|
||||
const { children, value } = props;
|
||||
|
||||
return (
|
||||
<StepExecutionsContext.Provider value={value}>
|
||||
{children}
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
import * as React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import set from 'lodash/set';
|
||||
import first from 'lodash/first';
|
||||
import last from 'lodash/last';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import api from 'helpers/api';
|
||||
import useFieldEntryContext from './useFieldEntryContext';
|
||||
|
||||
const variableRegExp = /({.*?})/;
|
||||
|
||||
function computeArguments(args, getValues) {
|
||||
function computeArguments(args, getValues, fieldEntryPaths) {
|
||||
const initialValue = {};
|
||||
|
||||
return args.reduce((result, { name, value }) => {
|
||||
const isVariable = variableRegExp.test(value);
|
||||
|
||||
if (isVariable) {
|
||||
const sanitizedFieldPath = value.replace(/{|}/g, '');
|
||||
const fieldsEntryPath = last(fieldEntryPaths);
|
||||
const outerFieldsEntryPath = first(fieldEntryPaths);
|
||||
|
||||
const sanitizedFieldPath = value
|
||||
.replace(/{|}/g, '')
|
||||
.replace('fieldsEntry.', `${fieldsEntryPath}.`)
|
||||
.replace('outerFieldsEntry.', `${outerFieldsEntryPath}.`);
|
||||
|
||||
const computedValue = getValues(sanitizedFieldPath);
|
||||
|
||||
if (computedValue === undefined)
|
||||
throw new Error(`The ${sanitizedFieldPath} field is required.`);
|
||||
|
||||
set(result, name, computedValue);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -52,7 +66,9 @@ function useDynamicData(stepId, schema) {
|
||||
});
|
||||
|
||||
const { getValues } = useFormContext();
|
||||
const { fieldEntryPaths } = useFieldEntryContext();
|
||||
const formValues = getValues();
|
||||
|
||||
/**
|
||||
* Return `null` when even a field is missing value.
|
||||
*
|
||||
@@ -62,23 +78,31 @@ function useDynamicData(stepId, schema) {
|
||||
const computedVariables = React.useMemo(() => {
|
||||
if (schema.type === 'dropdown' && schema.source) {
|
||||
try {
|
||||
const variables = computeArguments(schema.source.arguments, getValues);
|
||||
const variables = computeArguments(
|
||||
schema.source.arguments,
|
||||
getValues,
|
||||
fieldEntryPaths,
|
||||
);
|
||||
|
||||
// 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]);
|
||||
}, [schema, formValues, getValues, fieldEntryPaths]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
|
||||
8
packages/web/src/hooks/useFieldEntryContext.jsx
Normal file
8
packages/web/src/hooks/useFieldEntryContext.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { FieldEntryContext } from 'contexts/FieldEntry';
|
||||
|
||||
export default function useFieldEntryContext() {
|
||||
const fieldEntryContext = React.useContext(FieldEntryContext);
|
||||
|
||||
return fieldEntryContext;
|
||||
}
|
||||
Reference in New Issue
Block a user