@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
|
|
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
|
-
|
|
49
|
+
lastMessage = String(response?.error ?? 'Flowlu request limit exceeded');
|
|
49
50
|
}
|
|
50
51
|
catch (error) {
|
|
51
|
-
if (!isFlowluRateLimit(error))
|
|
52
|
-
throw error;
|
|
53
|
-
|
|
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
|
|
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, {
|
|
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
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
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
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
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
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
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
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
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
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
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
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
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
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
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
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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
|
-
|
|
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
|
|
1862
|
-
|
|
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
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
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
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
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
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
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
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
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
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
2113
|
-
linkOptionsCache[key] =
|
|
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
|
|
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] =
|
|
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
|
-
|
|
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
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
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
|
-
|
|
2652
|
-
|
|
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
|
-
|
|
2655
|
-
|
|
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
|
-
|
|
2674
|
-
|
|
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