diff --git a/packages/backend/src/apps/signalwire/actions/add-voice-xml-node/index.js b/packages/backend/src/apps/signalwire/actions/add-voice-xml-node/index.js index 0aa889a1..4d206c01 100644 --- a/packages/backend/src/apps/signalwire/actions/add-voice-xml-node/index.js +++ b/packages/backend/src/apps/signalwire/actions/add-voice-xml-node/index.js @@ -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}', diff --git a/packages/backend/src/apps/signalwire/dynamic-data/list-voice-xml-node-attribute-values/index.js b/packages/backend/src/apps/signalwire/dynamic-data/list-voice-xml-node-attribute-values/index.js index 27f775c0..ac9bcac0 100644 --- a/packages/backend/src/apps/signalwire/dynamic-data/list-voice-xml-node-attribute-values/index.js +++ b/packages/backend/src/apps/signalwire/dynamic-data/list-voice-xml-node-attribute-values/index.js @@ -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: [] }; diff --git a/packages/backend/src/apps/signalwire/dynamic-fields/list-node-fields/index.js b/packages/backend/src/apps/signalwire/dynamic-fields/list-node-fields/index.js index 692ffcb2..141cc7a6 100644 --- a/packages/backend/src/apps/signalwire/dynamic-fields/list-node-fields/index.js +++ b/packages/backend/src/apps/signalwire/dynamic-fields/list-node-fields/index.js @@ -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}', + }, + ], + }, }, ], }, diff --git a/packages/web/src/components/DynamicField/DynamicFieldEntry.jsx b/packages/web/src/components/DynamicField/DynamicFieldEntry.jsx new file mode 100644 index 00000000..9269cf12 --- /dev/null +++ b/packages/web/src/components/DynamicField/DynamicFieldEntry.jsx @@ -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 ( + + {fields.map((fieldSchema, fieldSchemaIndex) => ( + + + + ))} + + ); +} + +DynamicFieldEntry.propTypes = { + stepId: PropTypes.string, + namePrefix: PropTypes.string, + index: PropTypes.number, + fields: FieldsPropType.isRequired, +}; + +export default DynamicFieldEntry; diff --git a/packages/web/src/components/DynamicField/index.jsx b/packages/web/src/components/DynamicField/index.jsx index 19aaad49..6822c1b2 100644 --- a/packages/web/src/components/DynamicField/index.jsx +++ b/packages/web/src/components/DynamicField/index.jsx @@ -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 ( - + {label} {fieldsValue?.map?.((field, index) => ( @@ -76,22 +78,11 @@ function DynamicField(props) { minWidth: 0, }} > - {fields.map((fieldSchema, fieldSchemaIndex) => ( - - - - ))} + {description} - + ); } diff --git a/packages/web/src/components/InputCreator/index.jsx b/packages/web/src/components/InputCreator/index.jsx index 7177ed7b..aa04b09f 100644 --- a/packages/web/src/components/InputCreator/index.jsx +++ b/packages/web/src/components/InputCreator/index.jsx @@ -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; diff --git a/packages/web/src/contexts/Editor.jsx b/packages/web/src/contexts/Editor.jsx index bb554fb8..2835a83f 100644 --- a/packages/web/src/contexts/Editor.jsx +++ b/packages/web/src/contexts/Editor.jsx @@ -7,6 +7,7 @@ export const EditorContext = React.createContext({ export const EditorProvider = (props) => { const { children, value } = props; + return ( {children} ); @@ -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, }; diff --git a/packages/web/src/contexts/FieldEntry.jsx b/packages/web/src/contexts/FieldEntry.jsx new file mode 100644 index 00000000..9961b06d --- /dev/null +++ b/packages/web/src/contexts/FieldEntry.jsx @@ -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 ( + + {children} + + ); +}; + +FieldEntryProvider.propTypes = { + children: PropTypes.node.isRequired, + value: PropTypes.object, +}; diff --git a/packages/web/src/contexts/StepExecutions.jsx b/packages/web/src/contexts/StepExecutions.jsx index 2664c0ea..58ad62eb 100644 --- a/packages/web/src/contexts/StepExecutions.jsx +++ b/packages/web/src/contexts/StepExecutions.jsx @@ -6,6 +6,7 @@ export const StepExecutionsContext = React.createContext([]); export const StepExecutionsProvider = (props) => { const { children, value } = props; + return ( {children} diff --git a/packages/web/src/hooks/useDynamicData.js b/packages/web/src/hooks/useDynamicData.js index 79b02f35..c14b2a4c 100644 --- a/packages/web/src/hooks/useDynamicData.js +++ b/packages/web/src/hooks/useDynamicData.js @@ -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 ( diff --git a/packages/web/src/hooks/useFieldEntryContext.jsx b/packages/web/src/hooks/useFieldEntryContext.jsx new file mode 100644 index 00000000..612b3dfe --- /dev/null +++ b/packages/web/src/hooks/useFieldEntryContext.jsx @@ -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; +}