@belmontdigitalmarketing/n8n-nodes-flowlu 0.2.2 → 0.3.1

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.
@@ -27,6 +27,14 @@ class FlowluApi {
27
27
  description: 'Your Flowlu API key from Settings > API Settings',
28
28
  required: true,
29
29
  },
30
+ {
31
+ displayName: 'Default Task Owner User ID',
32
+ name: 'defaultOwnerId',
33
+ type: 'string',
34
+ default: '',
35
+ placeholder: 'e.g. 176265',
36
+ description: "Optional. When a task is created with no Owner set, it's assigned to this Flowlu user ID. User IDs differ between Flowlu accounts, so set this per credential. Leave blank to use Flowlu's own default.",
37
+ },
30
38
  ];
31
39
  }
32
40
  }
@@ -9,8 +9,9 @@ async function getFlowluCredentials(context) {
9
9
  const credentials = await context.getCredentials('flowluApi');
10
10
  const subdomain = credentials.subdomain;
11
11
  const apiKey = credentials.apiKey;
12
+ const defaultOwnerId = (credentials.defaultOwnerId || '').trim();
12
13
  const baseUrl = subdomain.endsWith('.flowlu.com') ? `https://${subdomain}` : `https://${subdomain}.flowlu.com`;
13
- return { baseUrl, apiKey };
14
+ return { baseUrl, apiKey, defaultOwnerId };
14
15
  }
15
16
  async function flowluApiRequest(method, baseUrl, endpoint, apiKey, body, queryParams) {
16
17
  const qs = { api_key: apiKey };
@@ -37,37 +38,42 @@ function isFlowluRateLimit(value) {
37
38
  // GET wrapper that retries through Flowlu's rate limit with linear backoff. If it
38
39
  // never succeeds it throws, so callers surface the real cause instead of silently
39
40
  // returning no data (which previously masqueraded as "No fields found").
40
- async function flowluApiGetWithRetry(baseUrl, endpoint, apiKey, queryParams, maxRetries = 3) {
41
- let lastError = new Error('Flowlu request failed');
41
+ async function flowluApiGetWithRetry(baseUrl, endpoint, apiKey, queryParams, maxRetries = 4) {
42
+ let lastMessage = 'Flowlu request failed';
42
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
+ });
43
51
  try {
44
52
  const response = await flowluApiRequest.call(this, 'GET', baseUrl, endpoint, apiKey, undefined, queryParams);
45
53
  if (!isFlowluRateLimit(response?.error ?? response)) {
46
54
  return response;
47
55
  }
48
- lastError = new Error(String(response?.error ?? 'Flowlu request limit exceeded'));
56
+ lastMessage = String(response?.error ?? 'Flowlu request limit exceeded');
49
57
  }
50
58
  catch (error) {
51
- if (!isFlowluRateLimit(error))
52
- throw error;
53
- lastError = error;
54
- }
55
- // linear backoff before the next attempt (no wait after the final one)
56
- if (attempt < maxRetries) {
57
- await new Promise((resolve) => {
58
- setTimeout(resolve, 1500 * (attempt + 1));
59
- });
59
+ if (!isFlowluRateLimit(error)) {
60
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Flowlu API error: ${error.message}`);
61
+ }
62
+ lastMessage = error.message || 'Flowlu request limit exceeded';
60
63
  }
61
64
  }
62
- throw lastError instanceof Error ? lastError : new Error('Flowlu request failed');
65
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Flowlu API rate limit reached while loading data (${lastMessage}). Wait a moment and click Retry.`);
63
66
  }
64
67
  // Fetch every page of a Flowlu list endpoint. The API caps responses at 50 items
65
68
  // per page, so a single call silently truncates larger result sets.
66
- async function flowluListAll(baseUrl, endpoint, apiKey) {
69
+ async function flowluListAll(baseUrl, endpoint, apiKey, queryParams) {
67
70
  const items = [];
68
71
  const maxPages = 100; // safety bound
69
72
  for (let page = 1; page <= maxPages; page++) {
70
- const response = await flowluApiGetWithRetry.call(this, baseUrl, endpoint, apiKey, { page: String(page) });
73
+ const response = await flowluApiGetWithRetry.call(this, baseUrl, endpoint, apiKey, {
74
+ ...(queryParams ?? {}),
75
+ page: String(page),
76
+ });
71
77
  const pageItems = response?.response?.items;
72
78
  if (!Array.isArray(pageItems) || pageItems.length === 0)
73
79
  break;
@@ -1311,6 +1317,59 @@ class Flowlu {
1311
1317
  default: '',
1312
1318
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1313
1319
  },
1320
+ {
1321
+ displayName: 'Owner Name or ID',
1322
+ 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>.',
1327
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1328
+ },
1329
+ {
1330
+ displayName: 'Priority',
1331
+ name: 'priority',
1332
+ type: 'options',
1333
+ options: [
1334
+ { name: 'Low', value: 1 },
1335
+ { name: 'Medium', value: 2 },
1336
+ { name: 'High', value: 3 },
1337
+ ],
1338
+ default: 2,
1339
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1340
+ },
1341
+ {
1342
+ displayName: 'Contact Name or ID',
1343
+ 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>.',
1348
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1349
+ },
1350
+ {
1351
+ displayName: 'Project Name or ID',
1352
+ 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>.',
1357
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1358
+ },
1359
+ {
1360
+ displayName: 'Start Date',
1361
+ name: 'plan_start_date',
1362
+ type: 'dateTime',
1363
+ default: '',
1364
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1365
+ },
1366
+ {
1367
+ displayName: 'End Date',
1368
+ name: 'deadline',
1369
+ type: 'dateTime',
1370
+ default: '',
1371
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1372
+ },
1314
1373
  // Task Create: Additional Fields
1315
1374
  {
1316
1375
  displayName: 'Additional Fields',
@@ -1321,15 +1380,6 @@ class Flowlu {
1321
1380
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1322
1381
  options: [
1323
1382
  { displayName: 'Allow End Date Change', name: 'deadline_allowchange', type: 'boolean', default: true },
1324
- {
1325
- displayName: 'Contact Name or ID',
1326
- name: 'contact_id',
1327
- type: 'options',
1328
- typeOptions: { loadOptionsMethod: 'getContacts' },
1329
- default: '',
1330
- 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>.',
1331
- },
1332
- { displayName: 'End Date', name: 'deadline', type: 'dateTime', default: '' },
1333
1383
  {
1334
1384
  displayName: 'Include Link to Workflow',
1335
1385
  name: 'includeLinkToWorkflow',
@@ -1337,14 +1387,6 @@ class Flowlu {
1337
1387
  default: false,
1338
1388
  description: 'Whether to append a "Generated via n8n: View Workflow" footer to the task description, linking back to this workflow',
1339
1389
  },
1340
- {
1341
- displayName: 'Owner Name or ID',
1342
- name: 'owner_id',
1343
- type: 'options',
1344
- typeOptions: { loadOptionsMethod: 'getUsers' },
1345
- description: 'The user who owns/created this task. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
1346
- default: '',
1347
- },
1348
1390
  {
1349
1391
  displayName: 'Parent Task ID',
1350
1392
  name: 'parent_id',
@@ -1354,27 +1396,7 @@ class Flowlu {
1354
1396
  },
1355
1397
  { displayName: 'Planned Cost', name: 'cost', type: 'number', default: 0, typeOptions: { minValue: 0 } },
1356
1398
  { displayName: 'Planned Income', name: 'price', type: 'number', default: 0, typeOptions: { minValue: 0 } },
1357
- {
1358
- displayName: 'Priority',
1359
- name: 'priority',
1360
- type: 'options',
1361
- options: [
1362
- { name: 'Low', value: 1 },
1363
- { name: 'Medium', value: 2 },
1364
- { name: 'High', value: 3 },
1365
- ],
1366
- default: 2,
1367
- },
1368
- {
1369
- displayName: 'Project Name or ID',
1370
- name: 'model_id',
1371
- type: 'options',
1372
- typeOptions: { loadOptionsMethod: 'getProjects' },
1373
- 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>.',
1374
- default: '',
1375
- },
1376
1399
  { displayName: 'Reviewed by Owner', name: 'task_checkbyowner', type: 'boolean', default: false },
1377
- { displayName: 'Start Date', name: 'plan_start_date', type: 'dateTime', default: '' },
1378
1400
  {
1379
1401
  displayName: 'Status',
1380
1402
  name: 'status',
@@ -1709,316 +1731,195 @@ class Flowlu {
1709
1731
  this.methods = {
1710
1732
  loadOptions: {
1711
1733
  async getUsers() {
1712
- try {
1713
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1714
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/core/user/list', apiKey, undefined, { 'filter[role_login]': '1' });
1715
- if (response?.response?.items) {
1716
- return response.response.items.map((user) => ({
1717
- name: user.name || user.email || `User ${user.id}`,
1718
- value: user.id.toString(),
1719
- }));
1720
- }
1721
- return [];
1722
- }
1723
- catch (error) {
1724
- return [];
1725
- }
1734
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1735
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/core/user/list', apiKey, {
1736
+ 'filter[role_login]': '1',
1737
+ });
1738
+ return items.map((user) => ({
1739
+ name: user.name || user.email || `User ${user.id}`,
1740
+ value: user.id.toString(),
1741
+ }));
1726
1742
  },
1727
1743
  async getAccounts() {
1728
- try {
1729
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1730
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/account/list', apiKey, undefined, { 'filter[type]': '1' });
1731
- if (response?.response?.items) {
1732
- return response.response.items.map((a) => ({
1733
- name: a.name || `Account ${a.id}`,
1734
- value: a.id.toString(),
1735
- }));
1736
- }
1737
- return [];
1738
- }
1739
- catch (error) {
1740
- return [];
1741
- }
1744
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1745
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/account/list', apiKey, {
1746
+ 'filter[type]': '1',
1747
+ });
1748
+ return items.map((a) => ({
1749
+ name: a.name || `Account ${a.id}`,
1750
+ value: a.id.toString(),
1751
+ }));
1742
1752
  },
1743
1753
  async getContacts() {
1744
- try {
1745
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1746
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/account/list', apiKey, undefined, { 'filter[type]': '2' });
1747
- if (response?.response?.items) {
1748
- return response.response.items.map((c) => ({
1749
- name: c.name || `Contact ${c.id}`,
1750
- value: c.id.toString(),
1751
- }));
1752
- }
1753
- return [];
1754
- }
1755
- catch (error) {
1756
- return [];
1757
- }
1754
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1755
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/account/list', apiKey, {
1756
+ 'filter[type]': '2',
1757
+ });
1758
+ return items.map((c) => ({
1759
+ name: c.name || `Contact ${c.id}`,
1760
+ value: c.id.toString(),
1761
+ }));
1758
1762
  },
1759
1763
  async getProjects() {
1760
- try {
1761
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1762
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/st/projects/list', apiKey, undefined, { 'filter[is_archive]': '0' });
1763
- if (response?.response?.items) {
1764
- return response.response.items.map((p) => ({
1765
- name: p.name || `Project ${p.id}`,
1766
- value: p.id.toString(),
1767
- }));
1768
- }
1769
- return [];
1770
- }
1771
- catch (error) {
1772
- return [];
1773
- }
1764
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1765
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/st/projects/list', apiKey, {
1766
+ 'filter[is_archive]': '0',
1767
+ });
1768
+ return items.map((p) => ({
1769
+ name: p.name || `Project ${p.id}`,
1770
+ value: p.id.toString(),
1771
+ }));
1774
1772
  },
1775
1773
  async getTaskWorkflows() {
1776
- try {
1777
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1778
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/task/workflows/list', apiKey);
1779
- if (response?.response?.items) {
1780
- return response.response.items.map((wf) => ({
1781
- name: wf.name || `Workflow ${wf.id}`,
1782
- value: wf.id.toString(),
1783
- }));
1784
- }
1785
- return [];
1786
- }
1787
- catch (error) {
1788
- return [];
1789
- }
1774
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1775
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/task/workflows/list', apiKey);
1776
+ return items.map((wf) => ({
1777
+ name: wf.name || `Workflow ${wf.id}`,
1778
+ value: wf.id.toString(),
1779
+ }));
1790
1780
  },
1791
1781
  async getTaskWorkflowStages() {
1792
- try {
1793
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1794
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/task/stages/list', apiKey);
1795
- if (response?.response?.items) {
1796
- return response.response.items.map((s) => ({
1797
- name: s.name || `Stage ${s.id}`,
1798
- value: s.id.toString(),
1799
- description: `Workflow ID: ${s.workflow_id}`,
1800
- }));
1801
- }
1802
- return [];
1803
- }
1804
- catch (error) {
1805
- return [];
1806
- }
1782
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1783
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/task/stages/list', apiKey);
1784
+ return items.map((s) => ({
1785
+ name: s.name || `Stage ${s.id}`,
1786
+ value: s.id.toString(),
1787
+ description: `Workflow ID: ${s.workflow_id}`,
1788
+ }));
1807
1789
  },
1808
1790
  async getPipelines() {
1809
- try {
1810
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1811
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/pipeline/list', apiKey);
1812
- if (response?.response?.items) {
1813
- return response.response.items.map((p) => ({
1814
- name: p.name || `Pipeline ${p.id}`,
1815
- value: p.id.toString(),
1816
- }));
1817
- }
1818
- return [];
1819
- }
1820
- catch (error) {
1821
- return [];
1822
- }
1791
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1792
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/pipeline/list', apiKey);
1793
+ return items.map((p) => ({
1794
+ name: p.name || `Pipeline ${p.id}`,
1795
+ value: p.id.toString(),
1796
+ }));
1823
1797
  },
1824
1798
  async getPipelineStages() {
1825
- try {
1826
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1827
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/pipeline_stage/list', apiKey);
1828
- if (response?.response?.items) {
1829
- return response.response.items.map((s) => ({
1830
- name: s.name || `Stage ${s.id}`,
1831
- value: s.id.toString(),
1832
- description: `Pipeline ID: ${s.pipeline_id}`,
1833
- }));
1834
- }
1835
- return [];
1836
- }
1837
- catch (error) {
1838
- return [];
1839
- }
1799
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1800
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/pipeline_stage/list', apiKey);
1801
+ return items.map((s) => ({
1802
+ name: s.name || `Stage ${s.id}`,
1803
+ value: s.id.toString(),
1804
+ description: `Pipeline ID: ${s.pipeline_id}`,
1805
+ }));
1840
1806
  },
1841
1807
  async getFilteredPipelineStages() {
1808
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1809
+ let pipelineId;
1842
1810
  try {
1843
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1844
- let pipelineId;
1845
- try {
1846
- pipelineId = this.getNodeParameter('opportunityFilterPipeline');
1847
- }
1848
- catch {
1849
- /* not set yet */
1850
- }
1851
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/pipeline_stage/list', apiKey);
1852
- if (response?.response?.items) {
1853
- let stages = response.response.items;
1854
- if (pipelineId) {
1855
- stages = stages.filter((s) => s.pipeline_id?.toString() === pipelineId);
1856
- }
1857
- return stages.map((s) => ({ name: s.name || `Stage ${s.id}`, value: s.id.toString() }));
1858
- }
1859
- return [];
1811
+ pipelineId = this.getNodeParameter('opportunityFilterPipeline');
1860
1812
  }
1861
- catch (error) {
1862
- return [];
1813
+ catch {
1814
+ /* not set yet */
1863
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() }));
1864
1821
  },
1865
1822
  async getAllCrmAccounts() {
1866
- try {
1867
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1868
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/account/list', apiKey);
1869
- if (response?.response?.items) {
1870
- return response.response.items.map((a) => {
1871
- const typeLabel = a.type === 1 ? '(Company)' : a.type === 2 ? '(Contact)' : '';
1872
- return { name: `${a.name || `#${a.id}`} ${typeLabel}`.trim(), value: a.id.toString() };
1873
- });
1874
- }
1875
- return [];
1876
- }
1877
- catch (error) {
1878
- return [];
1879
- }
1823
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1824
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/account/list', apiKey);
1825
+ return items.map((a) => {
1826
+ const typeLabel = a.type === 1 ? '(Company)' : a.type === 2 ? '(Contact)' : '';
1827
+ return { name: `${a.name || `#${a.id}`} ${typeLabel}`.trim(), value: a.id.toString() };
1828
+ });
1880
1829
  },
1881
1830
  async getOpportunitySources() {
1882
- try {
1883
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1884
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/source/list', apiKey);
1885
- if (response?.response?.items) {
1886
- return response.response.items.map((s) => ({
1887
- name: s.name || `Source ${s.id}`,
1888
- value: s.id.toString(),
1889
- }));
1890
- }
1891
- return [];
1892
- }
1893
- catch (error) {
1894
- return [];
1895
- }
1831
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1832
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/source/list', apiKey);
1833
+ return items.map((s) => ({
1834
+ name: s.name || `Source ${s.id}`,
1835
+ value: s.id.toString(),
1836
+ }));
1896
1837
  },
1897
1838
  async getPortfolios() {
1898
- try {
1899
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1900
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/st/portfolio/list', apiKey);
1901
- if (response?.response?.items) {
1902
- return response.response.items.map((p) => ({
1903
- name: p.name || `Portfolio ${p.id}`,
1904
- value: p.id.toString(),
1905
- }));
1906
- }
1907
- return [];
1908
- }
1909
- catch (error) {
1910
- return [];
1911
- }
1839
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1840
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/st/portfolio/list', apiKey);
1841
+ return items.map((p) => ({
1842
+ name: p.name || `Portfolio ${p.id}`,
1843
+ value: p.id.toString(),
1844
+ }));
1912
1845
  },
1913
1846
  async getProjectStages() {
1914
- try {
1915
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1916
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/st/stages/list', apiKey);
1917
- if (response?.response?.items) {
1918
- return response.response.items.map((s) => ({
1919
- name: s.name || `Stage ${s.id}`,
1920
- value: s.id.toString(),
1921
- }));
1922
- }
1923
- return [];
1924
- }
1925
- catch (error) {
1926
- return [];
1927
- }
1847
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1848
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/st/stages/list', apiKey);
1849
+ return items.map((s) => ({
1850
+ name: s.name || `Stage ${s.id}`,
1851
+ value: s.id.toString(),
1852
+ }));
1928
1853
  },
1929
1854
  async getRecordLists() {
1930
- try {
1931
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1932
- const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customlists/lists/list', apiKey);
1933
- if (response?.response?.items) {
1934
- return response.response.items.map((l) => ({
1935
- name: l.name || `List ${l.id}`,
1936
- value: l.id.toString(),
1937
- }));
1938
- }
1939
- return [];
1940
- }
1941
- catch (error) {
1942
- return [];
1943
- }
1855
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1856
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/customlists/lists/list', apiKey);
1857
+ return items.map((l) => ({
1858
+ name: l.name || `List ${l.id}`,
1859
+ value: l.id.toString(),
1860
+ }));
1944
1861
  },
1945
1862
  async getRecordListFields() {
1863
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1864
+ // Get the selected list ID from the current node parameters
1865
+ let listId;
1946
1866
  try {
1947
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
1948
- // Get the selected list ID from the current node parameters
1949
- let listId;
1950
- try {
1951
- listId = this.getNodeParameter('recordListId');
1952
- }
1953
- catch {
1954
- return [];
1955
- }
1956
- if (!listId)
1957
- return [];
1958
- // Find the fieldset for this list (customlists/items with group_id matching list_id)
1959
- const fieldsetsResponse = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customfields/fieldsets/list', apiKey);
1960
- const fieldsetIds = [];
1961
- if (fieldsetsResponse?.response?.items) {
1962
- for (const fs of fieldsetsResponse.response.items) {
1963
- if (fs.module === 'customlists' && fs.model === 'items' && fs.group_id?.toString() === listId) {
1964
- fieldsetIds.push(fs.id.toString());
1965
- }
1966
- }
1967
- }
1968
- if (fieldsetIds.length === 0)
1969
- return [];
1970
- // Get fields for those fieldsets
1971
- const fieldsResponse = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customfields/fields/list', apiKey);
1972
- if (fieldsResponse?.response?.items) {
1973
- // Also fetch all fieldsets so we can resolve custom list link names
1974
- const allFieldsets = fieldsetsResponse?.response?.items || [];
1975
- return fieldsResponse.response.items
1976
- .filter((f) => fieldsetIds.includes(f.fieldset_id?.toString()))
1977
- .filter((f) => f.active !== 0)
1978
- .map((f) => {
1979
- const cfKey = f.api_use_alias && f.alias ? `cf_${f.alias}` : `cf_${f.id}`;
1980
- let label = f.name || cfKey;
1981
- let desc = '';
1982
- // Detect link fields (model.any) and show what they link to
1983
- if (f.type === 'model.any' && f.module && f.model) {
1984
- const linkTargetMap = {
1985
- 'st/project': 'Project',
1986
- 'crm/company': 'Company',
1987
- 'crm/contact': 'Contact',
1988
- 'crm/client': 'Account',
1989
- 'crm/leads': 'Opportunity',
1990
- 'system/user': 'User',
1991
- };
1992
- const key = `${f.module}/${f.model}`;
1993
- if (linkTargetMap[key]) {
1994
- label = `${f.name} → ${linkTargetMap[key]}`;
1995
- desc = `Enter the ${linkTargetMap[key]} ID`;
1996
- }
1997
- else if (f.module === 'customlists' && f.model === 'items' && f.group_id) {
1998
- // Link to another custom list - find its name
1999
- const targetList = allFieldsets.find((fs) => fs.module === 'customlists' &&
2000
- fs.model === 'items' &&
2001
- fs.group_id?.toString() === f.group_id.toString());
2002
- const listName = targetList?.name || `List #${f.group_id}`;
2003
- label = `${f.name} → ${listName}`;
2004
- desc = `Enter the Record ID from "${listName}"`;
2005
- }
2006
- else {
2007
- label = `${f.name} → ${f.module}/${f.model}`;
2008
- desc = `Enter the linked record ID`;
2009
- }
2010
- }
2011
- const option = { name: label, value: cfKey };
2012
- if (desc)
2013
- option.description = desc;
2014
- return option;
2015
- });
2016
- }
2017
- return [];
1867
+ listId = this.getNodeParameter('recordListId');
2018
1868
  }
2019
- catch (error) {
1869
+ catch {
2020
1870
  return [];
2021
1871
  }
1872
+ if (!listId)
1873
+ return [];
1874
+ // Find the fieldset for this list (customlists/items with group_id matching list_id)
1875
+ const allFieldsets = await flowluListAll.call(this, baseUrl, '/api/v1/module/customfields/fieldsets/list', apiKey);
1876
+ const fieldsetIds = new Set(allFieldsets
1877
+ .filter((fs) => fs.module === 'customlists' && fs.model === 'items' && fs.group_id?.toString() === listId)
1878
+ .map((fs) => fs.id.toString()));
1879
+ if (fieldsetIds.size === 0)
1880
+ return [];
1881
+ const allFields = await flowluListAll.call(this, baseUrl, '/api/v1/module/customfields/fields/list', apiKey);
1882
+ return allFields
1883
+ .filter((f) => fieldsetIds.has(f.fieldset_id?.toString()))
1884
+ .filter((f) => f.active !== 0)
1885
+ .map((f) => {
1886
+ const cfKey = f.api_use_alias && f.alias ? `cf_${f.alias}` : `cf_${f.id}`;
1887
+ let label = f.name || cfKey;
1888
+ let desc = '';
1889
+ // Detect link fields (model.any) and show what they link to
1890
+ if (f.type === 'model.any' && f.module && f.model) {
1891
+ const linkTargetMap = {
1892
+ 'st/project': 'Project',
1893
+ 'crm/company': 'Company',
1894
+ 'crm/contact': 'Contact',
1895
+ 'crm/client': 'Account',
1896
+ 'crm/leads': 'Opportunity',
1897
+ 'system/user': 'User',
1898
+ };
1899
+ const key = `${f.module}/${f.model}`;
1900
+ if (linkTargetMap[key]) {
1901
+ label = `${f.name} → ${linkTargetMap[key]}`;
1902
+ desc = `Enter the ${linkTargetMap[key]} ID`;
1903
+ }
1904
+ else if (f.module === 'customlists' && f.model === 'items' && f.group_id) {
1905
+ // Link to another custom list - find its name
1906
+ const targetList = allFieldsets.find((fs) => fs.module === 'customlists' &&
1907
+ fs.model === 'items' &&
1908
+ fs.group_id?.toString() === f.group_id.toString());
1909
+ const listName = targetList?.name || `List #${f.group_id}`;
1910
+ label = `${f.name} → ${listName}`;
1911
+ desc = `Enter the Record ID from "${listName}"`;
1912
+ }
1913
+ else {
1914
+ label = `${f.name} → ${f.module}/${f.model}`;
1915
+ desc = `Enter the linked record ID`;
1916
+ }
1917
+ }
1918
+ const option = { name: label, value: cfKey };
1919
+ if (desc)
1920
+ option.description = desc;
1921
+ return option;
1922
+ });
2022
1923
  },
2023
1924
  async getTaskCustomFields() {
2024
1925
  return loadCustomFieldsForEntity(this, 'task', 'task');
@@ -2059,8 +1960,7 @@ class Flowlu {
2059
1960
  if (!listId)
2060
1961
  return { fields: [] };
2061
1962
  // Get fieldsets to find this list's fieldset
2062
- const fieldsetsResponse = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customfields/fieldsets/list', apiKey);
2063
- const allFieldsets = fieldsetsResponse?.response?.items || [];
1963
+ const allFieldsets = await flowluListAll.call(this, baseUrl, '/api/v1/module/customfields/fieldsets/list', apiKey);
2064
1964
  const fieldsetIds = [];
2065
1965
  for (const fs of allFieldsets) {
2066
1966
  if (fs.module === 'customlists' && fs.model === 'items' && fs.group_id?.toString() === listId) {
@@ -2070,9 +1970,7 @@ class Flowlu {
2070
1970
  if (fieldsetIds.length === 0)
2071
1971
  return { fields: [] };
2072
1972
  // Get all custom fields
2073
- const fieldsResponse = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customfields/fields/list', apiKey);
2074
- if (!fieldsResponse?.response?.items)
2075
- return { fields: [] };
1973
+ const allRecordFields = await flowluListAll.call(this, baseUrl, '/api/v1/module/customfields/fields/list', apiKey);
2076
1974
  // Map of linked entity endpoints for fetching dropdown options
2077
1975
  const linkEndpoints = {
2078
1976
  'st/project': '/api/v1/module/st/projects/list',
@@ -2092,7 +1990,7 @@ class Flowlu {
2092
1990
  };
2093
1991
  // Pre-fetch linked record options for all link field types we encounter
2094
1992
  const linkOptionsCache = {};
2095
- const filteredFields = fieldsResponse.response.items
1993
+ const filteredFields = allRecordFields
2096
1994
  .filter((f) => fieldsetIds.includes(f.fieldset_id?.toString()))
2097
1995
  .filter((f) => f.active !== 0);
2098
1996
  for (const f of filteredFields) {
@@ -2109,8 +2007,8 @@ class Flowlu {
2109
2007
  qs['filter[type]'] = '1';
2110
2008
  if (key === 'system/user')
2111
2009
  qs['filter[role_login]'] = '1';
2112
- const resp = await flowluApiRequest.call(this, 'GET', baseUrl, linkEndpoints[key], apiKey, undefined, qs);
2113
- linkOptionsCache[key] = (resp?.response?.items || []).map((item) => ({
2010
+ const linked = await flowluListAll.call(this, baseUrl, linkEndpoints[key], apiKey, qs);
2011
+ linkOptionsCache[key] = linked.map((item) => ({
2114
2012
  name: item.name || item.email || `#${item.id}`,
2115
2013
  value: item.id,
2116
2014
  }));
@@ -2124,12 +2022,10 @@ class Flowlu {
2124
2022
  const clKey = `customlists/${f.group_id}`;
2125
2023
  if (!linkOptionsCache[clKey]) {
2126
2024
  try {
2127
- const resp = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customlists/items/list', apiKey, undefined, {
2128
- 'filter[list_id]': f.group_id.toString(),
2129
- });
2025
+ const linked = await flowluListAll.call(this, baseUrl, '/api/v1/module/customlists/items/list', apiKey, { 'filter[list_id]': f.group_id.toString() });
2130
2026
  // Find the label field for this list
2131
2027
  const labelFieldId = f.group_label_field_id;
2132
- linkOptionsCache[clKey] = (resp?.response?.items || []).map((item) => {
2028
+ linkOptionsCache[clKey] = linked.map((item) => {
2133
2029
  const label = labelFieldId ? item[`cf_${labelFieldId}`] || `#${item.id}` : `#${item.id}`;
2134
2030
  return { name: label, value: item.id };
2135
2031
  });
@@ -2205,7 +2101,9 @@ class Flowlu {
2205
2101
  return { fields };
2206
2102
  }
2207
2103
  catch (error) {
2208
- return { fields: [] };
2104
+ if (error instanceof n8n_workflow_1.NodeOperationError)
2105
+ throw error;
2106
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Could not load Flowlu record-list fields: ${error.message}. If this is a rate limit, wait a moment and click Retry.`);
2209
2107
  }
2210
2108
  },
2211
2109
  },
@@ -2218,7 +2116,7 @@ class Flowlu {
2218
2116
  const operation = this.getNodeParameter('operation', 0);
2219
2117
  for (let i = 0; i < items.length; i++) {
2220
2118
  try {
2221
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
2119
+ const { baseUrl, apiKey, defaultOwnerId } = await getFlowluCredentials(this);
2222
2120
  let responseData;
2223
2121
  // ============================
2224
2122
  // CONTACT
@@ -2640,19 +2538,27 @@ class Flowlu {
2640
2538
  const additional = this.getNodeParameter('taskAdditionalFields', i);
2641
2539
  body.workflow_id = additional.workflow_id || '1';
2642
2540
  body.workflow_stage_id = additional.workflow_stage_id || '1';
2643
- if (additional.owner_id)
2644
- body.owner_id = additional.owner_id;
2645
- if (additional.contact_id)
2646
- body.crm_account_id = parseInt(additional.contact_id, 10);
2647
- if (additional.priority !== undefined)
2648
- body.priority = additional.priority;
2541
+ // Owner/Priority/Contact/Project/Start/End moved to top-level fields; fall back to
2542
+ // Additional Fields so task-create nodes built before the move keep working.
2543
+ const ownerId = this.getNodeParameter('owner_id', i, '') || additional.owner_id || defaultOwnerId;
2544
+ if (ownerId)
2545
+ body.owner_id = ownerId;
2546
+ const contactId = this.getNodeParameter('contact_id', i, '') || additional.contact_id;
2547
+ if (contactId)
2548
+ body.crm_account_id = parseInt(contactId, 10);
2549
+ body.priority =
2550
+ additional.priority !== undefined
2551
+ ? additional.priority
2552
+ : this.getNodeParameter('priority', i, 2);
2649
2553
  if (additional.status !== undefined)
2650
2554
  body.status = additional.status;
2651
- if (additional.plan_start_date) {
2652
- body.plan_start_date = new Date(additional.plan_start_date).toISOString().split('T')[0];
2555
+ const planStartDate = this.getNodeParameter('plan_start_date', i, '') || additional.plan_start_date;
2556
+ if (planStartDate) {
2557
+ body.plan_start_date = new Date(planStartDate).toISOString().split('T')[0];
2653
2558
  }
2654
- if (additional.deadline) {
2655
- body.deadline = new Date(additional.deadline).toISOString().split('T')[0];
2559
+ const deadline = this.getNodeParameter('deadline', i, '') || additional.deadline;
2560
+ if (deadline) {
2561
+ body.deadline = new Date(deadline).toISOString().split('T')[0];
2656
2562
  }
2657
2563
  if (additional.time_estimate && additional.time_estimate > 0) {
2658
2564
  body.time_estimate = Math.round(additional.time_estimate * 60);
@@ -2670,8 +2576,9 @@ class Flowlu {
2670
2576
  if (additional.parent_id && additional.parent_id > 0) {
2671
2577
  body.parent_id = additional.parent_id;
2672
2578
  }
2673
- if (additional.model_id) {
2674
- body.model_id = additional.model_id;
2579
+ const modelId = this.getNodeParameter('model_id', i, '') || additional.model_id;
2580
+ if (modelId) {
2581
+ body.model_id = modelId;
2675
2582
  body.model = 'project';
2676
2583
  body.module = 'st';
2677
2584
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@belmontdigitalmarketing/n8n-nodes-flowlu",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "n8n community node for the Flowlu CRM, project management, and task API",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",