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}', value: '{parameters.nodeName}',
}, },
{ {
name: 'parameters.attributes', name: 'parameters.attributeKey',
value: '{parameters.attributes}', value: '{fieldsEntry.key}',
}, },
], ],
}, },
@@ -112,10 +112,6 @@ export default defineAction({
name: 'key', name: 'key',
value: 'listNodeFields', value: 'listNodeFields',
}, },
{
name: 'nodeIndex',
value: 0,
},
{ {
name: 'parameters.hasChildrenNodes', name: 'parameters.hasChildrenNodes',
value: '{parameters.hasChildrenNodes}', value: '{parameters.hasChildrenNodes}',

View File

@@ -4,7 +4,7 @@ export default {
async run($) { async run($) {
const nodeName = $.step.parameters.nodeName; const nodeName = $.step.parameters.nodeName;
const attributeName = $.step.parameters.attributeName; const attributeKey = $.step.parameters.attributeKey;
// Node: Conference // Node: Conference
const conferenceMutedAttributeValues = [ const conferenceMutedAttributeValues = [
@@ -92,17 +92,6 @@ export default {
}, },
]; ];
const conferenceStayAloneAttributeValues = [
{
name: 'Yes',
value: true,
},
{
name: 'No',
value: false,
},
];
const conferenceJitterBufferAttributeValues = [ const conferenceJitterBufferAttributeValues = [
{ {
name: 'Off', name: 'Off',
@@ -126,7 +115,6 @@ export default {
waitMethod: conferenceWaitMethodAttributeValues, waitMethod: conferenceWaitMethodAttributeValues,
record: conferenceRecordAttributeValues, record: conferenceRecordAttributeValues,
trim: conferenceTrimAttributeValues, trim: conferenceTrimAttributeValues,
stayAlone: conferenceStayAloneAttributeValues,
jitterBuffer: conferenceJitterBufferAttributeValues, jitterBuffer: conferenceJitterBufferAttributeValues,
}; };
@@ -232,10 +220,10 @@ export default {
}; };
const allNodeAttributeValues = { const allNodeAttributeValues = {
conference, Conference: conference,
say, Say: say,
sip, Sip: sip,
stream, Stream: stream,
}; };
if (!nodeName) return { data: [] }; if (!nodeName) return { data: [] };
@@ -244,7 +232,7 @@ export default {
if (!selectedNodeAttributes) return { data: [] }; if (!selectedNodeAttributes) return { data: [] };
const selectedNodeAttributeValues = selectedNodeAttributes[attributeName]; const selectedNodeAttributeValues = selectedNodeAttributes[attributeKey];
if (!selectedNodeAttributeValues) return { data: [] }; if (!selectedNodeAttributeValues) return { data: [] };

View File

@@ -65,16 +65,48 @@ export default {
{ {
label: 'Attribute name', label: 'Attribute name',
key: 'key', key: 'key',
type: 'string', type: 'dropdown',
required: false, required: false,
variables: true, variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listVoiceXmlNodeAttributes',
},
{
name: 'parameters.nodeName',
value: '{outerFieldsEntry.nodeName}',
},
],
},
}, },
{ {
label: 'Attribute value', label: 'Attribute value',
key: 'value', key: 'value',
type: 'string', type: 'dropdown',
required: false, required: false,
variables: true, 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 IconButton from '@mui/material/IconButton';
import RemoveIcon from '@mui/icons-material/Remove'; import RemoveIcon from '@mui/icons-material/Remove';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import InputCreator from 'components/InputCreator';
import { EditorContext } from 'contexts/Editor';
import { FieldsPropType } from 'propTypes/propTypes'; import { FieldsPropType } from 'propTypes/propTypes';
import DynamicFieldEntry from './DynamicFieldEntry';
import { FieldEntryProvider } from 'contexts/FieldEntry';
import useFieldEntryContext from 'hooks/useFieldEntryContext';
function DynamicField(props) { function DynamicField(props) {
const { label, description, fields, name, defaultValue, stepId } = props; const { label, description, fields, name, defaultValue, stepId } = props;
const { control, setValue, getValues } = useFormContext(); const { control, setValue, getValues } = useFormContext();
const fieldsValue = useWatch({ control, name }); const fieldsValue = useWatch({ control, name });
const editorContext = React.useContext(EditorContext); const fieldEntryContext = useFieldEntryContext();
const createEmptyItem = React.useCallback(() => { const createEmptyItem = React.useCallback(() => {
return fields.reduce((previousValue, field) => { return fields.reduce((previousValue, field) => {
@@ -60,7 +62,7 @@ function DynamicField(props) {
); );
return ( return (
<React.Fragment> <FieldEntryProvider value={fieldEntryContext}>
<Typography variant="subtitle2">{label}</Typography> <Typography variant="subtitle2">{label}</Typography>
{fieldsValue?.map?.((field, index) => ( {fieldsValue?.map?.((field, index) => (
<Stack direction="row" spacing={2} key={`fieldGroup-${field.__id}`}> <Stack direction="row" spacing={2} key={`fieldGroup-${field.__id}`}>
@@ -76,22 +78,11 @@ function DynamicField(props) {
minWidth: 0, minWidth: 0,
}} }}
> >
{fields.map((fieldSchema, fieldSchemaIndex) => ( <DynamicFieldEntry
<Stack fields={fields}
minWidth={0} namePrefix={`${name}.${index}`}
flex="1 0 0px" stepId={stepId}
spacing={2} />
key={`field-${field.__id}-${fieldSchemaIndex}`}
>
<InputCreator
schema={fieldSchema}
namePrefix={`${name}.${index}`}
disabled={editorContext.readOnly}
shouldUnregister={false}
stepId={stepId}
/>
</Stack>
))}
</Stack> </Stack>
<IconButton <IconButton
size="small" size="small"
@@ -115,7 +106,7 @@ function DynamicField(props) {
</IconButton> </IconButton>
</Stack> </Stack>
<Typography variant="caption">{description}</Typography> <Typography variant="caption">{description}</Typography>
</React.Fragment> </FieldEntryProvider>
); );
} }

View File

@@ -26,6 +26,7 @@ function InputCreator(props) {
showOptionValue, showOptionValue,
shouldUnregister, shouldUnregister,
} = props; } = props;
const { const {
key: name, key: name,
label, label,
@@ -35,9 +36,11 @@ function InputCreator(props) {
description, description,
type, type,
} = schema; } = schema;
const { data, loading } = useDynamicData(stepId, schema); const { data, loading } = useDynamicData(stepId, schema);
const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } = const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } =
useDynamicFields(stepId, schema); useDynamicFields(stepId, schema);
const additionalFields = additionalFieldsData?.data; const additionalFields = additionalFieldsData?.data;
const computedName = namePrefix ? `${namePrefix}.${name}` : name; const computedName = namePrefix ? `${namePrefix}.${name}` : name;

View File

@@ -7,6 +7,7 @@ export const EditorContext = React.createContext({
export const EditorProvider = (props) => { export const EditorProvider = (props) => {
const { children, value } = props; const { children, value } = props;
return ( return (
<EditorContext.Provider value={value}>{children}</EditorContext.Provider> <EditorContext.Provider value={value}>{children}</EditorContext.Provider>
); );
@@ -14,5 +15,7 @@ export const EditorProvider = (props) => {
EditorProvider.propTypes = { EditorProvider.propTypes = {
children: PropTypes.node.isRequired, 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) => { export const StepExecutionsProvider = (props) => {
const { children, value } = props; const { children, value } = props;
return ( return (
<StepExecutionsContext.Provider value={value}> <StepExecutionsContext.Provider value={value}>
{children} {children}

View File

@@ -1,24 +1,38 @@
import * as React from 'react'; import * as React from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import set from 'lodash/set'; import set from 'lodash/set';
import first from 'lodash/first';
import last from 'lodash/last';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import api from 'helpers/api'; import api from 'helpers/api';
import useFieldEntryContext from './useFieldEntryContext';
const variableRegExp = /({.*?})/; const variableRegExp = /({.*?})/;
function computeArguments(args, getValues) { function computeArguments(args, getValues, fieldEntryPaths) {
const initialValue = {}; const initialValue = {};
return args.reduce((result, { name, value }) => { return args.reduce((result, { name, value }) => {
const isVariable = variableRegExp.test(value); const isVariable = variableRegExp.test(value);
if (isVariable) { 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); const computedValue = getValues(sanitizedFieldPath);
if (computedValue === undefined) if (computedValue === undefined)
throw new Error(`The ${sanitizedFieldPath} field is required.`); throw new Error(`The ${sanitizedFieldPath} field is required.`);
set(result, name, computedValue); set(result, name, computedValue);
return result; return result;
} }
@@ -52,7 +66,9 @@ function useDynamicData(stepId, schema) {
}); });
const { getValues } = useFormContext(); const { getValues } = useFormContext();
const { fieldEntryPaths } = useFieldEntryContext();
const formValues = getValues(); const formValues = getValues();
/** /**
* Return `null` when even a field is missing value. * Return `null` when even a field is missing value.
* *
@@ -62,23 +78,31 @@ function useDynamicData(stepId, schema) {
const computedVariables = React.useMemo(() => { const computedVariables = React.useMemo(() => {
if (schema.type === 'dropdown' && schema.source) { if (schema.type === 'dropdown' && schema.source) {
try { 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 computed variables are the same, return the last computed variables.
if (isEqual(variables, lastComputedVariables.current)) { if (isEqual(variables, lastComputedVariables.current)) {
return lastComputedVariables.current; return lastComputedVariables.current;
} }
lastComputedVariables.current = variables; lastComputedVariables.current = variables;
return variables; return variables;
} catch (err) { } catch (err) {
return null; return null;
} }
} }
return null; return null;
/** /**
* `formValues` is to trigger recomputation when form is updated. * `formValues` is to trigger recomputation when form is updated.
* `getValues` is for convenience as it supports paths for fields like `getValues('foo.bar.baz')`. * `getValues` is for convenience as it supports paths for fields like `getValues('foo.bar.baz')`.
*/ */
}, [schema, formValues, getValues]); }, [schema, formValues, getValues, fieldEntryPaths]);
React.useEffect(() => { React.useEffect(() => {
if ( 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;
}