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