feat: support scopes for dynamic field entries
This commit is contained in:
@@ -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}',
|
||||||
|
|||||||
@@ -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: [] };
|
||||||
|
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
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) => {
|
export const StepExecutionsProvider = (props) => {
|
||||||
const { children, value } = props;
|
const { children, value } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StepExecutionsContext.Provider value={value}>
|
<StepExecutionsContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
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