Sfoglia il codice sorgente

feat: a form-trigger node describing a form and its answers

fszontagh 1 mese fa
parent
commit
a7376688c5
2 ha cambiato i file con 121 aggiunte e 0 eliminazioni
  1. 105 0
      nodes/triggers/form-trigger.js
  2. 16 0
      tests/nodes/form-trigger-shape.json

+ 105 - 0
nodes/triggers/form-trigger.js

@@ -0,0 +1,105 @@
+/**
+ * @node form-trigger
+ * @name Form
+ * @category triggers
+ * @version 1.0.0
+ * @description Publishes an HTML form at a shareable URL. Submitting it starts the workflow.
+ * @trigger
+ * @icon clipboard-list
+ */
+
+const configSchema = {
+  type: 'object',
+  properties: {
+    title: {
+      type: 'string',
+      title: 'Form Title',
+      description: 'Shown as the heading of the page',
+      default: 'Untitled form'
+    },
+    description: {
+      type: 'string',
+      title: 'Description',
+      description: 'Shown under the heading. Plain text - it is escaped, not rendered as HTML.',
+      default: ''
+    },
+    fields: {
+      type: 'array',
+      title: 'Fields',
+      description: 'The questions on the form, in order.',
+      default: [],
+      items: {
+        type: 'object',
+        properties: {
+          name: { type: 'string', title: 'Name',
+                  description: 'The key the workflow reads, e.g. "image" for $node[\'Form\'].form.image. Letters, digits and underscore, not starting with a digit.' },
+          label: { type: 'string', title: 'Label', description: 'Shown next to the input' },
+          type: { type: 'string', title: 'Type',
+                  enum: ['text', 'textarea', 'number', 'select', 'checkbox', 'date', 'file'],
+                  default: 'text' },
+          required: { type: 'boolean', title: 'Required', default: false },
+          placeholder: { type: 'string', title: 'Placeholder', default: '' },
+          options: { type: 'array', title: 'Options', description: 'Choices, for a Select field',
+                     items: { type: 'string' }, default: [] },
+          accept: { type: 'string', title: 'Accept',
+                    description: 'For a File field, e.g. "image/*". A browser hint only - it is not enforced.',
+                    default: '' },
+          maxSizeMb: { type: 'number', title: 'Max Size (MB)',
+                       description: 'For a File field. 0 uses the server limit.', default: 0 }
+        }
+      }
+    },
+    password: {
+      type: 'string',
+      title: 'Password',
+      format: 'password',
+      description: 'Leave empty for anyone with the link. This is stored readable in the workflow, so treat it as a gate against a forwarded link, not as a secret.',
+      default: ''
+    },
+    responseMode: {
+      type: 'string',
+      title: 'After Submit',
+      enum: ['immediate', 'wait'],
+      description: 'Immediate returns a thank-you page at once and lets the run continue - needed for anything taking more than about 30 seconds. Wait holds the browser until the workflow finishes, so a Respond to Webhook node can answer.',
+      default: 'immediate'
+    },
+    responseMessage: {
+      type: 'string',
+      title: 'Thank-you Message',
+      default: 'Thanks, your answer was received.',
+      'x-if': 'responseMode'
+    },
+    path: {
+      type: 'string',
+      title: 'Path Filter',
+      description: 'Optional path suffix. Leave empty to answer at the bare form URL.',
+      default: ''
+    }
+  }
+};
+
+const inputSchema = { type: 'object', properties: {} };
+
+const outputSchema = {
+  type: 'object',
+  properties: {
+    form: { type: 'object', description: 'The answers, keyed by field name. A file field holds a binary object.' },
+    submittedAt: { type: 'number', description: 'Milliseconds since the epoch' },
+    clientIp: { type: 'string' }
+  }
+};
+
+async function execute(config, input, context) {
+  // Every value here is built by the webhook controller, which has already
+  // validated it. Executed directly - from the editor's Run button - there is
+  // no submission, and an empty form is the honest answer rather than an error.
+  const triggerData = context.triggerData || {};
+
+  return {
+    form: triggerData.form || {},
+    submittedAt: triggerData.submittedAt || Date.now(),
+    clientIp: triggerData.clientIp || ''
+  };
+}
+
+module.exports = { configSchema, inputSchema, outputSchema, execute };

+ 16 - 0
tests/nodes/form-trigger-shape.json

@@ -0,0 +1,16 @@
+{
+  "name": "verify-form-trigger-shape",
+  "nodes": [
+    {"id": "n1", "name": "Form", "type": "form-trigger", "position": {"x": 0, "y": 0},
+     "config": {"title": "Send an image",
+                "fields": [{"name": "image", "label": "Image", "type": "file", "required": true}]}},
+    {"id": "n2", "name": "Check", "type": "code", "position": {"x": 0, "y": 100},
+     "config": {"code": "return { hasForm: typeof input.data.form === 'object' && input.data.form !== null, submitted: typeof input.data.submittedAt === 'number' };"}}
+  ],
+  "connections": [
+    {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "n2", "targetInput": "data"}
+  ],
+  "expect": {
+    "n2": {"status": "completed", "output": {"result": {"hasForm": true, "submitted": true}}}
+  }
+}