@belmontdigitalmarketing/n8n-nodes-flowlu 0.3.1 → 0.5.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.
@@ -105,6 +105,15 @@ function applyResourceMapperFields(context, body, paramName, itemIndex) {
105
105
  // No custom fields set - ignore
106
106
  }
107
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
+ }
108
117
  // Helper to load an entity's custom fields as name/value options.
109
118
  async function loadCustomFieldsForEntity(context, moduleFilter, modelFilter) {
110
119
  const { baseUrl, apiKey } = await getFlowluCredentials(context);
@@ -635,32 +644,56 @@ class Flowlu {
635
644
  displayOptions: { show: { resource: ['opportunity'], operation: ['create'] } },
636
645
  },
637
646
  {
638
- displayName: 'Pipeline Name or ID',
647
+ displayName: 'Pipeline',
639
648
  name: 'pipeline_id',
640
- type: 'options',
641
- typeOptions: { loadOptionsMethod: 'getPipelines' },
649
+ type: 'resourceLocator',
642
650
  required: true,
643
- default: '',
644
- description: 'The sales pipeline for this opportunity. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
651
+ default: { mode: 'list', value: '' },
652
+ description: 'The sales pipeline for this opportunity',
653
+ modes: [
654
+ {
655
+ displayName: 'From List',
656
+ name: 'list',
657
+ type: 'list',
658
+ typeOptions: { searchListMethod: 'searchPipelines', searchable: true },
659
+ },
660
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 3' },
661
+ ],
645
662
  displayOptions: { show: { resource: ['opportunity'], operation: ['create'] } },
646
663
  },
647
664
  {
648
- displayName: 'Pipeline Stage Name or ID',
665
+ displayName: 'Pipeline Stage',
649
666
  name: 'pipeline_stage_id',
650
- type: 'options',
651
- typeOptions: { loadOptionsMethod: 'getPipelineStages' },
667
+ type: 'resourceLocator',
652
668
  required: true,
653
- default: '',
654
- description: 'The stage within the pipeline. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
669
+ default: { mode: 'list', value: '' },
670
+ description: 'The stage within the pipeline',
671
+ modes: [
672
+ {
673
+ displayName: 'From List',
674
+ name: 'list',
675
+ type: 'list',
676
+ typeOptions: { searchListMethod: 'searchPipelineStages', searchable: true },
677
+ },
678
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 12' },
679
+ ],
655
680
  displayOptions: { show: { resource: ['opportunity'], operation: ['create'] } },
656
681
  },
657
682
  {
658
- displayName: 'Assignee Name or ID',
683
+ displayName: 'Assignee',
659
684
  name: 'opportunityAssignee',
660
- type: 'options',
661
- typeOptions: { loadOptionsMethod: 'getUsers' },
662
- default: '',
663
- description: 'The user responsible for this opportunity. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
685
+ type: 'resourceLocator',
686
+ default: { mode: 'list', value: '' },
687
+ description: 'The user responsible for this opportunity',
688
+ modes: [
689
+ {
690
+ displayName: 'From List',
691
+ name: 'list',
692
+ type: 'list',
693
+ typeOptions: { searchListMethod: 'searchUsers', searchable: true },
694
+ },
695
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 176265' },
696
+ ],
664
697
  displayOptions: { show: { resource: ['opportunity'], operation: ['create'] } },
665
698
  },
666
699
  // Opportunity Create: Additional Fields
@@ -859,24 +892,37 @@ class Flowlu {
859
892
  displayOptions: { show: { resource: ['opportunity'], operation: ['getAll'] } },
860
893
  },
861
894
  {
862
- displayName: 'Pipeline Name or ID',
895
+ displayName: 'Pipeline',
863
896
  name: 'opportunityFilterPipeline',
864
- type: 'options',
865
- typeOptions: { loadOptionsMethod: 'getPipelines' },
866
- default: '',
867
- description: 'Filter by pipeline (also controls which stages appear below). Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
897
+ type: 'resourceLocator',
898
+ default: { mode: 'list', value: '' },
899
+ description: 'Filter by pipeline (also controls which stages appear below)',
900
+ modes: [
901
+ {
902
+ displayName: 'From List',
903
+ name: 'list',
904
+ type: 'list',
905
+ typeOptions: { searchListMethod: 'searchPipelines', searchable: true },
906
+ },
907
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 3' },
908
+ ],
868
909
  displayOptions: { show: { resource: ['opportunity'], operation: ['getAll'] } },
869
910
  },
870
911
  {
871
- displayName: 'Pipeline Stage Name or ID',
912
+ displayName: 'Pipeline Stage',
872
913
  name: 'opportunityFilterStage',
873
- type: 'options',
874
- typeOptions: {
875
- loadOptionsMethod: 'getFilteredPipelineStages',
876
- loadOptionsDependsOn: ['opportunityFilterPipeline'],
877
- },
878
- default: '',
879
- description: 'Filter by stage (select a pipeline first to see its stages). Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
914
+ type: 'resourceLocator',
915
+ default: { mode: 'list', value: '' },
916
+ description: 'Filter by stage (select a pipeline first to narrow the list)',
917
+ modes: [
918
+ {
919
+ displayName: 'From List',
920
+ name: 'list',
921
+ type: 'list',
922
+ typeOptions: { searchListMethod: 'searchFilteredPipelineStages', searchable: true },
923
+ },
924
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 12' },
925
+ ],
880
926
  displayOptions: { show: { resource: ['opportunity'], operation: ['getAll'] } },
881
927
  },
882
928
  {
@@ -939,12 +985,20 @@ class Flowlu {
939
985
  displayOptions: { show: { resource: ['project'], operation: ['create'] } },
940
986
  },
941
987
  {
942
- displayName: 'Manager Name or ID',
988
+ displayName: 'Manager',
943
989
  name: 'manager_id',
944
- type: 'options',
945
- typeOptions: { loadOptionsMethod: 'getUsers' },
946
- default: '',
947
- description: 'The user responsible for managing this project. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
990
+ type: 'resourceLocator',
991
+ default: { mode: 'list', value: '' },
992
+ description: 'The user responsible for managing this project',
993
+ modes: [
994
+ {
995
+ displayName: 'From List',
996
+ name: 'list',
997
+ type: 'list',
998
+ typeOptions: { searchListMethod: 'searchUsers', searchable: true },
999
+ },
1000
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 176265' },
1001
+ ],
948
1002
  displayOptions: { show: { resource: ['project'], operation: ['create'] } },
949
1003
  },
950
1004
  {
@@ -1300,13 +1354,21 @@ class Flowlu {
1300
1354
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1301
1355
  },
1302
1356
  {
1303
- displayName: 'Assignee Name or ID',
1357
+ displayName: 'Assignee',
1304
1358
  name: 'responsible_id',
1305
- type: 'options',
1306
- typeOptions: { loadOptionsMethod: 'getUsers' },
1359
+ type: 'resourceLocator',
1307
1360
  required: true,
1308
- default: '',
1309
- 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>.',
1361
+ default: { mode: 'list', value: '' },
1362
+ description: 'The user responsible for completing this task',
1363
+ modes: [
1364
+ {
1365
+ displayName: 'From List',
1366
+ name: 'list',
1367
+ type: 'list',
1368
+ typeOptions: { searchListMethod: 'searchUsers', searchable: true },
1369
+ },
1370
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 176265' },
1371
+ ],
1310
1372
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1311
1373
  },
1312
1374
  {
@@ -1318,12 +1380,20 @@ class Flowlu {
1318
1380
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1319
1381
  },
1320
1382
  {
1321
- displayName: 'Owner Name or ID',
1383
+ displayName: 'Owner',
1322
1384
  name: 'owner_id',
1323
- type: 'options',
1324
- typeOptions: { loadOptionsMethod: 'getUsers' },
1325
- default: '',
1326
- 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>.',
1385
+ type: 'resourceLocator',
1386
+ default: { mode: 'list', value: '' },
1387
+ description: 'The user who owns this task. If left blank, the credential\'s Default Task Owner is used.',
1388
+ modes: [
1389
+ {
1390
+ displayName: 'From List',
1391
+ name: 'list',
1392
+ type: 'list',
1393
+ typeOptions: { searchListMethod: 'searchUsers', searchable: true },
1394
+ },
1395
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 176265' },
1396
+ ],
1327
1397
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1328
1398
  },
1329
1399
  {
@@ -1339,21 +1409,37 @@ class Flowlu {
1339
1409
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1340
1410
  },
1341
1411
  {
1342
- displayName: 'Contact Name or ID',
1412
+ displayName: 'Contact',
1343
1413
  name: 'contact_id',
1344
- type: 'options',
1345
- typeOptions: { loadOptionsMethod: 'getContacts' },
1346
- default: '',
1347
- 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>.',
1414
+ type: 'resourceLocator',
1415
+ default: { mode: 'list', value: '' },
1416
+ description: 'The contact this task is related to. Search by name, or enter a contact ID.',
1417
+ modes: [
1418
+ {
1419
+ displayName: 'From List',
1420
+ name: 'list',
1421
+ type: 'list',
1422
+ typeOptions: { searchListMethod: 'searchContacts', searchable: true },
1423
+ },
1424
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 12345' },
1425
+ ],
1348
1426
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1349
1427
  },
1350
1428
  {
1351
- displayName: 'Project Name or ID',
1429
+ displayName: 'Project',
1352
1430
  name: 'model_id',
1353
- type: 'options',
1354
- typeOptions: { loadOptionsMethod: 'getProjects' },
1355
- default: '',
1356
- 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>.',
1431
+ type: 'resourceLocator',
1432
+ default: { mode: 'list', value: '' },
1433
+ description: 'The project this task belongs to',
1434
+ modes: [
1435
+ {
1436
+ displayName: 'From List',
1437
+ name: 'list',
1438
+ type: 'list',
1439
+ typeOptions: { searchListMethod: 'searchProjects', searchable: true },
1440
+ },
1441
+ { displayName: 'By ID', name: 'id', type: 'string', placeholder: 'e.g. 678' },
1442
+ ],
1357
1443
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1358
1444
  },
1359
1445
  {
@@ -1804,21 +1890,6 @@ class Flowlu {
1804
1890
  description: `Pipeline ID: ${s.pipeline_id}`,
1805
1891
  }));
1806
1892
  },
1807
- async getFilteredPipelineStages() {
1808
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1809
- let pipelineId;
1810
- try {
1811
- pipelineId = this.getNodeParameter('opportunityFilterPipeline');
1812
- }
1813
- catch {
1814
- /* not set yet */
1815
- }
1816
- const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/pipeline_stage/list', apiKey);
1817
- const stages = pipelineId
1818
- ? items.filter((s) => s.pipeline_id?.toString() === pipelineId)
1819
- : items;
1820
- return stages.map((s) => ({ name: s.name || `Stage ${s.id}`, value: s.id.toString() }));
1821
- },
1822
1893
  async getAllCrmAccounts() {
1823
1894
  const { baseUrl, apiKey } = await getFlowluCredentials(this);
1824
1895
  const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/account/list', apiKey);
@@ -2107,6 +2178,80 @@ class Flowlu {
2107
2178
  }
2108
2179
  },
2109
2180
  },
2181
+ listSearch: {
2182
+ async searchUsers(filter) {
2183
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
2184
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/core/user/list', apiKey, {
2185
+ 'filter[role_login]': '1',
2186
+ });
2187
+ const term = (filter ?? '').toLowerCase();
2188
+ const results = items
2189
+ .map((u) => ({ name: u.name || u.email || `User ${u.id}`, value: u.id.toString() }))
2190
+ .filter((r) => !term || r.name.toLowerCase().includes(term));
2191
+ return { results };
2192
+ },
2193
+ async searchProjects(filter) {
2194
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
2195
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/st/projects/list', apiKey, {
2196
+ 'filter[is_archive]': '0',
2197
+ });
2198
+ const term = (filter ?? '').toLowerCase();
2199
+ const results = items
2200
+ .map((p) => ({ name: p.name || `Project ${p.id}`, value: p.id.toString() }))
2201
+ .filter((r) => !term || r.name.toLowerCase().includes(term));
2202
+ return { results };
2203
+ },
2204
+ async searchContacts(filter) {
2205
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
2206
+ const qs = { 'filter[type]': '2' };
2207
+ // Contacts can number in the thousands, so use Flowlu's server-side search and
2208
+ // take just the first page rather than paginating the whole list.
2209
+ if (filter)
2210
+ qs.search = filter;
2211
+ const response = await flowluApiGetWithRetry.call(this, baseUrl, '/api/v1/module/crm/account/list', apiKey, qs);
2212
+ const items = response?.response?.items ?? [];
2213
+ const results = items.map((c) => ({
2214
+ name: c.name || `Contact ${c.id}`,
2215
+ value: c.id.toString(),
2216
+ }));
2217
+ return { results };
2218
+ },
2219
+ async searchPipelines(filter) {
2220
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
2221
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/pipeline/list', apiKey);
2222
+ const term = (filter ?? '').toLowerCase();
2223
+ const results = items
2224
+ .map((p) => ({ name: p.name || `Pipeline ${p.id}`, value: p.id.toString() }))
2225
+ .filter((r) => !term || r.name.toLowerCase().includes(term));
2226
+ return { results };
2227
+ },
2228
+ async searchPipelineStages(filter) {
2229
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
2230
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/pipeline_stage/list', apiKey);
2231
+ const term = (filter ?? '').toLowerCase();
2232
+ const results = items
2233
+ .map((s) => ({ name: s.name || `Stage ${s.id}`, value: s.id.toString() }))
2234
+ .filter((r) => !term || r.name.toLowerCase().includes(term));
2235
+ return { results };
2236
+ },
2237
+ async searchFilteredPipelineStages(filter) {
2238
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
2239
+ let pipelineId = '';
2240
+ try {
2241
+ pipelineId = this.getCurrentNodeParameter('opportunityFilterPipeline', { extractValue: true }) || '';
2242
+ }
2243
+ catch {
2244
+ /* pipeline not selected yet */
2245
+ }
2246
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/pipeline_stage/list', apiKey);
2247
+ const term = (filter ?? '').toLowerCase();
2248
+ const results = items
2249
+ .filter((s) => !pipelineId || s.pipeline_id?.toString() === pipelineId)
2250
+ .map((s) => ({ name: s.name || `Stage ${s.id}`, value: s.id.toString() }))
2251
+ .filter((r) => !term || r.name.toLowerCase().includes(term));
2252
+ return { results };
2253
+ },
2254
+ },
2110
2255
  };
2111
2256
  }
2112
2257
  async execute() {
@@ -2250,10 +2395,10 @@ class Flowlu {
2250
2395
  if (operation === 'create') {
2251
2396
  const body = {
2252
2397
  name: this.getNodeParameter('opportunityName', i),
2253
- pipeline_id: this.getNodeParameter('pipeline_id', i),
2254
- pipeline_stage_id: this.getNodeParameter('pipeline_stage_id', i),
2398
+ pipeline_id: getResourceValue(this, 'pipeline_id', i),
2399
+ pipeline_stage_id: getResourceValue(this, 'pipeline_stage_id', i),
2255
2400
  };
2256
- const assignee = this.getNodeParameter('opportunityAssignee', i);
2401
+ const assignee = getResourceValue(this, 'opportunityAssignee', i);
2257
2402
  if (assignee)
2258
2403
  body.assignee_id = assignee;
2259
2404
  const additional = this.getNodeParameter('opportunityAdditionalFields', i);
@@ -2308,8 +2453,8 @@ class Flowlu {
2308
2453
  }
2309
2454
  else if (operation === 'getAll') {
2310
2455
  const limit = this.getNodeParameter('limit', i);
2311
- const pipelineId = this.getNodeParameter('opportunityFilterPipeline', i);
2312
- const stageId = this.getNodeParameter('opportunityFilterStage', i);
2456
+ const pipelineId = getResourceValue(this, 'opportunityFilterPipeline', i);
2457
+ const stageId = getResourceValue(this, 'opportunityFilterStage', i);
2313
2458
  const filters = this.getNodeParameter('opportunityFilters', i);
2314
2459
  const qs = { limit: limit.toString() };
2315
2460
  if (pipelineId)
@@ -2380,7 +2525,7 @@ class Flowlu {
2380
2525
  const body = {
2381
2526
  name: this.getNodeParameter('projectName', i),
2382
2527
  };
2383
- const managerId = this.getNodeParameter('manager_id', i);
2528
+ const managerId = getResourceValue(this, 'manager_id', i);
2384
2529
  if (managerId)
2385
2530
  body.manager_id = managerId;
2386
2531
  const desc = this.getNodeParameter('projectDescription', i);
@@ -2530,7 +2675,7 @@ class Flowlu {
2530
2675
  if (operation === 'create') {
2531
2676
  const body = {
2532
2677
  name: this.getNodeParameter('taskName', i),
2533
- responsible_id: this.getNodeParameter('responsible_id', i),
2678
+ responsible_id: getResourceValue(this, 'responsible_id', i),
2534
2679
  };
2535
2680
  const description = this.getNodeParameter('description', i);
2536
2681
  if (description)
@@ -2540,10 +2685,10 @@ class Flowlu {
2540
2685
  body.workflow_stage_id = additional.workflow_stage_id || '1';
2541
2686
  // Owner/Priority/Contact/Project/Start/End moved to top-level fields; fall back to
2542
2687
  // Additional Fields so task-create nodes built before the move keep working.
2543
- const ownerId = this.getNodeParameter('owner_id', i, '') || additional.owner_id || defaultOwnerId;
2688
+ const ownerId = getResourceValue(this, 'owner_id', i) || additional.owner_id || defaultOwnerId;
2544
2689
  if (ownerId)
2545
2690
  body.owner_id = ownerId;
2546
- const contactId = this.getNodeParameter('contact_id', i, '') || additional.contact_id;
2691
+ const contactId = getResourceValue(this, 'contact_id', i) || additional.contact_id;
2547
2692
  if (contactId)
2548
2693
  body.crm_account_id = parseInt(contactId, 10);
2549
2694
  body.priority =
@@ -2576,7 +2721,7 @@ class Flowlu {
2576
2721
  if (additional.parent_id && additional.parent_id > 0) {
2577
2722
  body.parent_id = additional.parent_id;
2578
2723
  }
2579
- const modelId = this.getNodeParameter('model_id', i, '') || additional.model_id;
2724
+ const modelId = getResourceValue(this, 'model_id', i) || additional.model_id;
2580
2725
  if (modelId) {
2581
2726
  body.model_id = modelId;
2582
2727
  body.model = 'project';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@belmontdigitalmarketing/n8n-nodes-flowlu",
3
- "version": "0.3.1",
3
+ "version": "0.5.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",