feat(signalwire): add add-voice-xml-node and respond-with-voice-xml actions
This commit is contained in:
@@ -0,0 +1,130 @@
|
|||||||
|
import { XMLBuilder } from 'fast-xml-parser';
|
||||||
|
import defineAction from '../../../../helpers/define-action.js';
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Add voice XML node',
|
||||||
|
key: 'addVoiceXmlNode',
|
||||||
|
description: 'Add a voice XML node in the XML document',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Node name',
|
||||||
|
key: 'nodeName',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
description: 'The name of the node to be added.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Node value',
|
||||||
|
key: 'nodeValue',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
description: 'The value of the node to be added.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Attributes',
|
||||||
|
key: 'attributes',
|
||||||
|
type: 'dynamic',
|
||||||
|
required: false,
|
||||||
|
description: 'Add or remove attributes for the node as needed',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
key: '',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Attribute name',
|
||||||
|
key: 'key',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Attribute value',
|
||||||
|
key: 'value',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Add children node',
|
||||||
|
key: 'hasChildrenNodes',
|
||||||
|
type: 'dropdown',
|
||||||
|
required: true,
|
||||||
|
description: 'Add a nested node to the main node',
|
||||||
|
value: false,
|
||||||
|
options: [
|
||||||
|
{ label: 'Yes', value: true },
|
||||||
|
{ label: 'No', value: false },
|
||||||
|
],
|
||||||
|
additionalFields: {
|
||||||
|
type: 'query',
|
||||||
|
name: 'getDynamicFields',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
value: 'listNodeFields',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nodeIndex',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parameters.hasChildrenNodes',
|
||||||
|
value: '{parameters.hasChildrenNodes}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const nodeName = $.step.parameters.nodeName;
|
||||||
|
const nodeValue = $.step.parameters.nodeValue;
|
||||||
|
const attributes = $.step.parameters.attributes;
|
||||||
|
const childrenNodes = $.step.parameters.childrenNodes;
|
||||||
|
const hasChildrenNodes = $.step.parameters.hasChildrenNodes;
|
||||||
|
|
||||||
|
const builder = new XMLBuilder({
|
||||||
|
ignoreAttributes: false,
|
||||||
|
suppressEmptyNode: true,
|
||||||
|
preserveOrder: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const computeAttributes = (attributes) =>
|
||||||
|
attributes
|
||||||
|
.filter((attribute) => attribute.key || attribute.value)
|
||||||
|
.reduce(
|
||||||
|
(result, attribute) => ({
|
||||||
|
...result,
|
||||||
|
[`@_${attribute.key}`]: attribute.value,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const computeTextNode = (nodeValue) => ({
|
||||||
|
'#text': nodeValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedChildrenNodes = hasChildrenNodes
|
||||||
|
? childrenNodes.map((childNode) => ({
|
||||||
|
[childNode.nodeName]: [computeTextNode(childNode.nodeValue)],
|
||||||
|
':@': computeAttributes(childNode.attributes),
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const xmlObject = {
|
||||||
|
[nodeName]: [computeTextNode(nodeValue), ...computedChildrenNodes],
|
||||||
|
':@': computeAttributes(attributes),
|
||||||
|
};
|
||||||
|
|
||||||
|
const xmlString = builder.build([xmlObject]);
|
||||||
|
|
||||||
|
$.setActionItem({ raw: { stringNode: xmlString } });
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
import sendSms from './send-sms/index.js';
|
import sendSms from './send-sms/index.js';
|
||||||
|
import addVoiceXmlNode from './add-voice-xml-node/index.js';
|
||||||
|
import respondWithVoiceXml from './respond-with-voice-xml/index.js';
|
||||||
|
|
||||||
export default [sendSms];
|
export default [addVoiceXmlNode, respondWithVoiceXml, sendSms];
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
|
||||||
|
import defineAction from '../../../../helpers/define-action.js';
|
||||||
|
|
||||||
|
export default defineAction({
|
||||||
|
name: 'Respond with voice XML',
|
||||||
|
key: 'respondWithVoiceXml',
|
||||||
|
description: 'Respond with defined voice XML document',
|
||||||
|
arguments: [
|
||||||
|
{
|
||||||
|
label: 'Nodes',
|
||||||
|
key: 'nodes',
|
||||||
|
type: 'dynamic',
|
||||||
|
required: false,
|
||||||
|
description: 'Add or remove nodes for the XML document as needed',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
nodeString: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Node',
|
||||||
|
key: 'nodeString',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const builder = new XMLBuilder({
|
||||||
|
ignoreAttributes: false,
|
||||||
|
suppressEmptyNode: true,
|
||||||
|
preserveOrder: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parser = new XMLParser({
|
||||||
|
ignoreAttributes: false,
|
||||||
|
preserveOrder: true,
|
||||||
|
parseTagValue: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodes = $.step.parameters.nodes;
|
||||||
|
const computedNodes = nodes.map((node) => node.nodeString);
|
||||||
|
const parsedNodes = computedNodes.flatMap((computedNode) =>
|
||||||
|
parser.parse(computedNode)
|
||||||
|
);
|
||||||
|
|
||||||
|
const xmlString = builder.build([
|
||||||
|
{
|
||||||
|
Response: parsedNodes,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
$.setActionItem({
|
||||||
|
raw: {
|
||||||
|
body: xmlString,
|
||||||
|
statusCode: 200,
|
||||||
|
headers: { 'content-type': 'text/xml' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import listNodeFields from './list-node-fields/index.js';
|
||||||
|
|
||||||
|
export default [listNodeFields];
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
export default {
|
||||||
|
name: 'List node fields',
|
||||||
|
key: 'listNodeFields',
|
||||||
|
|
||||||
|
async run($) {
|
||||||
|
const hasChildrenNodes = $.step.parameters.hasChildrenNodes;
|
||||||
|
|
||||||
|
if (!hasChildrenNodes) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Children nodes',
|
||||||
|
key: 'childrenNodes',
|
||||||
|
type: 'dynamic',
|
||||||
|
required: false,
|
||||||
|
description: 'Add or remove nested node as needed',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
key: 'Content-Type',
|
||||||
|
value: 'application/json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Node name',
|
||||||
|
key: 'nodeName',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
description: 'The name of the node to be added.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Node value',
|
||||||
|
key: 'nodeValue',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
description: 'The value of the node to be added.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Attributes',
|
||||||
|
key: 'attributes',
|
||||||
|
type: 'dynamic',
|
||||||
|
required: false,
|
||||||
|
description: 'Add or remove attributes for the node as needed',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
key: '',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Attribute name',
|
||||||
|
key: 'key',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Attribute value',
|
||||||
|
key: 'value',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import auth from './auth/index.js';
|
|||||||
import triggers from './triggers/index.js';
|
import triggers from './triggers/index.js';
|
||||||
import actions from './actions/index.js';
|
import actions from './actions/index.js';
|
||||||
import dynamicData from './dynamic-data/index.js';
|
import dynamicData from './dynamic-data/index.js';
|
||||||
|
import dynamicFields from './dynamic-fields/index.js';
|
||||||
|
|
||||||
export default defineApp({
|
export default defineApp({
|
||||||
name: 'SignalWire',
|
name: 'SignalWire',
|
||||||
@@ -19,4 +20,5 @@ export default defineApp({
|
|||||||
triggers,
|
triggers,
|
||||||
actions,
|
actions,
|
||||||
dynamicData,
|
dynamicData,
|
||||||
|
dynamicFields,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -82,7 +82,11 @@ export default async (flowId, request, response) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionStep.key === 'respondWith' && !response.headersSent) {
|
if (
|
||||||
|
(actionStep.key === 'respondWith' ||
|
||||||
|
actionStep.key === 'respondWithVoiceXml') &&
|
||||||
|
!response.headersSent
|
||||||
|
) {
|
||||||
const { headers, statusCode, body } = actionExecutionStep.dataOut;
|
const { headers, statusCode, body } = actionExecutionStep.dataOut;
|
||||||
|
|
||||||
// we set the custom response headers
|
// we set the custom response headers
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ function DynamicField(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 editorContext = React.useContext(EditorContext);
|
||||||
|
|
||||||
const createEmptyItem = React.useCallback(() => {
|
const createEmptyItem = React.useCallback(() => {
|
||||||
return fields.reduce((previousValue, field) => {
|
return fields.reduce((previousValue, field) => {
|
||||||
return {
|
return {
|
||||||
@@ -26,6 +27,7 @@ function DynamicField(props) {
|
|||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
}, [fields]);
|
}, [fields]);
|
||||||
|
|
||||||
const addItem = React.useCallback(() => {
|
const addItem = React.useCallback(() => {
|
||||||
const values = getValues(name);
|
const values = getValues(name);
|
||||||
if (!values) {
|
if (!values) {
|
||||||
@@ -34,6 +36,7 @@ function DynamicField(props) {
|
|||||||
setValue(name, values.concat(createEmptyItem()));
|
setValue(name, values.concat(createEmptyItem()));
|
||||||
}
|
}
|
||||||
}, [getValues, createEmptyItem]);
|
}, [getValues, createEmptyItem]);
|
||||||
|
|
||||||
const removeItem = React.useCallback(
|
const removeItem = React.useCallback(
|
||||||
(index) => {
|
(index) => {
|
||||||
if (fieldsValue.length === 1) return;
|
if (fieldsValue.length === 1) return;
|
||||||
@@ -44,6 +47,7 @@ function DynamicField(props) {
|
|||||||
},
|
},
|
||||||
[fieldsValue],
|
[fieldsValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function addInitialGroupWhenEmpty() {
|
function addInitialGroupWhenEmpty() {
|
||||||
const fieldValues = getValues(name);
|
const fieldValues = getValues(name);
|
||||||
@@ -55,14 +59,17 @@ function DynamicField(props) {
|
|||||||
},
|
},
|
||||||
[createEmptyItem, defaultValue],
|
[createEmptyItem, defaultValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<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}`}>
|
||||||
<Stack
|
<Stack
|
||||||
direction={{ xs: 'column', sm: 'row' }}
|
direction={{
|
||||||
|
xs: 'column',
|
||||||
|
sm: fields.length > 2 ? 'column' : 'row',
|
||||||
|
}}
|
||||||
spacing={{ xs: 2 }}
|
spacing={{ xs: 2 }}
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -75,6 +82,7 @@ function DynamicField(props) {
|
|||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flex: '1 0 0px',
|
flex: '1 0 0px',
|
||||||
|
flexDirection: 'column',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
}}
|
}}
|
||||||
key={`field-${field.__id}-${fieldSchemaIndex}`}
|
key={`field-${field.__id}-${fieldSchemaIndex}`}
|
||||||
@@ -89,7 +97,6 @@ function DynamicField(props) {
|
|||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
edge="start"
|
edge="start"
|
||||||
@@ -100,7 +107,6 @@ function DynamicField(props) {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Stack direction="row" spacing={2}>
|
<Stack direction="row" spacing={2}>
|
||||||
<Stack spacing={{ xs: 2 }} sx={{ display: 'flex', flex: 1 }} />
|
<Stack spacing={{ xs: 2 }} sx={{ display: 'flex', flex: 1 }} />
|
||||||
|
|
||||||
@@ -113,7 +119,6 @@ function DynamicField(props) {
|
|||||||
<AddIcon />
|
<AddIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Typography variant="caption">{description}</Typography>
|
<Typography variant="caption">{description}</Typography>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ function InputCreator(props) {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <React.Fragment />;
|
return <React.Fragment />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user