@processlink/node-red-contrib-processlink 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git add:*)",
5
+ "Bash(git commit -m \"$\\(cat <<''EOF''\nAdd folder/area support to upload API\n\n- Add areaId and folderId params to /api/upload endpoint\n- Add API key auth to /api/sites/[siteId]/folders endpoint\n- Add new /api/sites/[siteId]/areas endpoint with API key auth\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
6
+ "Bash(git commit -m \"$\\(cat <<''EOF''\nAdd location selector for file uploads \\(v1.2.0\\)\n\n- Add Location dropdown showing Area > Folder hierarchy\n- Fetch areas and folders from Files API with API key auth\n- Send areaId and folderId in upload requests\n- Update README documentation\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
7
+ "Bash(npm publish:*)"
8
+ ]
9
+ }
10
+ }
package/README.md CHANGED
@@ -71,35 +71,44 @@ Then restart Node-RED.
71
71
  ### 3. Connect Your Flow
72
72
 
73
73
  ```
74
- [File In] → [files upload] ─┬─ Output 1 (success) → [Debug]
75
- └─ Output 2 (error) → [Debug]
74
+ [Inject] → [File In] → [files upload] ─┬─ Output 1 (success) → [Debug]
75
+ └─ Output 2 (error) → [Debug]
76
76
  ```
77
77
 
78
78
  ---
79
79
 
80
80
  ## Node Reference
81
81
 
82
- ### Files Upload
82
+ ---
83
+
84
+ ## Files Upload
83
85
 
84
86
  Uploads files to the Process Link Files API.
85
87
 
86
- #### Configuration
88
+ ### Configuration
87
89
 
88
90
  | Property | Description |
89
91
  |----------|-------------|
90
92
  | Config | Your Process Link credentials (Site ID + API Key) |
91
- | Filename | Default filename (optional, can be set via `msg.filename`) |
92
- | Prefix with timestamp | Adds `YYYY-MM-DD_HH-MM-SS_` prefix to filename |
93
+ | Filename | Filename for uploaded file (takes priority over `msg.filename`) |
94
+ | Location | Destination area/folder in the Files app (default: site root) |
95
+ | Prefix with timestamp | Adds `YYYY-MM-DD_HH-mm-ss_` prefix to filename (ISO 8601 format) |
93
96
  | Timeout | Request timeout in milliseconds (default: 30000) |
94
97
 
95
- #### Inputs
98
+ **Filename priority:** Config filename → `msg.filename` → `file.bin`
99
+
100
+ **Location:** The dropdown shows your site's folder structure organized by area. Select where uploaded files should be stored. Areas and folders are fetched from the Files API when you open the node configuration.
101
+
102
+ **Timestamp prefix:** When enabled, always prepends the current date/time to the filename, regardless of whether it came from config or `msg.filename`.
103
+
104
+ ### Inputs
96
105
 
97
106
  | Property | Type | Description |
98
107
  |----------|------|-------------|
99
108
  | `msg.payload` | Buffer \| string | The file content to upload |
100
- | `msg.filename` | string | *(Optional)* Filename to use |
109
+ | `msg.filename` | string | *(Optional)* Fallback filename if not set in config |
101
110
 
102
- #### Outputs
111
+ ### Outputs
103
112
 
104
113
  This node has **two outputs**:
105
114
 
@@ -108,15 +117,15 @@ This node has **two outputs**:
108
117
  | **1 - Success** | HTTP 201 | `msg.payload.ok`, `msg.payload.file_id`, `msg.file_id`, `msg.statusCode` |
109
118
  | **2 - Error** | API error, network error, timeout | `msg.payload.error`, `msg.statusCode` |
110
119
 
111
- #### Status Indicators
120
+ ### Status Indicators
112
121
 
113
122
  | Color | Meaning |
114
123
  |-------|---------|
124
+ | 🔴 Red | Error occurred |
115
125
  | 🟡 Yellow | Uploading in progress |
116
126
  | 🟢 Green | Upload successful |
117
- | 🔴 Red | Error occurred |
118
127
 
119
- #### Status Codes
128
+ ### Status Codes
120
129
 
121
130
  | Code | Meaning |
122
131
  |------|---------|
@@ -130,22 +139,22 @@ This node has **two outputs**:
130
139
 
131
140
  ---
132
141
 
133
- ### System Info
142
+ ## System Info
134
143
 
135
144
  Outputs system information for diagnostics and monitoring.
136
145
 
137
- #### Configuration
146
+ ### Configuration
138
147
 
139
148
  | Property | Description |
140
149
  |----------|-------------|
141
150
  | Send on deploy | When checked (default), outputs system info when the flow is deployed |
142
151
 
143
- #### Triggers
152
+ ### Triggers
144
153
 
145
154
  - **On deploy** (if enabled) - Automatically sends when flow starts
146
155
  - **On input** - Any incoming message triggers a fresh reading
147
156
 
148
- #### Output
157
+ ### Output
149
158
 
150
159
  `msg.payload` contains:
151
160
 
@@ -169,7 +178,7 @@ Outputs system information for diagnostics and monitoring.
169
178
  | `nodejs` | version |
170
179
  | `processMemory` | rss, heapTotal, heapUsed |
171
180
 
172
- #### Uptime/Memory Structure
181
+ ### Uptime/Memory Structure
173
182
 
174
183
  ```json
175
184
  {
@@ -293,7 +302,6 @@ Copy the JSON below and import into Node-RED: **Menu → Import → Clipboard**
293
302
 
294
303
  ## Support
295
304
 
296
- - 📖 **Documentation**: [GitHub Wiki](https://github.com/process-link/node-red-contrib-processlink/wiki)
297
305
  - 🐛 **Issues**: [GitHub Issues](https://github.com/process-link/node-red-contrib-processlink/issues)
298
306
  - 📧 **Email**: support@processlink.com.au
299
307
  - 🌐 **Website**: [processlink.com.au](https://processlink.com.au)
@@ -6,6 +6,8 @@
6
6
  name: { value: "" },
7
7
  server: { value: "", type: "processlink-config", required: true },
8
8
  filename: { value: "" },
9
+ areaId: { value: "" },
10
+ folderId: { value: "" },
9
11
  timestampPrefix: { value: false },
10
12
  timeout: { value: "30000" },
11
13
  apiUrl: { value: "https://files.processlink.com.au/api/upload" },
@@ -22,6 +24,131 @@
22
24
  },
23
25
  inputLabels: "file buffer",
24
26
  outputLabels: ["success", "error"],
27
+ oneditprepare: function () {
28
+ var node = this;
29
+ var $location = $("#node-input-location");
30
+ var $areaId = $("#node-input-areaId");
31
+ var $folderId = $("#node-input-folderId");
32
+ var $server = $("#node-input-server");
33
+
34
+ function loadLocations() {
35
+ var configId = $server.val();
36
+ if (!configId) {
37
+ $location.html('<option value="">-- Select config first --</option>');
38
+ return;
39
+ }
40
+
41
+ var configNode = RED.nodes.node(configId);
42
+ if (!configNode || !configNode.siteId) {
43
+ $location.html('<option value="">-- Config not ready --</option>');
44
+ return;
45
+ }
46
+
47
+ $location.html('<option value="">Loading...</option>');
48
+
49
+ var baseUrl = "https://files.processlink.com.au/api/sites/" + configNode.siteId;
50
+ var headers = { "Authorization": "Bearer " + configNode.credentials.apiKey };
51
+
52
+ // Fetch both areas and folders in parallel
53
+ $.when(
54
+ $.ajax({ url: baseUrl + "/areas", method: "GET", headers: headers }),
55
+ $.ajax({ url: baseUrl + "/folders", method: "GET", headers: headers })
56
+ ).done(function (areasResult, foldersResult) {
57
+ var areas = areasResult[0] || [];
58
+ var folders = foldersResult[0] || [];
59
+
60
+ // Build current selection key for matching
61
+ var currentAreaId = node.areaId || "";
62
+ var currentFolderId = node.folderId || "";
63
+ var currentKey = currentAreaId + "|" + currentFolderId;
64
+
65
+ var options = '<option value="|" data-area="" data-folder="">Site root (default)</option>';
66
+
67
+ // Group folders by area
68
+ var foldersByArea = {};
69
+ folders.forEach(function (f) {
70
+ var areaKey = f.area_id || "";
71
+ if (!foldersByArea[areaKey]) foldersByArea[areaKey] = [];
72
+ foldersByArea[areaKey].push(f);
73
+ });
74
+
75
+ // Add areas with their folders
76
+ areas.forEach(function (area) {
77
+ var areaFolders = foldersByArea[area.id] || [];
78
+ options += '<optgroup label="' + area.area_name + '">';
79
+
80
+ // Area root option
81
+ var areaRootKey = area.id + "|";
82
+ var areaRootSelected = areaRootKey === currentKey ? ' selected' : '';
83
+ options += '<option value="' + areaRootKey + '" data-area="' + area.id + '" data-folder=""' + areaRootSelected + '>' + area.area_name + ' (root)</option>';
84
+
85
+ // Root folders in this area
86
+ var rootFolders = areaFolders.filter(function (f) { return !f.parent_id; });
87
+ rootFolders.sort(function (a, b) { return a.name.localeCompare(b.name); });
88
+
89
+ rootFolders.forEach(function (folder) {
90
+ var folderKey = area.id + "|" + folder.id;
91
+ var selected = folderKey === currentKey ? ' selected' : '';
92
+ options += '<option value="' + folderKey + '" data-area="' + area.id + '" data-folder="' + folder.id + '"' + selected + '>&nbsp;&nbsp;' + folder.name + '</option>';
93
+
94
+ // Child folders
95
+ var children = areaFolders.filter(function (c) { return c.parent_id === folder.id; });
96
+ children.sort(function (a, b) { return a.name.localeCompare(b.name); });
97
+ children.forEach(function (child) {
98
+ var childKey = area.id + "|" + child.id;
99
+ var childSelected = childKey === currentKey ? ' selected' : '';
100
+ options += '<option value="' + childKey + '" data-area="' + area.id + '" data-folder="' + child.id + '"' + childSelected + '>&nbsp;&nbsp;&nbsp;&nbsp;' + child.name + '</option>';
101
+ });
102
+ });
103
+
104
+ options += '</optgroup>';
105
+ });
106
+
107
+ // Add folders without an area
108
+ var noAreaFolders = foldersByArea[""] || [];
109
+ if (noAreaFolders.length > 0) {
110
+ options += '<optgroup label="No Area">';
111
+ var rootFolders = noAreaFolders.filter(function (f) { return !f.parent_id; });
112
+ rootFolders.sort(function (a, b) { return a.name.localeCompare(b.name); });
113
+
114
+ rootFolders.forEach(function (folder) {
115
+ var folderKey = "|" + folder.id;
116
+ var selected = folderKey === currentKey ? ' selected' : '';
117
+ options += '<option value="' + folderKey + '" data-area="" data-folder="' + folder.id + '"' + selected + '>' + folder.name + '</option>';
118
+
119
+ var children = noAreaFolders.filter(function (c) { return c.parent_id === folder.id; });
120
+ children.sort(function (a, b) { return a.name.localeCompare(b.name); });
121
+ children.forEach(function (child) {
122
+ var childKey = "|" + child.id;
123
+ var childSelected = childKey === currentKey ? ' selected' : '';
124
+ options += '<option value="' + childKey + '" data-area="" data-folder="' + child.id + '"' + childSelected + '>&nbsp;&nbsp;' + child.name + '</option>';
125
+ });
126
+ });
127
+ options += '</optgroup>';
128
+ }
129
+
130
+ $location.html(options);
131
+ }).fail(function (xhr) {
132
+ console.error("Failed to load locations:", xhr.responseText);
133
+ $location.html('<option value="|">Site root (default)</option><option disabled>-- Failed to load --</option>');
134
+ });
135
+ }
136
+
137
+ // Update hidden inputs when selection changes
138
+ $location.on("change", function () {
139
+ var $selected = $location.find(":selected");
140
+ $areaId.val($selected.data("area") || "");
141
+ $folderId.val($selected.data("folder") || "");
142
+ });
143
+
144
+ // Load locations when config changes
145
+ $server.on("change", function () {
146
+ loadLocations();
147
+ });
148
+
149
+ // Initial load
150
+ loadLocations();
151
+ },
25
152
  });
26
153
  </script>
27
154
 
@@ -38,10 +165,18 @@
38
165
  <label for="node-input-filename"><i class="fa fa-file"></i> Filename</label>
39
166
  <input type="text" id="node-input-filename" placeholder="msg.filename (or specify default)" />
40
167
  </div>
168
+ <div class="form-row">
169
+ <label for="node-input-location"><i class="fa fa-folder"></i> Location</label>
170
+ <select id="node-input-location" style="width: 70%">
171
+ <option value="|">Site root (default)</option>
172
+ </select>
173
+ <input type="hidden" id="node-input-areaId" />
174
+ <input type="hidden" id="node-input-folderId" />
175
+ </div>
41
176
  <div class="form-row">
42
177
  <label for="node-input-timestampPrefix">&nbsp;</label>
43
178
  <input type="checkbox" id="node-input-timestampPrefix" style="width: auto; margin-right: 10px;">
44
- <span>Prefix filename with timestamp (YYYY-MM-DD_HH-MM-SS_)</span>
179
+ <span>Prefix filename with timestamp (YYYY-MM-DD_HH-mm-ss_)</span>
45
180
  </div>
46
181
  <div class="form-row">
47
182
  <label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout</label>
@@ -62,7 +197,7 @@
62
197
  <dt>payload <span class="property-type">buffer | string</span></dt>
63
198
  <dd>The file content to upload.</dd>
64
199
  <dt class="optional">filename <span class="property-type">string</span></dt>
65
- <dd>Filename for the upload. Can be set via <code>msg.filename</code> or in the node config. Defaults to "file.bin".</dd>
200
+ <dd>Fallback filename if not set in config. Priority: config <code>msg.filename</code> "file.bin".</dd>
66
201
  </dl>
67
202
 
68
203
  <h3>Outputs</h3>
@@ -95,9 +230,11 @@
95
230
  <h3>Configuration</h3>
96
231
  <dl class="message-properties">
97
232
  <dt>Filename <span class="property-type">string</span></dt>
98
- <dd>Default filename if <code>msg.filename</code> is not set.</dd>
233
+ <dd>Filename for uploaded file. Takes priority over <code>msg.filename</code>.</dd>
234
+ <dt>Location <span class="property-type">select</span></dt>
235
+ <dd>Destination area/folder in the Files app. Default: site root.</dd>
99
236
  <dt>Prefix with timestamp <span class="property-type">boolean</span></dt>
100
- <dd>Adds <code>YYYY-MM-DD_HH-MM-SS_</code> prefix to filename.</dd>
237
+ <dd>Adds <code>YYYY-MM-DD_HH-mm-ss_</code> prefix (ISO 8601). Applied regardless of filename source.</dd>
101
238
  <dt>Timeout <span class="property-type">number</span></dt>
102
239
  <dd>Request timeout in ms. Default: 30000.</dd>
103
240
  </dl>
@@ -53,8 +53,8 @@ module.exports = function (RED) {
53
53
  return;
54
54
  }
55
55
 
56
- // Get filename
57
- const filename = msg.filename || config.filename || "file.bin";
56
+ // Get filename (config takes priority over msg.filename)
57
+ const filename = config.filename || msg.filename || "file.bin";
58
58
  let basename = filename.split(/[\\/]/).pop();
59
59
 
60
60
  // Add timestamp prefix if enabled
@@ -71,14 +71,38 @@ module.exports = function (RED) {
71
71
 
72
72
  // Build multipart form data
73
73
  const boundary = "----NodeREDProcessLink" + Date.now() + Math.random().toString(36).substring(2);
74
+ const parts = [];
74
75
 
75
- const header = Buffer.from(
76
+ // Add file part
77
+ parts.push(Buffer.from(
76
78
  `--${boundary}\r\n` +
77
79
  `Content-Disposition: form-data; name="file"; filename="${basename}"\r\n` +
78
80
  `Content-Type: application/octet-stream\r\n\r\n`
79
- );
80
- const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
81
- const body = Buffer.concat([header, fileBuffer, footer]);
81
+ ));
82
+ parts.push(fileBuffer);
83
+
84
+ // Add areaId if specified
85
+ if (config.areaId) {
86
+ parts.push(Buffer.from(
87
+ `\r\n--${boundary}\r\n` +
88
+ `Content-Disposition: form-data; name="areaId"\r\n\r\n` +
89
+ config.areaId
90
+ ));
91
+ }
92
+
93
+ // Add folderId if specified
94
+ if (config.folderId) {
95
+ parts.push(Buffer.from(
96
+ `\r\n--${boundary}\r\n` +
97
+ `Content-Disposition: form-data; name="folderId"\r\n\r\n` +
98
+ config.folderId
99
+ ));
100
+ }
101
+
102
+ // Add closing boundary
103
+ parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
104
+
105
+ const body = Buffer.concat(parts);
82
106
 
83
107
  // Parse URL
84
108
  const apiUrl = config.apiUrl || "https://files.processlink.com.au/api/upload";
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.1.1",
6
+ "version": "1.2.0",
7
7
  "description": "Node-RED nodes for Process Link platform integration - upload files, send notifications, and connect to industrial automation systems",
8
8
  "keywords": [
9
9
  "node-red",