@belmontdigitalmarketing/n8n-nodes-flowlu 0.3.0 → 0.4.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.
@@ -38,9 +38,16 @@ function isFlowluRateLimit(value) {
38
38
  // GET wrapper that retries through Flowlu's rate limit with linear backoff. If it
39
39
  // never succeeds it throws, so callers surface the real cause instead of silently
40
40
  // returning no data (which previously masqueraded as "No fields found").
41
- async function flowluApiGetWithRetry(baseUrl, endpoint, apiKey, queryParams, maxRetries = 3) {
41
+ async function flowluApiGetWithRetry(baseUrl, endpoint, apiKey, queryParams, maxRetries = 4) {
42
42
  let lastMessage = 'Flowlu request failed';
43
43
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
44
+ // Jittered wait before each attempt. The small random delay on the first
45
+ // attempt de-syncs the burst of dropdown loads fired on node-open so they
46
+ // don't hit Flowlu's rate limit in lockstep; later attempts back off longer.
47
+ const wait = attempt === 0 ? Math.floor(Math.random() * 1200) : 1500 * attempt + Math.floor(Math.random() * 1000);
48
+ await new Promise((resolve) => {
49
+ setTimeout(resolve, wait);
50
+ });
44
51
  try {
45
52
  const response = await flowluApiRequest.call(this, 'GET', baseUrl, endpoint, apiKey, undefined, queryParams);
46
53
  if (!isFlowluRateLimit(response?.error ?? response)) {
@@ -54,12 +61,6 @@ async function flowluApiGetWithRetry(baseUrl, endpoint, apiKey, queryParams, max
54
61
  }
55
62
  lastMessage = error.message || 'Flowlu request limit exceeded';
56
63
  }
57
- // linear backoff before the next attempt (no wait after the final one)
58
- if (attempt < maxRetries) {
59
- await new Promise((resolve) => {
60
- setTimeout(resolve, 1500 * (attempt + 1));
61
- });
62
- }
63
64
  }
64
65
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Flowlu API rate limit reached while loading data (${lastMessage}). Wait a moment and click Retry.`);
65
66
  }
@@ -104,6 +105,15 @@ function applyResourceMapperFields(context, body, paramName, itemIndex) {
104
105
  // No custom fields set - ignore
105
106
  }
106
107
  }
108
+ // Read a resourceLocator parameter (or a legacy scalar value) as its underlying id.
109
+ function getResourceValue(context, name, itemIndex) {
110
+ const raw = context.getNodeParameter(name, itemIndex, '');
111
+ if (raw && typeof raw === 'object' && 'value' in raw) {
112
+ const value = raw.value;
113
+ return value === undefined || value === null ? '' : String(value);
114
+ }
115
+ return raw ? String(raw) : '';
116
+ }
107
117
  // Helper to load an entity's custom fields as name/value options.
108
118
  async function loadCustomFieldsForEntity(context, moduleFilter, modelFilter) {
109
119
  const { baseUrl, apiKey } = await getFlowluCredentials(context);
@@ -1299,13 +1309,21 @@ class Flowlu {
1299
1309
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1300
1310
  },
1301
1311
  {
1302
- displayName: 'Assignee Name or ID',
1312
+ displayName: 'Assignee',
1303
1313
  name: 'responsible_id',
1304
- type: 'options',
1305
- typeOptions: { loadOptionsMethod: 'getUsers' },
1314
+ type: 'resourceLocator',
1306
1315
  required: true,
1307
- default: '',
1308
- description: 'The user responsible for completing this task. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
1316
+ default: { mode: 'list', value: '' },
1317
+ description: 'The user responsible for completing this task',
1318
+ modes: [
1319
+ {
1320
+ displayName: 'From List',
1321
+ name: 'list',
1322
+ type: 'list',
1323
+ typeOptions: { searchListMethod: 'searchUsers', searchable: true },
1324
+ },
1325
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 176265' },
1326
+ ],
1309
1327
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1310
1328
  },
1311
1329
  {
@@ -1317,12 +1335,20 @@ class Flowlu {
1317
1335
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1318
1336
  },
1319
1337
  {
1320
- displayName: 'Owner Name or ID',
1338
+ displayName: 'Owner',
1321
1339
  name: 'owner_id',
1322
- type: 'options',
1323
- typeOptions: { loadOptionsMethod: 'getUsers' },
1324
- default: '',
1325
- description: 'The user who owns this task. If left blank, the credential\'s Default Task Owner is used. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
1340
+ type: 'resourceLocator',
1341
+ default: { mode: 'list', value: '' },
1342
+ description: 'The user who owns this task. If left blank, the credential\'s Default Task Owner is used.',
1343
+ modes: [
1344
+ {
1345
+ displayName: 'From List',
1346
+ name: 'list',
1347
+ type: 'list',
1348
+ typeOptions: { searchListMethod: 'searchUsers', searchable: true },
1349
+ },
1350
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 176265' },
1351
+ ],
1326
1352
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1327
1353
  },
1328
1354
  {
@@ -1338,21 +1364,37 @@ class Flowlu {
1338
1364
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1339
1365
  },
1340
1366
  {
1341
- displayName: 'Contact Name or ID',
1367
+ displayName: 'Contact',
1342
1368
  name: 'contact_id',
1343
- type: 'options',
1344
- typeOptions: { loadOptionsMethod: 'getContacts' },
1345
- default: '',
1346
- description: 'The contact this task is related to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
1369
+ type: 'resourceLocator',
1370
+ default: { mode: 'list', value: '' },
1371
+ description: 'The contact this task is related to. Search by name, or enter a contact ID.',
1372
+ modes: [
1373
+ {
1374
+ displayName: 'From List',
1375
+ name: 'list',
1376
+ type: 'list',
1377
+ typeOptions: { searchListMethod: 'searchContacts', searchable: true },
1378
+ },
1379
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 12345' },
1380
+ ],
1347
1381
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1348
1382
  },
1349
1383
  {
1350
- displayName: 'Project Name or ID',
1384
+ displayName: 'Project',
1351
1385
  name: 'model_id',
1352
- type: 'options',
1353
- typeOptions: { loadOptionsMethod: 'getProjects' },
1354
- default: '',
1355
- description: 'The project this task belongs to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
1386
+ type: 'resourceLocator',
1387
+ default: { mode: 'list', value: '' },
1388
+ description: 'The project this task belongs to',
1389
+ modes: [
1390
+ {
1391
+ displayName: 'From List',
1392
+ name: 'list',
1393
+ type: 'list',
1394
+ typeOptions: { searchListMethod: 'searchProjects', searchable: true },
1395
+ },
1396
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 678' },
1397
+ ],
1356
1398
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1357
1399
  },
1358
1400
  {
@@ -2106,6 +2148,45 @@ class Flowlu {
2106
2148
  }
2107
2149
  },
2108
2150
  },
2151
+ listSearch: {
2152
+ async searchUsers(filter) {
2153
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
2154
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/core/user/list', apiKey, {
2155
+ 'filter[role_login]': '1',
2156
+ });
2157
+ const term = (filter ?? '').toLowerCase();
2158
+ const results = items
2159
+ .map((u) => ({ name: u.name || u.email || `User ${u.id}`, value: u.id.toString() }))
2160
+ .filter((r) => !term || r.name.toLowerCase().includes(term));
2161
+ return { results };
2162
+ },
2163
+ async searchProjects(filter) {
2164
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
2165
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/st/projects/list', apiKey, {
2166
+ 'filter[is_archive]': '0',
2167
+ });
2168
+ const term = (filter ?? '').toLowerCase();
2169
+ const results = items
2170
+ .map((p) => ({ name: p.name || `Project ${p.id}`, value: p.id.toString() }))
2171
+ .filter((r) => !term || r.name.toLowerCase().includes(term));
2172
+ return { results };
2173
+ },
2174
+ async searchContacts(filter) {
2175
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
2176
+ const qs = { 'filter[type]': '2' };
2177
+ // Contacts can number in the thousands, so use Flowlu's server-side search and
2178
+ // take just the first page rather than paginating the whole list.
2179
+ if (filter)
2180
+ qs.search = filter;
2181
+ const response = await flowluApiGetWithRetry.call(this, baseUrl, '/api/v1/module/crm/account/list', apiKey, qs);
2182
+ const items = response?.response?.items ?? [];
2183
+ const results = items.map((c) => ({
2184
+ name: c.name || `Contact ${c.id}`,
2185
+ value: c.id.toString(),
2186
+ }));
2187
+ return { results };
2188
+ },
2189
+ },
2109
2190
  };
2110
2191
  }
2111
2192
  async execute() {
@@ -2529,7 +2610,7 @@ class Flowlu {
2529
2610
  if (operation === 'create') {
2530
2611
  const body = {
2531
2612
  name: this.getNodeParameter('taskName', i),
2532
- responsible_id: this.getNodeParameter('responsible_id', i),
2613
+ responsible_id: getResourceValue(this, 'responsible_id', i),
2533
2614
  };
2534
2615
  const description = this.getNodeParameter('description', i);
2535
2616
  if (description)
@@ -2539,10 +2620,10 @@ class Flowlu {
2539
2620
  body.workflow_stage_id = additional.workflow_stage_id || '1';
2540
2621
  // Owner/Priority/Contact/Project/Start/End moved to top-level fields; fall back to
2541
2622
  // Additional Fields so task-create nodes built before the move keep working.
2542
- const ownerId = this.getNodeParameter('owner_id', i, '') || additional.owner_id || defaultOwnerId;
2623
+ const ownerId = getResourceValue(this, 'owner_id', i) || additional.owner_id || defaultOwnerId;
2543
2624
  if (ownerId)
2544
2625
  body.owner_id = ownerId;
2545
- const contactId = this.getNodeParameter('contact_id', i, '') || additional.contact_id;
2626
+ const contactId = getResourceValue(this, 'contact_id', i) || additional.contact_id;
2546
2627
  if (contactId)
2547
2628
  body.crm_account_id = parseInt(contactId, 10);
2548
2629
  body.priority =
@@ -2575,7 +2656,7 @@ class Flowlu {
2575
2656
  if (additional.parent_id && additional.parent_id > 0) {
2576
2657
  body.parent_id = additional.parent_id;
2577
2658
  }
2578
- const modelId = this.getNodeParameter('model_id', i, '') || additional.model_id;
2659
+ const modelId = getResourceValue(this, 'model_id', i) || additional.model_id;
2579
2660
  if (modelId) {
2580
2661
  body.model_id = modelId;
2581
2662
  body.model = 'project';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@belmontdigitalmarketing/n8n-nodes-flowlu",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "n8n community node for the Flowlu CRM, project management, and task API",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",