feat: support scopes for dynamic field entries

This commit is contained in:
Ali BARIN
2025-01-15 11:26:03 +00:00
parent 7130d8e934
commit 61624baa68
11 changed files with 167 additions and 52 deletions

View File

@@ -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}',

View File

@@ -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: [] };

View File

@@ -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}',
},
],
},
},
],
},

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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,
};

View 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,
};

View File

@@ -6,6 +6,7 @@ export const StepExecutionsContext = React.createContext([]);
export const StepExecutionsProvider = (props) => {
const { children, value } = props;
return (
<StepExecutionsContext.Provider value={value}>
{children}

View File

@@ -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 (

View 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;
}