@belmontdigitalmarketing/n8n-nodes-flowlu 0.2.2 → 0.3.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.
@@ -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 };
@@ -38,19 +39,20 @@ function isFlowluRateLimit(value) {
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
41
  async function flowluApiGetWithRetry(baseUrl, endpoint, apiKey, queryParams, maxRetries = 3) {
41
- let lastError = new Error('Flowlu request failed');
42
+ let lastMessage = 'Flowlu request failed';
42
43
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
43
44
  try {
44
45
  const response = await flowluApiRequest.call(this, 'GET', baseUrl, endpoint, apiKey, undefined, queryParams);
45
46
  if (!isFlowluRateLimit(response?.error ?? response)) {
46
47
  return response;
47
48
  }
48
- lastError = new Error(String(response?.error ?? 'Flowlu request limit exceeded'));
49
+ lastMessage = String(response?.error ?? 'Flowlu request limit exceeded');
49
50
  }
50
51
  catch (error) {
51
- if (!isFlowluRateLimit(error))
52
- throw error;
53
- lastError = error;
52
+ if (!isFlowluRateLimit(error)) {
53
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Flowlu API error: ${error.message}`);
54
+ }
55
+ lastMessage = error.message || 'Flowlu request limit exceeded';
54
56
  }
55
57
  // linear backoff before the next attempt (no wait after the final one)
56
58
  if (attempt < maxRetries) {
@@ -59,15 +61,18 @@ async function flowluApiGetWithRetry(baseUrl, endpoint, apiKey, queryParams, max
59
61
  });
60
62
  }
61
63
  }
62
- throw lastError instanceof Error ? lastError : new Error('Flowlu request failed');
64
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Flowlu API rate limit reached while loading data (${lastMessage}). Wait a moment and click Retry.`);
63
65
  }
64
66
  // Fetch every page of a Flowlu list endpoint. The API caps responses at 50 items
65
67
  // per page, so a single call silently truncates larger result sets.
66
- async function flowluListAll(baseUrl, endpoint, apiKey) {
68
+ async function flowluListAll(baseUrl, endpoint, apiKey, queryParams) {
67
69
  const items = [];
68
70
  const maxPages = 100; // safety bound
69
71
  for (let page = 1; page <= maxPages; page++) {
70
- const response = await flowluApiGetWithRetry.call(this, baseUrl, endpoint, apiKey, { page: String(page) });
72
+ const response = await flowluApiGetWithRetry.call(this, baseUrl, endpoint, apiKey, {
73
+ ...(queryParams ?? {}),
74
+ page: String(page),
75
+ });
71
76
  const pageItems = response?.response?.items;
72
77
  if (!Array.isArray(pageItems) || pageItems.length === 0)
73
78
  break;
@@ -1311,6 +1316,59 @@ class Flowlu {
1311
1316
  default: '',
1312
1317
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1313
1318
  },
1319
+ {
1320
+ displayName: 'Owner Name or ID',
1321
+ 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>.',
1326
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1327
+ },
1328
+ {
1329
+ displayName: 'Priority',
1330
+ name: 'priority',
1331
+ type: 'options',
1332
+ options: [
1333
+ { name: 'Low', value: 1 },
1334
+ { name: 'Medium', value: 2 },
1335
+ { name: 'High', value: 3 },
1336
+ ],
1337
+ default: 2,
1338
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1339
+ },
1340
+ {
1341
+ displayName: 'Contact Name or ID',
1342
+ 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>.',
1347
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1348
+ },
1349
+ {
1350
+ displayName: 'Project Name or ID',
1351
+ 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>.',
1356
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1357
+ },
1358
+ {
1359
+ displayName: 'Start Date',
1360
+ name: 'plan_start_date',
1361
+ type: 'dateTime',
1362
+ default: '',
1363
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1364
+ },
1365
+ {
1366
+ displayName: 'End Date',
1367
+ name: 'deadline',
1368
+ type: 'dateTime',
1369
+ default: '',
1370
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1371
+ },
1314
1372
  // Task Create: Additional Fields
1315
1373
  {
1316
1374
  displayName: 'Additional Fields',
@@ -1321,15 +1379,6 @@ class Flowlu {
1321
1379
  displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1322
1380
  options: [
1323
1381
  { 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
1382
  {
1334
1383
  displayName: 'Include Link to Workflow',
1335
1384
  name: 'includeLinkToWorkflow',
@@ -1337,14 +1386,6 @@ class Flowlu {
1337
1386
  default: false,
1338
1387
  description: 'Whether to append a "Generated via n8n: View Workflow" footer to the task description, linking back to this workflow',
1339
1388
  },
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
1389
  {
1349
1390
  displayName: 'Parent Task ID',
1350
1391
  name: 'parent_id',
@@ -1354,27 +1395,7 @@ class Flowlu {
1354
1395
  },
1355
1396
  { displayName: 'Planned Cost', name: 'cost', type: 'number', default: 0, typeOptions: { minValue: 0 } },
1356
1397
  { 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
1398
  { displayName: 'Reviewed by Owner', name: 'task_checkbyowner', type: 'boolean', default: false },
1377
- { displayName: 'Start Date', name: 'plan_start_date', type: 'dateTime', default: '' },
1378
1399
  {
1379
1400
  displayName: 'Status',
1380
1401
  name: 'status',
@@ -1709,316 +1730,195 @@ class Flowlu {
1709
1730
  this.methods = {
1710
1731
  loadOptions: {
1711
1732
  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
- }
1733
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1734
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/core/user/list', apiKey, {
1735
+ 'filter[role_login]': '1',
1736
+ });
1737
+ return items.map((user) => ({
1738
+ name: user.name || user.email || `User ${user.id}`,
1739
+ value: user.id.toString(),
1740
+ }));
1726
1741
  },
1727
1742
  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
- }
1743
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1744
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/account/list', apiKey, {
1745
+ 'filter[type]': '1',
1746
+ });
1747
+ return items.map((a) => ({
1748
+ name: a.name || `Account ${a.id}`,
1749
+ value: a.id.toString(),
1750
+ }));
1742
1751
  },
1743
1752
  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
- }
1753
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1754
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/account/list', apiKey, {
1755
+ 'filter[type]': '2',
1756
+ });
1757
+ return items.map((c) => ({
1758
+ name: c.name || `Contact ${c.id}`,
1759
+ value: c.id.toString(),
1760
+ }));
1758
1761
  },
1759
1762
  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
- }
1763
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1764
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/st/projects/list', apiKey, {
1765
+ 'filter[is_archive]': '0',
1766
+ });
1767
+ return items.map((p) => ({
1768
+ name: p.name || `Project ${p.id}`,
1769
+ value: p.id.toString(),
1770
+ }));
1774
1771
  },
1775
1772
  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
- }
1773
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1774
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/task/workflows/list', apiKey);
1775
+ return items.map((wf) => ({
1776
+ name: wf.name || `Workflow ${wf.id}`,
1777
+ value: wf.id.toString(),
1778
+ }));
1790
1779
  },
1791
1780
  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
- }
1781
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1782
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/task/stages/list', apiKey);
1783
+ return items.map((s) => ({
1784
+ name: s.name || `Stage ${s.id}`,
1785
+ value: s.id.toString(),
1786
+ description: `Workflow ID: ${s.workflow_id}`,
1787
+ }));
1807
1788
  },
1808
1789
  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
- }
1790
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1791
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/pipeline/list', apiKey);
1792
+ return items.map((p) => ({
1793
+ name: p.name || `Pipeline ${p.id}`,
1794
+ value: p.id.toString(),
1795
+ }));
1823
1796
  },
1824
1797
  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
- }
1798
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1799
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/pipeline_stage/list', apiKey);
1800
+ return items.map((s) => ({
1801
+ name: s.name || `Stage ${s.id}`,
1802
+ value: s.id.toString(),
1803
+ description: `Pipeline ID: ${s.pipeline_id}`,
1804
+ }));
1840
1805
  },
1841
1806
  async getFilteredPipelineStages() {
1807
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1808
+ let pipelineId;
1842
1809
  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 [];
1810
+ pipelineId = this.getNodeParameter('opportunityFilterPipeline');
1860
1811
  }
1861
- catch (error) {
1862
- return [];
1812
+ catch {
1813
+ /* not set yet */
1863
1814
  }
1815
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/pipeline_stage/list', apiKey);
1816
+ const stages = pipelineId
1817
+ ? items.filter((s) => s.pipeline_id?.toString() === pipelineId)
1818
+ : items;
1819
+ return stages.map((s) => ({ name: s.name || `Stage ${s.id}`, value: s.id.toString() }));
1864
1820
  },
1865
1821
  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
- }
1822
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1823
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/account/list', apiKey);
1824
+ return items.map((a) => {
1825
+ const typeLabel = a.type === 1 ? '(Company)' : a.type === 2 ? '(Contact)' : '';
1826
+ return { name: `${a.name || `#${a.id}`} ${typeLabel}`.trim(), value: a.id.toString() };
1827
+ });
1880
1828
  },
1881
1829
  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
- }
1830
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1831
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/crm/source/list', apiKey);
1832
+ return items.map((s) => ({
1833
+ name: s.name || `Source ${s.id}`,
1834
+ value: s.id.toString(),
1835
+ }));
1896
1836
  },
1897
1837
  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
- }
1838
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1839
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/st/portfolio/list', apiKey);
1840
+ return items.map((p) => ({
1841
+ name: p.name || `Portfolio ${p.id}`,
1842
+ value: p.id.toString(),
1843
+ }));
1912
1844
  },
1913
1845
  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
- }
1846
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1847
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/st/stages/list', apiKey);
1848
+ return items.map((s) => ({
1849
+ name: s.name || `Stage ${s.id}`,
1850
+ value: s.id.toString(),
1851
+ }));
1928
1852
  },
1929
1853
  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
- }
1854
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1855
+ const items = await flowluListAll.call(this, baseUrl, '/api/v1/module/customlists/lists/list', apiKey);
1856
+ return items.map((l) => ({
1857
+ name: l.name || `List ${l.id}`,
1858
+ value: l.id.toString(),
1859
+ }));
1944
1860
  },
1945
1861
  async getRecordListFields() {
1862
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1863
+ // Get the selected list ID from the current node parameters
1864
+ let listId;
1946
1865
  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 [];
1866
+ listId = this.getNodeParameter('recordListId');
2018
1867
  }
2019
- catch (error) {
1868
+ catch {
2020
1869
  return [];
2021
1870
  }
1871
+ if (!listId)
1872
+ return [];
1873
+ // Find the fieldset for this list (customlists/items with group_id matching list_id)
1874
+ const allFieldsets = await flowluListAll.call(this, baseUrl, '/api/v1/module/customfields/fieldsets/list', apiKey);
1875
+ const fieldsetIds = new Set(allFieldsets
1876
+ .filter((fs) => fs.module === 'customlists' && fs.model === 'items' && fs.group_id?.toString() === listId)
1877
+ .map((fs) => fs.id.toString()));
1878
+ if (fieldsetIds.size === 0)
1879
+ return [];
1880
+ const allFields = await flowluListAll.call(this, baseUrl, '/api/v1/module/customfields/fields/list', apiKey);
1881
+ return allFields
1882
+ .filter((f) => fieldsetIds.has(f.fieldset_id?.toString()))
1883
+ .filter((f) => f.active !== 0)
1884
+ .map((f) => {
1885
+ const cfKey = f.api_use_alias && f.alias ? `cf_${f.alias}` : `cf_${f.id}`;
1886
+ let label = f.name || cfKey;
1887
+ let desc = '';
1888
+ // Detect link fields (model.any) and show what they link to
1889
+ if (f.type === 'model.any' && f.module && f.model) {
1890
+ const linkTargetMap = {
1891
+ 'st/project': 'Project',
1892
+ 'crm/company': 'Company',
1893
+ 'crm/contact': 'Contact',
1894
+ 'crm/client': 'Account',
1895
+ 'crm/leads': 'Opportunity',
1896
+ 'system/user': 'User',
1897
+ };
1898
+ const key = `${f.module}/${f.model}`;
1899
+ if (linkTargetMap[key]) {
1900
+ label = `${f.name} → ${linkTargetMap[key]}`;
1901
+ desc = `Enter the ${linkTargetMap[key]} ID`;
1902
+ }
1903
+ else if (f.module === 'customlists' && f.model === 'items' && f.group_id) {
1904
+ // Link to another custom list - find its name
1905
+ const targetList = allFieldsets.find((fs) => fs.module === 'customlists' &&
1906
+ fs.model === 'items' &&
1907
+ fs.group_id?.toString() === f.group_id.toString());
1908
+ const listName = targetList?.name || `List #${f.group_id}`;
1909
+ label = `${f.name} → ${listName}`;
1910
+ desc = `Enter the Record ID from "${listName}"`;
1911
+ }
1912
+ else {
1913
+ label = `${f.name} → ${f.module}/${f.model}`;
1914
+ desc = `Enter the linked record ID`;
1915
+ }
1916
+ }
1917
+ const option = { name: label, value: cfKey };
1918
+ if (desc)
1919
+ option.description = desc;
1920
+ return option;
1921
+ });
2022
1922
  },
2023
1923
  async getTaskCustomFields() {
2024
1924
  return loadCustomFieldsForEntity(this, 'task', 'task');
@@ -2059,8 +1959,7 @@ class Flowlu {
2059
1959
  if (!listId)
2060
1960
  return { fields: [] };
2061
1961
  // 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 || [];
1962
+ const allFieldsets = await flowluListAll.call(this, baseUrl, '/api/v1/module/customfields/fieldsets/list', apiKey);
2064
1963
  const fieldsetIds = [];
2065
1964
  for (const fs of allFieldsets) {
2066
1965
  if (fs.module === 'customlists' && fs.model === 'items' && fs.group_id?.toString() === listId) {
@@ -2070,9 +1969,7 @@ class Flowlu {
2070
1969
  if (fieldsetIds.length === 0)
2071
1970
  return { fields: [] };
2072
1971
  // 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: [] };
1972
+ const allRecordFields = await flowluListAll.call(this, baseUrl, '/api/v1/module/customfields/fields/list', apiKey);
2076
1973
  // Map of linked entity endpoints for fetching dropdown options
2077
1974
  const linkEndpoints = {
2078
1975
  'st/project': '/api/v1/module/st/projects/list',
@@ -2092,7 +1989,7 @@ class Flowlu {
2092
1989
  };
2093
1990
  // Pre-fetch linked record options for all link field types we encounter
2094
1991
  const linkOptionsCache = {};
2095
- const filteredFields = fieldsResponse.response.items
1992
+ const filteredFields = allRecordFields
2096
1993
  .filter((f) => fieldsetIds.includes(f.fieldset_id?.toString()))
2097
1994
  .filter((f) => f.active !== 0);
2098
1995
  for (const f of filteredFields) {
@@ -2109,8 +2006,8 @@ class Flowlu {
2109
2006
  qs['filter[type]'] = '1';
2110
2007
  if (key === 'system/user')
2111
2008
  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) => ({
2009
+ const linked = await flowluListAll.call(this, baseUrl, linkEndpoints[key], apiKey, qs);
2010
+ linkOptionsCache[key] = linked.map((item) => ({
2114
2011
  name: item.name || item.email || `#${item.id}`,
2115
2012
  value: item.id,
2116
2013
  }));
@@ -2124,12 +2021,10 @@ class Flowlu {
2124
2021
  const clKey = `customlists/${f.group_id}`;
2125
2022
  if (!linkOptionsCache[clKey]) {
2126
2023
  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
- });
2024
+ const linked = await flowluListAll.call(this, baseUrl, '/api/v1/module/customlists/items/list', apiKey, { 'filter[list_id]': f.group_id.toString() });
2130
2025
  // Find the label field for this list
2131
2026
  const labelFieldId = f.group_label_field_id;
2132
- linkOptionsCache[clKey] = (resp?.response?.items || []).map((item) => {
2027
+ linkOptionsCache[clKey] = linked.map((item) => {
2133
2028
  const label = labelFieldId ? item[`cf_${labelFieldId}`] || `#${item.id}` : `#${item.id}`;
2134
2029
  return { name: label, value: item.id };
2135
2030
  });
@@ -2205,7 +2100,9 @@ class Flowlu {
2205
2100
  return { fields };
2206
2101
  }
2207
2102
  catch (error) {
2208
- return { fields: [] };
2103
+ if (error instanceof n8n_workflow_1.NodeOperationError)
2104
+ throw error;
2105
+ 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
2106
  }
2210
2107
  },
2211
2108
  },
@@ -2218,7 +2115,7 @@ class Flowlu {
2218
2115
  const operation = this.getNodeParameter('operation', 0);
2219
2116
  for (let i = 0; i < items.length; i++) {
2220
2117
  try {
2221
- const { baseUrl, apiKey } = await getFlowluCredentials(this);
2118
+ const { baseUrl, apiKey, defaultOwnerId } = await getFlowluCredentials(this);
2222
2119
  let responseData;
2223
2120
  // ============================
2224
2121
  // CONTACT
@@ -2640,19 +2537,27 @@ class Flowlu {
2640
2537
  const additional = this.getNodeParameter('taskAdditionalFields', i);
2641
2538
  body.workflow_id = additional.workflow_id || '1';
2642
2539
  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;
2540
+ // Owner/Priority/Contact/Project/Start/End moved to top-level fields; fall back to
2541
+ // Additional Fields so task-create nodes built before the move keep working.
2542
+ const ownerId = this.getNodeParameter('owner_id', i, '') || additional.owner_id || defaultOwnerId;
2543
+ if (ownerId)
2544
+ body.owner_id = ownerId;
2545
+ const contactId = this.getNodeParameter('contact_id', i, '') || additional.contact_id;
2546
+ if (contactId)
2547
+ body.crm_account_id = parseInt(contactId, 10);
2548
+ body.priority =
2549
+ additional.priority !== undefined
2550
+ ? additional.priority
2551
+ : this.getNodeParameter('priority', i, 2);
2649
2552
  if (additional.status !== undefined)
2650
2553
  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];
2554
+ const planStartDate = this.getNodeParameter('plan_start_date', i, '') || additional.plan_start_date;
2555
+ if (planStartDate) {
2556
+ body.plan_start_date = new Date(planStartDate).toISOString().split('T')[0];
2653
2557
  }
2654
- if (additional.deadline) {
2655
- body.deadline = new Date(additional.deadline).toISOString().split('T')[0];
2558
+ const deadline = this.getNodeParameter('deadline', i, '') || additional.deadline;
2559
+ if (deadline) {
2560
+ body.deadline = new Date(deadline).toISOString().split('T')[0];
2656
2561
  }
2657
2562
  if (additional.time_estimate && additional.time_estimate > 0) {
2658
2563
  body.time_estimate = Math.round(additional.time_estimate * 60);
@@ -2670,8 +2575,9 @@ class Flowlu {
2670
2575
  if (additional.parent_id && additional.parent_id > 0) {
2671
2576
  body.parent_id = additional.parent_id;
2672
2577
  }
2673
- if (additional.model_id) {
2674
- body.model_id = additional.model_id;
2578
+ const modelId = this.getNodeParameter('model_id', i, '') || additional.model_id;
2579
+ if (modelId) {
2580
+ body.model_id = modelId;
2675
2581
  body.model = 'project';
2676
2582
  body.module = 'st';
2677
2583
  }
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.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",