feat(signalwire): add add-voice-xml-node and respond-with-voice-xml actions

This commit is contained in:
Ali BARIN
2024-12-13 19:26:22 +00:00
parent 0056940fa2
commit 661a5b24b2
9 changed files with 295 additions and 8 deletions

View File

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

View File

@@ -1,3 +1,5 @@
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];

View File

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

View File

@@ -0,0 +1,3 @@
import listNodeFields from './list-node-fields/index.js';
export default [listNodeFields];

View File

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

View File

@@ -4,6 +4,7 @@ import auth from './auth/index.js';
import triggers from './triggers/index.js';
import actions from './actions/index.js';
import dynamicData from './dynamic-data/index.js';
import dynamicFields from './dynamic-fields/index.js';
export default defineApp({
name: 'SignalWire',
@@ -19,4 +20,5 @@ export default defineApp({
triggers,
actions,
dynamicData,
dynamicFields,
});

View File

@@ -82,7 +82,11 @@ export default async (flowId, request, response) => {
break;
}
if (actionStep.key === 'respondWith' && !response.headersSent) {
if (
(actionStep.key === 'respondWith' ||
actionStep.key === 'respondWithVoiceXml') &&
!response.headersSent
) {
const { headers, statusCode, body } = actionExecutionStep.dataOut;
// we set the custom response headers

View File

@@ -17,6 +17,7 @@ function DynamicField(props) {
const { control, setValue, getValues } = useFormContext();
const fieldsValue = useWatch({ control, name });
const editorContext = React.useContext(EditorContext);
const createEmptyItem = React.useCallback(() => {
return fields.reduce((previousValue, field) => {
return {
@@ -26,6 +27,7 @@ function DynamicField(props) {
};
}, {});
}, [fields]);
const addItem = React.useCallback(() => {
const values = getValues(name);
if (!values) {
@@ -34,6 +36,7 @@ function DynamicField(props) {
setValue(name, values.concat(createEmptyItem()));
}
}, [getValues, createEmptyItem]);
const removeItem = React.useCallback(
(index) => {
if (fieldsValue.length === 1) return;
@@ -44,6 +47,7 @@ function DynamicField(props) {
},
[fieldsValue],
);
React.useEffect(
function addInitialGroupWhenEmpty() {
const fieldValues = getValues(name);
@@ -55,14 +59,17 @@ function DynamicField(props) {
},
[createEmptyItem, defaultValue],
);
return (
<React.Fragment>
<Typography variant="subtitle2">{label}</Typography>
{fieldsValue?.map((field, index) => (
{fieldsValue?.map?.((field, index) => (
<Stack direction="row" spacing={2} key={`fieldGroup-${field.__id}`}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
direction={{
xs: 'column',
sm: fields.length > 2 ? 'column' : 'row',
}}
spacing={{ xs: 2 }}
sx={{
display: 'flex',
@@ -75,6 +82,7 @@ function DynamicField(props) {
sx={{
display: 'flex',
flex: '1 0 0px',
flexDirection: 'column',
minWidth: 0,
}}
key={`field-${field.__id}-${fieldSchemaIndex}`}
@@ -89,7 +97,6 @@ function DynamicField(props) {
</Box>
))}
</Stack>
<IconButton
size="small"
edge="start"
@@ -100,7 +107,6 @@ function DynamicField(props) {
</IconButton>
</Stack>
))}
<Stack direction="row" spacing={2}>
<Stack spacing={{ xs: 2 }} sx={{ display: 'flex', flex: 1 }} />
@@ -113,7 +119,6 @@ function DynamicField(props) {
<AddIcon />
</IconButton>
</Stack>
<Typography variant="caption">{description}</Typography>
</React.Fragment>
);

View File

@@ -224,6 +224,7 @@ function InputCreator(props) {
</React.Fragment>
);
}
return <React.Fragment />;
}