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 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 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,
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -224,6 +224,7 @@ function InputCreator(props) {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment />;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user