@belmontdigitalmarketing/n8n-nodes-flowlu 0.2.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.
@@ -0,0 +1,2397 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Flowlu = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ // ============================================================
6
+ // Shared Helpers
7
+ // ============================================================
8
+ async function getFlowluCredentials(context) {
9
+ const credentials = await context.getCredentials('flowluApi');
10
+ const subdomain = credentials.subdomain;
11
+ const apiKey = credentials.apiKey;
12
+ const baseUrl = subdomain.endsWith('.flowlu.com')
13
+ ? `https://${subdomain}`
14
+ : `https://${subdomain}.flowlu.com`;
15
+ return { baseUrl, apiKey };
16
+ }
17
+ async function flowluApiRequest(method, baseUrl, endpoint, apiKey, body, queryParams) {
18
+ const qs = { api_key: apiKey };
19
+ if (queryParams) {
20
+ Object.assign(qs, queryParams);
21
+ }
22
+ const requestOptions = {
23
+ method,
24
+ uri: `${baseUrl}${endpoint}`,
25
+ qs,
26
+ json: false,
27
+ };
28
+ if (method === 'POST' && body) {
29
+ requestOptions.form = body;
30
+ }
31
+ const response = await this.helpers.request(requestOptions);
32
+ return typeof response === 'string' ? JSON.parse(response) : response;
33
+ }
34
+ function appendWorkflowFooter(target, contentKey = 'description') {
35
+ const { id } = this.getWorkflow();
36
+ const footer = `<hr><em>Generated via n8n:</em> <a href="${this.getInstanceBaseUrl()}workflow/${id}">View Workflow</a>`;
37
+ const existing = target[contentKey];
38
+ target[contentKey] = existing ? `${existing}<br><br>${footer}` : footer;
39
+ }
40
+ function applyResourceMapperFields(context, body, paramName, itemIndex) {
41
+ try {
42
+ const mappingValues = context.getNodeParameter(`${paramName}.value`, itemIndex);
43
+ if (mappingValues) {
44
+ for (const [key, value] of Object.entries(mappingValues)) {
45
+ if (value !== undefined && value !== null && value !== '') {
46
+ body[key] = value;
47
+ }
48
+ }
49
+ }
50
+ }
51
+ catch {
52
+ // No custom fields set - ignore
53
+ }
54
+ }
55
+ // Helper to load custom fields by module/model, with fallback approach
56
+ async function loadCustomFieldsForEntity(context, moduleFilter, modelFilter) {
57
+ try {
58
+ const { baseUrl, apiKey } = await getFlowluCredentials(context);
59
+ // Strategy 1: Get all fieldsets and match by module/model
60
+ const fieldsetsResponse = await flowluApiRequest.call(context, 'GET', baseUrl, '/api/v1/module/customfields/fieldsets/list', apiKey);
61
+ const fieldsetIds = [];
62
+ if (fieldsetsResponse?.response?.items) {
63
+ for (const fs of fieldsetsResponse.response.items) {
64
+ if (fs.module === moduleFilter && fs.model === modelFilter) {
65
+ fieldsetIds.push(fs.id.toString());
66
+ }
67
+ }
68
+ }
69
+ // Get all custom fields
70
+ const fieldsResponse = await flowluApiRequest.call(context, 'GET', baseUrl, '/api/v1/module/customfields/fields/list', apiKey);
71
+ if (fieldsResponse?.response?.items) {
72
+ let fields = fieldsResponse.response.items;
73
+ // Filter by fieldset if we found matching fieldsets
74
+ if (fieldsetIds.length > 0) {
75
+ fields = fields.filter((f) => fieldsetIds.includes(f.fieldset_id?.toString()));
76
+ }
77
+ else {
78
+ // Fallback: filter by module/model directly on the field
79
+ fields = fields.filter((f) => f.module === moduleFilter && f.model === modelFilter);
80
+ }
81
+ return fields
82
+ .filter((f) => f.active !== 0)
83
+ .map((f) => {
84
+ const cfKey = (f.api_use_alias && f.alias)
85
+ ? `cf_${f.alias}`
86
+ : `cf_${f.id}`;
87
+ return {
88
+ name: f.name || cfKey,
89
+ value: cfKey,
90
+ };
91
+ });
92
+ }
93
+ return [];
94
+ }
95
+ catch (error) {
96
+ return [];
97
+ }
98
+ }
99
+ // ============================================================
100
+ // Reusable custom field option definitions
101
+ // ============================================================
102
+ // Helper to load entity custom fields as resourceMapper columns
103
+ async function loadEntityCustomFieldColumns(moduleFilter, modelFilter) {
104
+ try {
105
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
106
+ const fieldsetsResponse = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customfields/fieldsets/list', apiKey);
107
+ const fieldsetIds = [];
108
+ if (fieldsetsResponse?.response?.items) {
109
+ for (const fs of fieldsetsResponse.response.items) {
110
+ if (fs.module === moduleFilter && fs.model === modelFilter) {
111
+ fieldsetIds.push(fs.id.toString());
112
+ }
113
+ }
114
+ }
115
+ const fieldsResponse = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customfields/fields/list', apiKey);
116
+ if (!fieldsResponse?.response?.items)
117
+ return { fields: [] };
118
+ let filtered = fieldsResponse.response.items.filter((f) => f.active !== 0);
119
+ if (fieldsetIds.length > 0) {
120
+ filtered = filtered.filter((f) => fieldsetIds.includes(f.fieldset_id?.toString()));
121
+ }
122
+ else {
123
+ filtered = filtered.filter((f) => f.module === moduleFilter && f.model === modelFilter);
124
+ }
125
+ const fields = filtered.map((f) => {
126
+ const cfKey = (f.api_use_alias && f.alias) ? `cf_${f.alias}` : `cf_${f.id}`;
127
+ let fieldType = 'string';
128
+ if (f.type === 'int')
129
+ fieldType = 'number';
130
+ else if (f.type === 'boolean')
131
+ fieldType = 'boolean';
132
+ else if (f.type === 'date' || f.type === 'datetime')
133
+ fieldType = 'dateTime';
134
+ else if (f.type === 'price' || f.type === 'decimal')
135
+ fieldType = 'number';
136
+ else if (f.type === 'model.any')
137
+ fieldType = 'number';
138
+ let fieldOptions;
139
+ if (f.type === 'select.single') {
140
+ fieldType = 'options';
141
+ try {
142
+ const opts = JSON.parse(f.extra_fields || '{}');
143
+ const selectOptions = JSON.parse(opts.select_options || '[]');
144
+ fieldOptions = selectOptions
145
+ .filter((o) => o.value)
146
+ .map((o) => ({ name: o.value, value: o.id?.toString() || o.value }));
147
+ }
148
+ catch { /* ignore */ }
149
+ }
150
+ const field = {
151
+ id: cfKey,
152
+ displayName: f.name || cfKey,
153
+ required: f.required === 1,
154
+ defaultMatch: false,
155
+ display: true,
156
+ type: fieldType,
157
+ canBeUsedToMatch: false,
158
+ };
159
+ if (fieldOptions)
160
+ field.options = fieldOptions;
161
+ return field;
162
+ });
163
+ return { fields };
164
+ }
165
+ catch (error) {
166
+ return { fields: [] };
167
+ }
168
+ }
169
+ function makeCustomFieldsOption(loadMethod) {
170
+ return {
171
+ displayName: 'Custom Fields',
172
+ name: 'customFields',
173
+ type: 'fixedCollection',
174
+ default: {},
175
+ placeholder: 'Add Custom Field',
176
+ typeOptions: { multipleValues: true },
177
+ description: 'Custom fields defined in your Flowlu account',
178
+ options: [
179
+ {
180
+ displayName: 'Custom Field',
181
+ name: 'customField',
182
+ values: [
183
+ {
184
+ displayName: 'Field Name',
185
+ name: 'fieldId',
186
+ type: 'options',
187
+ typeOptions: { loadOptionsMethod: loadMethod },
188
+ default: '',
189
+ description: 'Select the custom field',
190
+ },
191
+ {
192
+ displayName: 'Value',
193
+ name: 'value',
194
+ type: 'string',
195
+ default: '',
196
+ description: 'The value for this custom field',
197
+ },
198
+ ],
199
+ },
200
+ ],
201
+ };
202
+ }
203
+ const taskCustomFieldsOption = makeCustomFieldsOption('getTaskCustomFields');
204
+ const contactCustomFieldsOption = makeCustomFieldsOption('getContactCustomFields');
205
+ const projectCustomFieldsOption = makeCustomFieldsOption('getProjectCustomFields');
206
+ const opportunityCustomFieldsOption = makeCustomFieldsOption('getOpportunityCustomFields');
207
+ // ============================================================
208
+ // Node Definition
209
+ // ============================================================
210
+ class Flowlu {
211
+ constructor() {
212
+ this.description = {
213
+ displayName: 'Flowlu',
214
+ name: 'flowlu',
215
+ icon: 'file:flowlu.svg',
216
+ group: ['apps'],
217
+ version: 1,
218
+ description: 'Interact with the Flowlu API to manage contacts, tasks, projects, and opportunities',
219
+ defaults: { name: 'Flowlu' },
220
+ inputs: ['main'],
221
+ outputs: ['main'],
222
+ credentials: [{ name: 'flowluApi', required: true }],
223
+ properties: [
224
+ // ========================================
225
+ // RESOURCE SELECTOR
226
+ // ========================================
227
+ {
228
+ displayName: 'Resource',
229
+ name: 'resource',
230
+ type: 'options',
231
+ noDataExpression: true,
232
+ options: [
233
+ { name: 'Comment', value: 'comment' },
234
+ { name: 'Contact', value: 'contact' },
235
+ { name: 'Opportunity', value: 'opportunity' },
236
+ { name: 'Project', value: 'project' },
237
+ { name: 'Record List', value: 'recordList' },
238
+ { name: 'Tag', value: 'tag' },
239
+ { name: 'Task', value: 'task' },
240
+ ],
241
+ default: 'task',
242
+ },
243
+ // ========================================
244
+ // TAG OPERATIONS
245
+ // ========================================
246
+ {
247
+ displayName: 'Operation',
248
+ name: 'operation',
249
+ type: 'options',
250
+ noDataExpression: true,
251
+ displayOptions: { show: { resource: ['tag'] } },
252
+ options: [
253
+ { name: 'Add', value: 'add', description: 'Add a tag to an entity', action: 'Add a tag' },
254
+ { name: 'Get Many', value: 'getAll', description: 'Get all tags on an entity', action: 'Get tags on entity' },
255
+ { name: 'List All', value: 'listAll', description: 'List all tags in your account', action: 'List all tags' },
256
+ { name: 'Remove', value: 'remove', description: 'Remove a tag from an entity', action: 'Remove a tag' },
257
+ ],
258
+ default: 'add',
259
+ },
260
+ // ========================================
261
+ // COMMENT OPERATIONS
262
+ // ========================================
263
+ {
264
+ displayName: 'Operation',
265
+ name: 'operation',
266
+ type: 'options',
267
+ noDataExpression: true,
268
+ displayOptions: { show: { resource: ['comment'] } },
269
+ options: [
270
+ { name: 'Create', value: 'create', description: 'Create a comment on an entity', action: 'Create a comment' },
271
+ { name: 'Get Many', value: 'getAll', description: 'Get all comments on an entity', action: 'Get comments on entity' },
272
+ ],
273
+ default: 'create',
274
+ },
275
+ // ========================================
276
+ // CONTACT OPERATIONS
277
+ // ========================================
278
+ {
279
+ displayName: 'Operation',
280
+ name: 'operation',
281
+ type: 'options',
282
+ noDataExpression: true,
283
+ displayOptions: { show: { resource: ['contact'] } },
284
+ options: [
285
+ { name: 'Create', value: 'create', description: 'Create a new contact', action: 'Create a contact' },
286
+ { name: 'Delete', value: 'delete', description: 'Delete a contact', action: 'Delete a contact' },
287
+ { name: 'Get', value: 'get', description: 'Get a contact by ID', action: 'Get a contact' },
288
+ { name: 'Get Many', value: 'getAll', description: 'Get many contacts', action: 'Get many contacts' },
289
+ { name: 'Update', value: 'update', description: 'Update a contact', action: 'Update a contact' },
290
+ ],
291
+ default: 'create',
292
+ },
293
+ // ========================================
294
+ // OPPORTUNITY OPERATIONS
295
+ // ========================================
296
+ {
297
+ displayName: 'Operation',
298
+ name: 'operation',
299
+ type: 'options',
300
+ noDataExpression: true,
301
+ displayOptions: { show: { resource: ['opportunity'] } },
302
+ options: [
303
+ { name: 'Create', value: 'create', description: 'Create a new opportunity', action: 'Create an opportunity' },
304
+ { name: 'Delete', value: 'delete', description: 'Delete an opportunity', action: 'Delete an opportunity' },
305
+ { name: 'Get', value: 'get', description: 'Get an opportunity by ID', action: 'Get an opportunity' },
306
+ { name: 'Get Many', value: 'getAll', description: 'Get many opportunities', action: 'Get many opportunities' },
307
+ { name: 'Update', value: 'update', description: 'Update an opportunity', action: 'Update an opportunity' },
308
+ ],
309
+ default: 'create',
310
+ },
311
+ // ========================================
312
+ // PROJECT OPERATIONS
313
+ // ========================================
314
+ {
315
+ displayName: 'Operation',
316
+ name: 'operation',
317
+ type: 'options',
318
+ noDataExpression: true,
319
+ displayOptions: { show: { resource: ['project'] } },
320
+ options: [
321
+ { name: 'Create', value: 'create', description: 'Create a new project', action: 'Create a project' },
322
+ { name: 'Delete', value: 'delete', description: 'Delete a project', action: 'Delete a project' },
323
+ { name: 'Get', value: 'get', description: 'Get a project by ID', action: 'Get a project' },
324
+ { name: 'Get Many', value: 'getAll', description: 'Get many projects', action: 'Get many projects' },
325
+ { name: 'Update', value: 'update', description: 'Update a project', action: 'Update a project' },
326
+ ],
327
+ default: 'create',
328
+ },
329
+ // ========================================
330
+ // RECORD LIST OPERATIONS
331
+ // ========================================
332
+ {
333
+ displayName: 'Operation',
334
+ name: 'operation',
335
+ type: 'options',
336
+ noDataExpression: true,
337
+ displayOptions: { show: { resource: ['recordList'] } },
338
+ options: [
339
+ { name: 'Create', value: 'create', description: 'Create a new record', action: 'Create a record' },
340
+ { name: 'Delete', value: 'delete', description: 'Delete a record', action: 'Delete a record' },
341
+ { name: 'Get', value: 'get', description: 'Get a record by ID', action: 'Get a record' },
342
+ { name: 'Get Many', value: 'getAll', description: 'Get many records', action: 'Get many records' },
343
+ { name: 'Update', value: 'update', description: 'Update a record', action: 'Update a record' },
344
+ ],
345
+ default: 'create',
346
+ },
347
+ // ========================================
348
+ // TASK OPERATIONS
349
+ // ========================================
350
+ {
351
+ displayName: 'Operation',
352
+ name: 'operation',
353
+ type: 'options',
354
+ noDataExpression: true,
355
+ displayOptions: { show: { resource: ['task'] } },
356
+ options: [
357
+ { name: 'Create', value: 'create', description: 'Create a new task', action: 'Create a task' },
358
+ { name: 'Delete', value: 'delete', description: 'Delete a task', action: 'Delete a task' },
359
+ { name: 'Get', value: 'get', description: 'Get a task by ID', action: 'Get a task' },
360
+ { name: 'Get Many', value: 'getAll', description: 'Get many tasks', action: 'Get many tasks' },
361
+ { name: 'Update', value: 'update', description: 'Update a task', action: 'Update a task' },
362
+ ],
363
+ default: 'create',
364
+ },
365
+ // ========================================
366
+ // CONTACT FIELDS
367
+ // ========================================
368
+ {
369
+ displayName: 'Contact ID',
370
+ name: 'contactId',
371
+ type: 'number',
372
+ default: 0,
373
+ required: true,
374
+ description: 'The ID of the contact in Flowlu',
375
+ displayOptions: { show: { resource: ['contact'], operation: ['get', 'update', 'delete'] } },
376
+ },
377
+ {
378
+ displayName: 'First Name',
379
+ name: 'first_name',
380
+ type: 'string',
381
+ default: '',
382
+ required: true,
383
+ displayOptions: { show: { resource: ['contact'], operation: ['create'] } },
384
+ },
385
+ {
386
+ displayName: 'Last Name',
387
+ name: 'last_name',
388
+ type: 'string',
389
+ default: '',
390
+ required: true,
391
+ displayOptions: { show: { resource: ['contact'], operation: ['create'] } },
392
+ },
393
+ {
394
+ displayName: 'Email',
395
+ name: 'email',
396
+ type: 'string',
397
+ default: '',
398
+ displayOptions: { show: { resource: ['contact'], operation: ['create'] } },
399
+ },
400
+ {
401
+ displayName: 'Phone',
402
+ name: 'phone',
403
+ type: 'string',
404
+ default: '',
405
+ displayOptions: { show: { resource: ['contact'], operation: ['create'] } },
406
+ },
407
+ {
408
+ displayName: 'Additional Fields',
409
+ name: 'contactAdditionalFields',
410
+ type: 'collection',
411
+ placeholder: 'Add Field',
412
+ default: {},
413
+ displayOptions: { show: { resource: ['contact'], operation: ['create'] } },
414
+ options: [
415
+ { displayName: 'Middle Name', name: 'middle_name', type: 'string', default: '' },
416
+ { displayName: 'Description', name: 'description', type: 'string', typeOptions: { rows: 4 }, default: '' },
417
+ { displayName: 'Owner', name: 'owner_id', type: 'options', typeOptions: { loadOptionsMethod: 'getUsers' }, default: '', description: 'Assigned user for this contact' },
418
+ { displayName: 'Date of Birth', name: 'birth_date', type: 'dateTime', default: '' },
419
+ { displayName: 'Website', name: 'web', type: 'string', default: '' },
420
+ { displayName: 'Phone 2', name: 'phone2', type: 'string', default: '' },
421
+ { displayName: 'Phone 3', name: 'phone3', type: 'string', default: '' },
422
+ { displayName: 'Personal Email', name: 'email_personal', type: 'string', default: '' },
423
+ { displayName: 'Telegram', name: 'social_network_link_2', type: 'string', default: '' },
424
+ { displayName: 'Facebook', name: 'social_network_link_3', type: 'string', default: '' },
425
+ { displayName: 'X (Twitter)', name: 'social_network_link_4', type: 'string', default: '' },
426
+ { displayName: 'LinkedIn', name: 'social_network_link_5', type: 'string', default: '' },
427
+ { displayName: 'Instagram', name: 'social_network_link_6', type: 'string', default: '' },
428
+ { displayName: 'Skype', name: 'social_network_link_1', type: 'string', default: '' },
429
+ { displayName: 'Address', name: 'address', type: 'string', default: '' },
430
+ { displayName: 'Shipping Address Line 1', name: 'shipping_address_line_1', type: 'string', default: '' },
431
+ { displayName: 'Shipping City', name: 'shipping_city', type: 'string', default: '' },
432
+ { displayName: 'Shipping State', name: 'shipping_state', type: 'string', default: '' },
433
+ { displayName: 'Shipping Zip', name: 'shipping_zip', type: 'string', default: '' },
434
+ { displayName: 'Shipping Country', name: 'shipping_country', type: 'string', default: '' },
435
+ { displayName: 'Billing Address Line 1', name: 'billing_address_line_1', type: 'string', default: '' },
436
+ { displayName: 'Billing City', name: 'billing_city', type: 'string', default: '' },
437
+ { displayName: 'Billing State', name: 'billing_state', type: 'string', default: '' },
438
+ { displayName: 'Billing Zip', name: 'billing_zip', type: 'string', default: '' },
439
+ { displayName: 'Billing Country', name: 'billing_country', type: 'string', default: '' },
440
+ { displayName: 'VAT / Tax ID', name: 'VAT', type: 'string', default: '' },
441
+ { displayName: 'Timezone', name: 'timezone', type: 'string', default: '' },
442
+ {
443
+ displayName: 'Include Link to Workflow',
444
+ name: 'includeLinkToWorkflow',
445
+ type: 'boolean',
446
+ default: false,
447
+ description: 'Whether to append a "Generated via n8n: View Workflow" footer to the contact description, linking back to this workflow',
448
+ },
449
+ ],
450
+ },
451
+ {
452
+ displayName: 'Custom Fields',
453
+ name: 'contactCustomFields',
454
+ type: 'resourceMapper',
455
+ noDataExpression: true,
456
+ default: { mappingMode: 'defineBelow', value: null },
457
+ displayOptions: { show: { resource: ['contact'], operation: ['create'] } },
458
+ typeOptions: {
459
+ resourceMapper: {
460
+ resourceMapperMethod: 'getContactColumns',
461
+ mode: 'add',
462
+ fieldWords: { singular: 'field', plural: 'fields' },
463
+ addAllFields: false,
464
+ multiKeyMatch: false,
465
+ },
466
+ },
467
+ },
468
+ {
469
+ displayName: 'Update Fields',
470
+ name: 'contactUpdateFields',
471
+ type: 'collection',
472
+ placeholder: 'Add Field',
473
+ default: {},
474
+ displayOptions: { show: { resource: ['contact'], operation: ['update'] } },
475
+ options: [
476
+ { displayName: 'First Name', name: 'first_name', type: 'string', default: '' },
477
+ { displayName: 'Last Name', name: 'last_name', type: 'string', default: '' },
478
+ { displayName: 'Middle Name', name: 'middle_name', type: 'string', default: '' },
479
+ { displayName: 'Email', name: 'email', type: 'string', default: '' },
480
+ { displayName: 'Phone', name: 'phone', type: 'string', default: '' },
481
+ { displayName: 'Phone 2', name: 'phone2', type: 'string', default: '' },
482
+ { displayName: 'Phone 3', name: 'phone3', type: 'string', default: '' },
483
+ { displayName: 'Description', name: 'description', type: 'string', typeOptions: { rows: 4 }, default: '' },
484
+ { displayName: 'Owner', name: 'owner_id', type: 'options', typeOptions: { loadOptionsMethod: 'getUsers' }, default: '' },
485
+ { displayName: 'Date of Birth', name: 'birth_date', type: 'dateTime', default: '' },
486
+ { displayName: 'Website', name: 'web', type: 'string', default: '' },
487
+ { displayName: 'Personal Email', name: 'email_personal', type: 'string', default: '' },
488
+ { displayName: 'Telegram', name: 'social_network_link_2', type: 'string', default: '' },
489
+ { displayName: 'Facebook', name: 'social_network_link_3', type: 'string', default: '' },
490
+ { displayName: 'X (Twitter)', name: 'social_network_link_4', type: 'string', default: '' },
491
+ { displayName: 'LinkedIn', name: 'social_network_link_5', type: 'string', default: '' },
492
+ { displayName: 'Instagram', name: 'social_network_link_6', type: 'string', default: '' },
493
+ { displayName: 'Skype', name: 'social_network_link_1', type: 'string', default: '' },
494
+ { displayName: 'Address', name: 'address', type: 'string', default: '' },
495
+ { displayName: 'VAT / Tax ID', name: 'VAT', type: 'string', default: '' },
496
+ {
497
+ displayName: 'Include Link to Workflow',
498
+ name: 'includeLinkToWorkflow',
499
+ type: 'boolean',
500
+ default: false,
501
+ description: 'Whether to append a "Generated via n8n: View Workflow" footer to the contact description, linking back to this workflow. Requires Description to be set.',
502
+ },
503
+ ],
504
+ },
505
+ {
506
+ displayName: 'Custom Fields',
507
+ name: 'contactUpdateCustomFields',
508
+ type: 'resourceMapper',
509
+ noDataExpression: true,
510
+ default: { mappingMode: 'defineBelow', value: null },
511
+ displayOptions: { show: { resource: ['contact'], operation: ['update'] } },
512
+ typeOptions: {
513
+ resourceMapper: {
514
+ resourceMapperMethod: 'getContactColumns',
515
+ mode: 'update',
516
+ fieldWords: { singular: 'field', plural: 'fields' },
517
+ addAllFields: false,
518
+ multiKeyMatch: false,
519
+ },
520
+ },
521
+ },
522
+ // Contact Get Many: Filters
523
+ {
524
+ displayName: 'Limit',
525
+ name: 'limit',
526
+ type: 'number',
527
+ default: 50,
528
+ description: 'Max number of results to return',
529
+ typeOptions: { minValue: 1, maxValue: 500 },
530
+ displayOptions: { show: { resource: ['contact'], operation: ['getAll'] } },
531
+ },
532
+ {
533
+ displayName: 'Filters',
534
+ name: 'contactFilters',
535
+ type: 'collection',
536
+ placeholder: 'Add Filter',
537
+ default: {},
538
+ displayOptions: { show: { resource: ['contact'], operation: ['getAll'] } },
539
+ options: [
540
+ { displayName: 'Name', name: 'name', type: 'string', default: '', description: 'Filter by contact name' },
541
+ { displayName: 'Email', name: 'email', type: 'string', default: '', description: 'Filter by email address' },
542
+ { displayName: 'Phone', name: 'phone', type: 'string', default: '', description: 'Filter by phone number' },
543
+ { displayName: 'Owner', name: 'owner_id', type: 'options', typeOptions: { loadOptionsMethod: 'getUsers' }, default: '', description: 'Filter by assigned owner' },
544
+ ],
545
+ },
546
+ // ========================================
547
+ // OPPORTUNITY FIELDS
548
+ // ========================================
549
+ {
550
+ displayName: 'Opportunity ID',
551
+ name: 'opportunityId',
552
+ type: 'number',
553
+ default: 0,
554
+ required: true,
555
+ description: 'The ID of the opportunity in Flowlu',
556
+ displayOptions: { show: { resource: ['opportunity'], operation: ['get', 'update', 'delete'] } },
557
+ },
558
+ // Opportunity Create: Main Fields
559
+ {
560
+ displayName: 'Name',
561
+ name: 'opportunityName',
562
+ type: 'string',
563
+ default: '',
564
+ required: true,
565
+ displayOptions: { show: { resource: ['opportunity'], operation: ['create'] } },
566
+ },
567
+ {
568
+ displayName: 'Pipeline',
569
+ name: 'pipeline_id',
570
+ type: 'options',
571
+ typeOptions: { loadOptionsMethod: 'getPipelines' },
572
+ required: true,
573
+ default: '',
574
+ description: 'The sales pipeline for this opportunity',
575
+ displayOptions: { show: { resource: ['opportunity'], operation: ['create'] } },
576
+ },
577
+ {
578
+ displayName: 'Pipeline Stage',
579
+ name: 'pipeline_stage_id',
580
+ type: 'options',
581
+ typeOptions: { loadOptionsMethod: 'getPipelineStages' },
582
+ required: true,
583
+ default: '',
584
+ description: 'The stage within the pipeline',
585
+ displayOptions: { show: { resource: ['opportunity'], operation: ['create'] } },
586
+ },
587
+ {
588
+ displayName: 'Assignee',
589
+ name: 'opportunityAssignee',
590
+ type: 'options',
591
+ typeOptions: { loadOptionsMethod: 'getUsers' },
592
+ default: '',
593
+ description: 'The user responsible for this opportunity',
594
+ displayOptions: { show: { resource: ['opportunity'], operation: ['create'] } },
595
+ },
596
+ // Opportunity Create: Additional Fields
597
+ {
598
+ displayName: 'Additional Fields',
599
+ name: 'opportunityAdditionalFields',
600
+ type: 'collection',
601
+ placeholder: 'Add Field',
602
+ default: {},
603
+ displayOptions: { show: { resource: ['opportunity'], operation: ['create'] } },
604
+ options: [
605
+ { displayName: 'Budget', name: 'budget', type: 'number', default: 0, description: 'Opportunity value/amount' },
606
+ { displayName: 'Description', name: 'description', type: 'string', typeOptions: { rows: 4 }, default: '' },
607
+ { displayName: 'Link to Account', name: 'link_account_id', type: 'options', typeOptions: { loadOptionsMethod: 'getAccounts' }, default: '', description: 'Link this opportunity to an existing CRM account (organization)' },
608
+ { displayName: 'Link to Contact', name: 'link_contact_id', type: 'options', typeOptions: { loadOptionsMethod: 'getContacts' }, default: '', description: 'Link this opportunity to an existing CRM contact' },
609
+ { displayName: 'Start Date', name: 'start_date', type: 'dateTime', default: '' },
610
+ { displayName: 'Planned Close Date', name: 'deadline', type: 'dateTime', default: '' },
611
+ { displayName: 'Source', name: 'source_id', type: 'options', typeOptions: { loadOptionsMethod: 'getOpportunitySources' }, default: '', description: 'Where this opportunity came from' },
612
+ { displayName: 'Contact Name', name: 'contact_name', type: 'string', default: '', description: 'Quick-add contact name (if not linking to existing contact)' },
613
+ { displayName: 'Contact Email', name: 'contact_email', type: 'string', default: '' },
614
+ { displayName: 'Contact Phone', name: 'contact_phone', type: 'string', default: '' },
615
+ { displayName: 'Contact Mobile', name: 'contact_mobile', type: 'string', default: '' },
616
+ { displayName: 'Contact Company', name: 'contact_company', type: 'string', default: '' },
617
+ { displayName: 'Contact Position', name: 'contact_position', type: 'string', default: '' },
618
+ { displayName: 'Contact Website', name: 'contact_web', type: 'string', default: '' },
619
+ {
620
+ displayName: 'Include Link to Workflow',
621
+ name: 'includeLinkToWorkflow',
622
+ type: 'boolean',
623
+ default: false,
624
+ description: 'Whether to append a "Generated via n8n: View Workflow" footer to the opportunity description, linking back to this workflow',
625
+ },
626
+ ],
627
+ },
628
+ {
629
+ displayName: 'Custom Fields',
630
+ name: 'opportunityCustomFields',
631
+ type: 'resourceMapper',
632
+ noDataExpression: true,
633
+ default: { mappingMode: 'defineBelow', value: null },
634
+ displayOptions: { show: { resource: ['opportunity'], operation: ['create'] } },
635
+ typeOptions: {
636
+ resourceMapper: {
637
+ resourceMapperMethod: 'getOpportunityColumns',
638
+ mode: 'add',
639
+ fieldWords: { singular: 'field', plural: 'fields' },
640
+ addAllFields: false,
641
+ multiKeyMatch: false,
642
+ },
643
+ },
644
+ },
645
+ // Opportunity Update: Fields
646
+ {
647
+ displayName: 'Update Fields',
648
+ name: 'opportunityUpdateFields',
649
+ type: 'collection',
650
+ placeholder: 'Add Field',
651
+ default: {},
652
+ displayOptions: { show: { resource: ['opportunity'], operation: ['update'] } },
653
+ options: [
654
+ { displayName: 'Name', name: 'name', type: 'string', default: '' },
655
+ { displayName: 'Pipeline', name: 'pipeline_id', type: 'options', typeOptions: { loadOptionsMethod: 'getPipelines' }, default: '' },
656
+ { displayName: 'Pipeline Stage', name: 'pipeline_stage_id', type: 'options', typeOptions: { loadOptionsMethod: 'getPipelineStages' }, default: '' },
657
+ { displayName: 'Assignee', name: 'assignee_id', type: 'options', typeOptions: { loadOptionsMethod: 'getUsers' }, default: '' },
658
+ { displayName: 'Budget', name: 'budget', type: 'number', default: 0 },
659
+ { displayName: 'Description', name: 'description', type: 'string', typeOptions: { rows: 4 }, default: '' },
660
+ { displayName: 'Link to Account', name: 'link_account_id', type: 'options', typeOptions: { loadOptionsMethod: 'getAccounts' }, default: '', description: 'Link this opportunity to a CRM account' },
661
+ { displayName: 'Link to Contact', name: 'link_contact_id', type: 'options', typeOptions: { loadOptionsMethod: 'getContacts' }, default: '', description: 'Link this opportunity to a CRM contact' },
662
+ { displayName: 'Start Date', name: 'start_date', type: 'dateTime', default: '' },
663
+ { displayName: 'Planned Close Date', name: 'deadline', type: 'dateTime', default: '' },
664
+ {
665
+ displayName: 'Status',
666
+ name: 'active',
667
+ type: 'options',
668
+ options: [
669
+ { name: 'In Progress', value: 1 },
670
+ { name: 'Lost', value: 2 },
671
+ { name: 'Won', value: 3 },
672
+ ],
673
+ default: 1,
674
+ },
675
+ { displayName: 'Closing Comment', name: 'closing_comment', type: 'string', default: '', description: 'Reason for win/loss' },
676
+ { displayName: 'Contact Name', name: 'contact_name', type: 'string', default: '' },
677
+ { displayName: 'Contact Email', name: 'contact_email', type: 'string', default: '' },
678
+ { displayName: 'Contact Phone', name: 'contact_phone', type: 'string', default: '' },
679
+ {
680
+ displayName: 'Include Link to Workflow',
681
+ name: 'includeLinkToWorkflow',
682
+ type: 'boolean',
683
+ default: false,
684
+ description: 'Whether to append a "Generated via n8n: View Workflow" footer to the opportunity description, linking back to this workflow. Requires Description to be set.',
685
+ },
686
+ ],
687
+ },
688
+ {
689
+ displayName: 'Custom Fields',
690
+ name: 'opportunityUpdateCustomFields',
691
+ type: 'resourceMapper',
692
+ noDataExpression: true,
693
+ default: { mappingMode: 'defineBelow', value: null },
694
+ displayOptions: { show: { resource: ['opportunity'], operation: ['update'] } },
695
+ typeOptions: {
696
+ resourceMapper: {
697
+ resourceMapperMethod: 'getOpportunityColumns',
698
+ mode: 'update',
699
+ fieldWords: { singular: 'field', plural: 'fields' },
700
+ addAllFields: false,
701
+ multiKeyMatch: false,
702
+ },
703
+ },
704
+ },
705
+ // Opportunity Get Many: Filters
706
+ {
707
+ displayName: 'Limit',
708
+ name: 'limit',
709
+ type: 'number',
710
+ default: 50,
711
+ description: 'Max number of results to return',
712
+ typeOptions: { minValue: 1, maxValue: 500 },
713
+ displayOptions: { show: { resource: ['opportunity'], operation: ['getAll'] } },
714
+ },
715
+ {
716
+ displayName: 'Pipeline',
717
+ name: 'opportunityFilterPipeline',
718
+ type: 'options',
719
+ typeOptions: { loadOptionsMethod: 'getPipelines' },
720
+ default: '',
721
+ description: 'Filter by pipeline (also controls which stages appear below)',
722
+ displayOptions: { show: { resource: ['opportunity'], operation: ['getAll'] } },
723
+ },
724
+ {
725
+ displayName: 'Pipeline Stage',
726
+ name: 'opportunityFilterStage',
727
+ type: 'options',
728
+ typeOptions: {
729
+ loadOptionsMethod: 'getFilteredPipelineStages',
730
+ loadOptionsDependsOn: ['opportunityFilterPipeline'],
731
+ },
732
+ default: '',
733
+ description: 'Filter by stage (select a pipeline first to see its stages)',
734
+ displayOptions: { show: { resource: ['opportunity'], operation: ['getAll'] } },
735
+ },
736
+ {
737
+ displayName: 'Filters',
738
+ name: 'opportunityFilters',
739
+ type: 'collection',
740
+ placeholder: 'Add Filter',
741
+ default: {},
742
+ displayOptions: { show: { resource: ['opportunity'], operation: ['getAll'] } },
743
+ options: [
744
+ { displayName: 'Name', name: 'name', type: 'string', default: '', description: 'Filter by opportunity name' },
745
+ { displayName: 'Assignee', name: 'assignee_id', type: 'options', typeOptions: { loadOptionsMethod: 'getUsers' }, default: '' },
746
+ { displayName: 'Account / Contact', name: 'company_id', type: 'options', typeOptions: { loadOptionsMethod: 'getAllCrmAccounts' }, default: '', description: 'Filter by linked company or contact (note: Flowlu may not support this filter)' },
747
+ {
748
+ displayName: 'Status',
749
+ name: 'active',
750
+ type: 'options',
751
+ options: [
752
+ { name: 'In Progress', value: 1 },
753
+ { name: 'Lost', value: 2 },
754
+ { name: 'Won', value: 3 },
755
+ ],
756
+ default: 1,
757
+ },
758
+ ],
759
+ },
760
+ // ========================================
761
+ // PROJECT FIELDS
762
+ // ========================================
763
+ {
764
+ displayName: 'Project ID',
765
+ name: 'projectId',
766
+ type: 'number',
767
+ default: 0,
768
+ required: true,
769
+ description: 'The ID of the project in Flowlu',
770
+ displayOptions: { show: { resource: ['project'], operation: ['get', 'update', 'delete'] } },
771
+ },
772
+ // Project Create: Main Fields
773
+ {
774
+ displayName: 'Project Name',
775
+ name: 'projectName',
776
+ type: 'string',
777
+ default: '',
778
+ required: true,
779
+ displayOptions: { show: { resource: ['project'], operation: ['create'] } },
780
+ },
781
+ {
782
+ displayName: 'Manager',
783
+ name: 'manager_id',
784
+ type: 'options',
785
+ typeOptions: { loadOptionsMethod: 'getUsers' },
786
+ default: '',
787
+ description: 'The user responsible for managing this project',
788
+ displayOptions: { show: { resource: ['project'], operation: ['create'] } },
789
+ },
790
+ {
791
+ displayName: 'Description',
792
+ name: 'projectDescription',
793
+ type: 'string',
794
+ typeOptions: { rows: 4 },
795
+ default: '',
796
+ displayOptions: { show: { resource: ['project'], operation: ['create'] } },
797
+ },
798
+ // Project Create: Additional Fields
799
+ {
800
+ displayName: 'Additional Fields',
801
+ name: 'projectAdditionalFields',
802
+ type: 'collection',
803
+ placeholder: 'Add Field',
804
+ default: {},
805
+ displayOptions: { show: { resource: ['project'], operation: ['create'] } },
806
+ options: [
807
+ {
808
+ displayName: 'Priority',
809
+ name: 'priority',
810
+ type: 'options',
811
+ options: [
812
+ { name: 'Low', value: 1 },
813
+ { name: 'Medium', value: 2 },
814
+ { name: 'High', value: 3 },
815
+ ],
816
+ default: 2,
817
+ },
818
+ { displayName: 'Start Date', name: 'startdate', type: 'dateTime', default: '' },
819
+ { displayName: 'End Date', name: 'enddate', type: 'dateTime', default: '' },
820
+ { displayName: 'Contract Sum (Revenue)', name: 'estimated_revenue', type: 'number', default: 0 },
821
+ { displayName: 'Expense Sum', name: 'estimated_expenses', type: 'number', default: 0 },
822
+ { displayName: 'Customer (Company ID)', name: 'customer_id', type: 'options', typeOptions: { loadOptionsMethod: 'getContacts' }, default: '', description: 'CRM company linked to this project' },
823
+ { displayName: 'Customer Contact', name: 'customer_crm_contact_id', type: 'options', typeOptions: { loadOptionsMethod: 'getContacts' }, default: '', description: 'CRM contact linked to this project' },
824
+ { displayName: 'Portfolio', name: 'briefcase_id', type: 'options', typeOptions: { loadOptionsMethod: 'getPortfolios' }, default: '' },
825
+ { displayName: 'Project Stage', name: 'stage_id', type: 'options', typeOptions: { loadOptionsMethod: 'getProjectStages' }, default: '' },
826
+ { displayName: 'Task Workflow', name: 'tasks_workflow_id', type: 'options', typeOptions: { loadOptionsMethod: 'getTaskWorkflows' }, default: '', description: 'Default task workflow for tasks in this project' },
827
+ {
828
+ displayName: 'Billing Type',
829
+ name: 'billing_type',
830
+ type: 'options',
831
+ options: [
832
+ { name: 'Fixed Values', value: 0 },
833
+ { name: 'Based on Task Hours', value: 10 },
834
+ { name: 'Cash Flow Planning', value: 20 },
835
+ { name: 'Ignore Finances', value: 30 },
836
+ ],
837
+ default: 0,
838
+ },
839
+ { displayName: 'Opportunity', name: 'crm_lead_id', type: 'number', default: 0, description: 'ID of a linked CRM opportunity' },
840
+ {
841
+ displayName: 'Include Link to Workflow',
842
+ name: 'includeLinkToWorkflow',
843
+ type: 'boolean',
844
+ default: false,
845
+ description: 'Whether to append a "Generated via n8n: View Workflow" footer to the project description, linking back to this workflow',
846
+ },
847
+ ],
848
+ },
849
+ {
850
+ displayName: 'Custom Fields',
851
+ name: 'projectCustomFields',
852
+ type: 'resourceMapper',
853
+ noDataExpression: true,
854
+ default: { mappingMode: 'defineBelow', value: null },
855
+ displayOptions: { show: { resource: ['project'], operation: ['create'] } },
856
+ typeOptions: {
857
+ resourceMapper: {
858
+ resourceMapperMethod: 'getProjectColumns',
859
+ mode: 'add',
860
+ fieldWords: { singular: 'field', plural: 'fields' },
861
+ addAllFields: false,
862
+ multiKeyMatch: false,
863
+ },
864
+ },
865
+ },
866
+ // Project Update: Fields
867
+ {
868
+ displayName: 'Update Fields',
869
+ name: 'projectUpdateFields',
870
+ type: 'collection',
871
+ placeholder: 'Add Field',
872
+ default: {},
873
+ displayOptions: { show: { resource: ['project'], operation: ['update'] } },
874
+ options: [
875
+ { displayName: 'Project Name', name: 'name', type: 'string', default: '' },
876
+ { displayName: 'Description', name: 'description', type: 'string', typeOptions: { rows: 4 }, default: '' },
877
+ { displayName: 'Manager', name: 'manager_id', type: 'options', typeOptions: { loadOptionsMethod: 'getUsers' }, default: '' },
878
+ {
879
+ displayName: 'Priority',
880
+ name: 'priority',
881
+ type: 'options',
882
+ options: [
883
+ { name: 'Low', value: 1 },
884
+ { name: 'Medium', value: 2 },
885
+ { name: 'High', value: 3 },
886
+ ],
887
+ default: 2,
888
+ },
889
+ { displayName: 'Start Date', name: 'startdate', type: 'dateTime', default: '' },
890
+ { displayName: 'End Date', name: 'enddate', type: 'dateTime', default: '' },
891
+ { displayName: 'Contract Sum (Revenue)', name: 'estimated_revenue', type: 'number', default: 0 },
892
+ { displayName: 'Expense Sum', name: 'estimated_expenses', type: 'number', default: 0 },
893
+ { displayName: 'Project Stage', name: 'stage_id', type: 'options', typeOptions: { loadOptionsMethod: 'getProjectStages' }, default: '' },
894
+ {
895
+ displayName: 'Archived',
896
+ name: 'is_archive',
897
+ type: 'boolean',
898
+ default: false,
899
+ },
900
+ {
901
+ displayName: 'Include Link to Workflow',
902
+ name: 'includeLinkToWorkflow',
903
+ type: 'boolean',
904
+ default: false,
905
+ description: 'Whether to append a "Generated via n8n: View Workflow" footer to the project description, linking back to this workflow. Requires Description to be set.',
906
+ },
907
+ ],
908
+ },
909
+ {
910
+ displayName: 'Custom Fields',
911
+ name: 'projectUpdateCustomFields',
912
+ type: 'resourceMapper',
913
+ noDataExpression: true,
914
+ default: { mappingMode: 'defineBelow', value: null },
915
+ displayOptions: { show: { resource: ['project'], operation: ['update'] } },
916
+ typeOptions: {
917
+ resourceMapper: {
918
+ resourceMapperMethod: 'getProjectColumns',
919
+ mode: 'update',
920
+ fieldWords: { singular: 'field', plural: 'fields' },
921
+ addAllFields: false,
922
+ multiKeyMatch: false,
923
+ },
924
+ },
925
+ },
926
+ // Project Get Many: Filters
927
+ {
928
+ displayName: 'Limit',
929
+ name: 'limit',
930
+ type: 'number',
931
+ default: 50,
932
+ description: 'Max number of results to return',
933
+ typeOptions: { minValue: 1, maxValue: 500 },
934
+ displayOptions: { show: { resource: ['project'], operation: ['getAll'] } },
935
+ },
936
+ {
937
+ displayName: 'Filters',
938
+ name: 'projectFilters',
939
+ type: 'collection',
940
+ placeholder: 'Add Filter',
941
+ default: {},
942
+ displayOptions: { show: { resource: ['project'], operation: ['getAll'] } },
943
+ options: [
944
+ { displayName: 'Name', name: 'name', type: 'string', default: '', description: 'Filter by project name' },
945
+ { displayName: 'Manager', name: 'manager_id', type: 'options', typeOptions: { loadOptionsMethod: 'getUsers' }, default: '' },
946
+ { displayName: 'Customer', name: 'customer_id', type: 'options', typeOptions: { loadOptionsMethod: 'getAccounts' }, default: '', description: 'Filter by linked company' },
947
+ {
948
+ displayName: 'Include Archived',
949
+ name: 'is_archive',
950
+ type: 'boolean',
951
+ default: false,
952
+ },
953
+ ],
954
+ },
955
+ // ========================================
956
+ // RECORD LIST FIELDS
957
+ // ========================================
958
+ {
959
+ displayName: 'Record List',
960
+ name: 'recordListId',
961
+ type: 'options',
962
+ typeOptions: { loadOptionsMethod: 'getRecordLists' },
963
+ required: true,
964
+ default: '',
965
+ description: 'Which record list to work with',
966
+ displayOptions: { show: { resource: ['recordList'] } },
967
+ },
968
+ {
969
+ displayName: 'Record ID',
970
+ name: 'recordId',
971
+ type: 'number',
972
+ default: 0,
973
+ required: true,
974
+ description: 'The ID of the record',
975
+ displayOptions: { show: { resource: ['recordList'], operation: ['get', 'update', 'delete'] } },
976
+ },
977
+ {
978
+ displayName: 'Fields',
979
+ name: 'recordFields',
980
+ type: 'resourceMapper',
981
+ noDataExpression: true,
982
+ default: {
983
+ mappingMode: 'defineBelow',
984
+ value: null,
985
+ },
986
+ required: true,
987
+ displayOptions: { show: { resource: ['recordList'], operation: ['create'] } },
988
+ typeOptions: {
989
+ loadOptionsDependsOn: ['recordListId'],
990
+ resourceMapper: {
991
+ resourceMapperMethod: 'getRecordListColumns',
992
+ mode: 'add',
993
+ fieldWords: { singular: 'field', plural: 'fields' },
994
+ addAllFields: false,
995
+ multiKeyMatch: false,
996
+ },
997
+ },
998
+ },
999
+ {
1000
+ displayName: 'Fields',
1001
+ name: 'recordUpdateFields',
1002
+ type: 'resourceMapper',
1003
+ noDataExpression: true,
1004
+ default: {
1005
+ mappingMode: 'defineBelow',
1006
+ value: null,
1007
+ },
1008
+ required: true,
1009
+ displayOptions: { show: { resource: ['recordList'], operation: ['update'] } },
1010
+ typeOptions: {
1011
+ loadOptionsDependsOn: ['recordListId'],
1012
+ resourceMapper: {
1013
+ resourceMapperMethod: 'getRecordListColumns',
1014
+ mode: 'update',
1015
+ fieldWords: { singular: 'field', plural: 'fields' },
1016
+ addAllFields: false,
1017
+ multiKeyMatch: false,
1018
+ },
1019
+ },
1020
+ },
1021
+ // Record List Get Many: Limit
1022
+ {
1023
+ displayName: 'Limit',
1024
+ name: 'limit',
1025
+ type: 'number',
1026
+ default: 50,
1027
+ description: 'Max number of results to return',
1028
+ typeOptions: { minValue: 1, maxValue: 500 },
1029
+ displayOptions: { show: { resource: ['recordList'], operation: ['getAll'] } },
1030
+ },
1031
+ {
1032
+ displayName: 'Filters',
1033
+ name: 'recordListFilters',
1034
+ type: 'collection',
1035
+ placeholder: 'Add Filter',
1036
+ default: {},
1037
+ displayOptions: { show: { resource: ['recordList'], operation: ['getAll'] } },
1038
+ options: [
1039
+ { displayName: 'Search', name: 'search', type: 'string', default: '', description: 'Search records by name/content' },
1040
+ ],
1041
+ },
1042
+ // ========================================
1043
+ // TASK FIELDS
1044
+ // ========================================
1045
+ {
1046
+ displayName: 'Task ID',
1047
+ name: 'taskId',
1048
+ type: 'number',
1049
+ default: 0,
1050
+ required: true,
1051
+ description: 'The ID of the task in Flowlu',
1052
+ displayOptions: { show: { resource: ['task'], operation: ['get', 'update', 'delete'] } },
1053
+ },
1054
+ // Task Create: Main Fields
1055
+ {
1056
+ displayName: 'Task Name',
1057
+ name: 'taskName',
1058
+ type: 'string',
1059
+ default: '',
1060
+ required: true,
1061
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1062
+ },
1063
+ {
1064
+ displayName: 'Assignee',
1065
+ name: 'responsible_id',
1066
+ type: 'options',
1067
+ typeOptions: { loadOptionsMethod: 'getUsers' },
1068
+ required: true,
1069
+ default: '',
1070
+ description: 'The user responsible for completing this task',
1071
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1072
+ },
1073
+ {
1074
+ displayName: 'Description',
1075
+ name: 'description',
1076
+ type: 'string',
1077
+ typeOptions: { rows: 4 },
1078
+ default: '',
1079
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1080
+ },
1081
+ // Task Create: Additional Fields
1082
+ {
1083
+ displayName: 'Additional Fields',
1084
+ name: 'taskAdditionalFields',
1085
+ type: 'collection',
1086
+ placeholder: 'Add Field',
1087
+ default: {},
1088
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1089
+ options: [
1090
+ { displayName: 'Owner', name: 'owner_id', type: 'options', typeOptions: { loadOptionsMethod: 'getUsers' }, description: 'The user who owns/created this task', default: '' },
1091
+ { displayName: 'Contact', name: 'contact_id', type: 'options', typeOptions: { loadOptionsMethod: 'getContacts' }, default: '', description: 'The contact this task is related to' },
1092
+ { displayName: 'Project', name: 'model_id', type: 'options', typeOptions: { loadOptionsMethod: 'getProjects' }, description: 'The project this task belongs to', default: '' },
1093
+ { displayName: 'Workflow', name: 'workflow_id', type: 'options', typeOptions: { loadOptionsMethod: 'getTaskWorkflows' }, default: '', description: 'The task workflow to use (defaults to the first workflow if not set)' },
1094
+ { displayName: 'Workflow Stage', name: 'workflow_stage_id', type: 'options', typeOptions: { loadOptionsMethod: 'getTaskWorkflowStages' }, default: '', description: 'The starting stage within the workflow' },
1095
+ {
1096
+ displayName: 'Priority',
1097
+ name: 'priority',
1098
+ type: 'options',
1099
+ options: [
1100
+ { name: 'Low', value: 1 },
1101
+ { name: 'Medium', value: 2 },
1102
+ { name: 'High', value: 3 },
1103
+ ],
1104
+ default: 2,
1105
+ },
1106
+ {
1107
+ displayName: 'Status',
1108
+ name: 'status',
1109
+ type: 'options',
1110
+ options: [
1111
+ { name: 'New', value: 1 },
1112
+ { name: 'In Progress', value: 3 },
1113
+ { name: 'Pending Approval', value: 4 },
1114
+ { name: 'Completed', value: 5 },
1115
+ ],
1116
+ default: 1,
1117
+ },
1118
+ { displayName: 'Start Date', name: 'plan_start_date', type: 'dateTime', default: '' },
1119
+ { displayName: 'End Date', name: 'deadline', type: 'dateTime', default: '' },
1120
+ { displayName: 'Time Estimate (Minutes)', name: 'time_estimate', type: 'number', default: 0, typeOptions: { minValue: 0 } },
1121
+ { displayName: 'Planned Cost', name: 'cost', type: 'number', default: 0, typeOptions: { minValue: 0 } },
1122
+ { displayName: 'Planned Income', name: 'price', type: 'number', default: 0, typeOptions: { minValue: 0 } },
1123
+ { displayName: 'Reviewed by Owner', name: 'task_checkbyowner', type: 'boolean', default: false },
1124
+ { displayName: 'Allow End Date Change', name: 'deadline_allowchange', type: 'boolean', default: true },
1125
+ { displayName: 'Parent Task ID', name: 'parent_id', type: 'number', default: 0, description: 'ID of the parent task (for subtasks)' },
1126
+ {
1127
+ displayName: 'Include Link to Workflow',
1128
+ name: 'includeLinkToWorkflow',
1129
+ type: 'boolean',
1130
+ default: false,
1131
+ description: 'Whether to append a "Generated via n8n: View Workflow" footer to the task description, linking back to this workflow',
1132
+ },
1133
+ ],
1134
+ },
1135
+ {
1136
+ displayName: 'Custom Fields',
1137
+ name: 'taskCustomFields',
1138
+ type: 'resourceMapper',
1139
+ noDataExpression: true,
1140
+ default: { mappingMode: 'defineBelow', value: null },
1141
+ displayOptions: { show: { resource: ['task'], operation: ['create'] } },
1142
+ typeOptions: {
1143
+ resourceMapper: {
1144
+ resourceMapperMethod: 'getTaskColumns',
1145
+ mode: 'add',
1146
+ fieldWords: { singular: 'field', plural: 'fields' },
1147
+ addAllFields: false,
1148
+ multiKeyMatch: false,
1149
+ },
1150
+ },
1151
+ },
1152
+ // Task Update: Fields
1153
+ {
1154
+ displayName: 'Update Fields',
1155
+ name: 'taskUpdateFields',
1156
+ type: 'collection',
1157
+ placeholder: 'Add Field',
1158
+ default: {},
1159
+ displayOptions: { show: { resource: ['task'], operation: ['update'] } },
1160
+ options: [
1161
+ { displayName: 'Task Name', name: 'name', type: 'string', default: '' },
1162
+ { displayName: 'Description', name: 'description', type: 'string', typeOptions: { rows: 4 }, default: '' },
1163
+ { displayName: 'Assignee', name: 'responsible_id', type: 'options', typeOptions: { loadOptionsMethod: 'getUsers' }, default: '' },
1164
+ { displayName: 'Owner', name: 'owner_id', type: 'options', typeOptions: { loadOptionsMethod: 'getUsers' }, default: '' },
1165
+ { displayName: 'Project', name: 'model_id', type: 'options', typeOptions: { loadOptionsMethod: 'getProjects' }, default: '' },
1166
+ { displayName: 'Workflow', name: 'workflow_id', type: 'options', typeOptions: { loadOptionsMethod: 'getTaskWorkflows' }, default: '' },
1167
+ { displayName: 'Workflow Stage', name: 'workflow_stage_id', type: 'options', typeOptions: { loadOptionsMethod: 'getTaskWorkflowStages' }, default: '' },
1168
+ {
1169
+ displayName: 'Priority',
1170
+ name: 'priority',
1171
+ type: 'options',
1172
+ options: [
1173
+ { name: 'Low', value: 1 },
1174
+ { name: 'Medium', value: 2 },
1175
+ { name: 'High', value: 3 },
1176
+ ],
1177
+ default: 2,
1178
+ },
1179
+ {
1180
+ displayName: 'Status',
1181
+ name: 'status',
1182
+ type: 'options',
1183
+ options: [
1184
+ { name: 'New', value: 1 },
1185
+ { name: 'In Progress', value: 3 },
1186
+ { name: 'Pending Approval', value: 4 },
1187
+ { name: 'Completed', value: 5 },
1188
+ ],
1189
+ default: 1,
1190
+ },
1191
+ { displayName: 'Start Date', name: 'plan_start_date', type: 'dateTime', default: '' },
1192
+ { displayName: 'End Date', name: 'deadline', type: 'dateTime', default: '' },
1193
+ { displayName: 'Time Estimate (Minutes)', name: 'time_estimate', type: 'number', default: 0, typeOptions: { minValue: 0 } },
1194
+ { displayName: 'Planned Cost', name: 'cost', type: 'number', default: 0, typeOptions: { minValue: 0 } },
1195
+ { displayName: 'Planned Income', name: 'price', type: 'number', default: 0, typeOptions: { minValue: 0 } },
1196
+ { displayName: 'Parent Task ID', name: 'parent_id', type: 'number', default: 0 },
1197
+ {
1198
+ displayName: 'Include Link to Workflow',
1199
+ name: 'includeLinkToWorkflow',
1200
+ type: 'boolean',
1201
+ default: false,
1202
+ description: 'Whether to append a "Generated via n8n: View Workflow" footer to the task description, linking back to this workflow. Requires Description to be set.',
1203
+ },
1204
+ ],
1205
+ },
1206
+ {
1207
+ displayName: 'Custom Fields',
1208
+ name: 'taskUpdateCustomFields',
1209
+ type: 'resourceMapper',
1210
+ noDataExpression: true,
1211
+ default: { mappingMode: 'defineBelow', value: null },
1212
+ displayOptions: { show: { resource: ['task'], operation: ['update'] } },
1213
+ typeOptions: {
1214
+ resourceMapper: {
1215
+ resourceMapperMethod: 'getTaskColumns',
1216
+ mode: 'update',
1217
+ fieldWords: { singular: 'field', plural: 'fields' },
1218
+ addAllFields: false,
1219
+ multiKeyMatch: false,
1220
+ },
1221
+ },
1222
+ },
1223
+ // Task Get Many: Filters
1224
+ {
1225
+ displayName: 'Limit',
1226
+ name: 'limit',
1227
+ type: 'number',
1228
+ default: 50,
1229
+ description: 'Max number of results to return',
1230
+ typeOptions: { minValue: 1, maxValue: 500 },
1231
+ displayOptions: { show: { resource: ['task'], operation: ['getAll'] } },
1232
+ },
1233
+ {
1234
+ displayName: 'Filters',
1235
+ name: 'taskFilters',
1236
+ type: 'collection',
1237
+ placeholder: 'Add Filter',
1238
+ default: {},
1239
+ displayOptions: { show: { resource: ['task'], operation: ['getAll'] } },
1240
+ options: [
1241
+ { displayName: 'Name', name: 'name', type: 'string', default: '', description: 'Filter by task name' },
1242
+ { displayName: 'Assignee', name: 'responsible_id', type: 'options', typeOptions: { loadOptionsMethod: 'getUsers' }, default: '' },
1243
+ { displayName: 'Project', name: 'model_id', type: 'options', typeOptions: { loadOptionsMethod: 'getProjects' }, default: '' },
1244
+ {
1245
+ displayName: 'Priority',
1246
+ name: 'priority',
1247
+ type: 'options',
1248
+ options: [
1249
+ { name: 'Low', value: 1 },
1250
+ { name: 'Medium', value: 2 },
1251
+ { name: 'High', value: 3 },
1252
+ ],
1253
+ default: 2,
1254
+ },
1255
+ {
1256
+ displayName: 'Status',
1257
+ name: 'status',
1258
+ type: 'options',
1259
+ options: [
1260
+ { name: 'New', value: 1 },
1261
+ { name: 'In Progress', value: 3 },
1262
+ { name: 'Pending Approval', value: 4 },
1263
+ { name: 'Completed', value: 5 },
1264
+ ],
1265
+ default: 1,
1266
+ },
1267
+ ],
1268
+ },
1269
+ // ========================================
1270
+ // TAG FIELDS
1271
+ // ========================================
1272
+ {
1273
+ displayName: 'Entity Type',
1274
+ name: 'tagEntityType',
1275
+ type: 'options',
1276
+ noDataExpression: true,
1277
+ displayOptions: { show: { resource: ['tag'], operation: ['add', 'getAll', 'remove'] } },
1278
+ options: [
1279
+ { name: 'Contact', value: 'crm/account' },
1280
+ { name: 'Opportunity', value: 'crm/lead' },
1281
+ { name: 'Task', value: 'task/tasks' },
1282
+ { name: 'Project', value: 'st/projects' },
1283
+ ],
1284
+ default: 'crm/account',
1285
+ description: 'The type of entity to manage tags on',
1286
+ },
1287
+ {
1288
+ displayName: 'Entity ID',
1289
+ name: 'tagEntityId',
1290
+ type: 'number',
1291
+ default: 0,
1292
+ required: true,
1293
+ displayOptions: { show: { resource: ['tag'], operation: ['add', 'getAll', 'remove'] } },
1294
+ description: 'The ID of the entity (Contact, Opportunity, Task, or Project)',
1295
+ },
1296
+ {
1297
+ displayName: 'Tag Name',
1298
+ name: 'tagName',
1299
+ type: 'string',
1300
+ default: '',
1301
+ required: true,
1302
+ displayOptions: { show: { resource: ['tag'], operation: ['add'] } },
1303
+ description: 'The name of the tag to add. If the tag does not exist, it will be created automatically.',
1304
+ },
1305
+ {
1306
+ displayName: 'Tag ID',
1307
+ name: 'tagIdToRemove',
1308
+ type: 'number',
1309
+ default: 0,
1310
+ required: true,
1311
+ displayOptions: { show: { resource: ['tag'], operation: ['remove'] } },
1312
+ description: 'The ID of the tag to remove. Use "Get Many" or "List All" to find tag IDs.',
1313
+ },
1314
+ // ========================================
1315
+ // COMMENT FIELDS
1316
+ // ========================================
1317
+ {
1318
+ displayName: 'Entity Type',
1319
+ name: 'commentEntityType',
1320
+ type: 'options',
1321
+ noDataExpression: true,
1322
+ displayOptions: { show: { resource: ['comment'] } },
1323
+ options: [
1324
+ { name: 'Contact', value: 'crm/account' },
1325
+ { name: 'Opportunity', value: 'crm/lead' },
1326
+ { name: 'Task', value: 'task/tasks' },
1327
+ { name: 'Project', value: 'st/projects' },
1328
+ ],
1329
+ default: 'crm/account',
1330
+ description: 'The type of entity to comment on',
1331
+ },
1332
+ {
1333
+ displayName: 'Entity ID',
1334
+ name: 'commentEntityId',
1335
+ type: 'number',
1336
+ default: 0,
1337
+ required: true,
1338
+ displayOptions: { show: { resource: ['comment'] } },
1339
+ description: 'The ID of the entity (Contact, Opportunity, Task, or Project)',
1340
+ },
1341
+ {
1342
+ displayName: 'Comment Text',
1343
+ name: 'commentText',
1344
+ type: 'string',
1345
+ typeOptions: { rows: 6 },
1346
+ default: '',
1347
+ required: true,
1348
+ displayOptions: { show: { resource: ['comment'], operation: ['create'] } },
1349
+ description: 'The comment text (HTML supported)',
1350
+ },
1351
+ {
1352
+ displayName: 'Include Link to Workflow',
1353
+ name: 'commentIncludeLinkToWorkflow',
1354
+ type: 'boolean',
1355
+ default: false,
1356
+ displayOptions: { show: { resource: ['comment'], operation: ['create'] } },
1357
+ description: 'Whether to append a "Generated via n8n: View Workflow" footer to the comment text, linking back to this workflow',
1358
+ },
1359
+ {
1360
+ displayName: 'Limit',
1361
+ name: 'limit',
1362
+ type: 'number',
1363
+ default: 50,
1364
+ description: 'Max number of results to return',
1365
+ typeOptions: { minValue: 1, maxValue: 500 },
1366
+ displayOptions: { show: { resource: ['comment'], operation: ['getAll'] } },
1367
+ },
1368
+ ],
1369
+ };
1370
+ this.methods = {
1371
+ loadOptions: {
1372
+ async getUsers() {
1373
+ try {
1374
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1375
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/core/user/list', apiKey, undefined, { 'filter[role_login]': '1' });
1376
+ if (response?.response?.items) {
1377
+ return response.response.items.map((user) => ({ name: user.name || user.email || `User ${user.id}`, value: user.id.toString() }));
1378
+ }
1379
+ return [];
1380
+ }
1381
+ catch (error) {
1382
+ return [];
1383
+ }
1384
+ },
1385
+ async getAccounts() {
1386
+ try {
1387
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1388
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/account/list', apiKey, undefined, { 'filter[type]': '1' });
1389
+ if (response?.response?.items) {
1390
+ return response.response.items.map((a) => ({ name: a.name || `Account ${a.id}`, value: a.id.toString() }));
1391
+ }
1392
+ return [];
1393
+ }
1394
+ catch (error) {
1395
+ return [];
1396
+ }
1397
+ },
1398
+ async getContacts() {
1399
+ try {
1400
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1401
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/account/list', apiKey, undefined, { 'filter[type]': '2' });
1402
+ if (response?.response?.items) {
1403
+ return response.response.items.map((c) => ({ name: c.name || `Contact ${c.id}`, value: c.id.toString() }));
1404
+ }
1405
+ return [];
1406
+ }
1407
+ catch (error) {
1408
+ return [];
1409
+ }
1410
+ },
1411
+ async getProjects() {
1412
+ try {
1413
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1414
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/st/projects/list', apiKey, undefined, { 'filter[is_archive]': '0' });
1415
+ if (response?.response?.items) {
1416
+ return response.response.items.map((p) => ({ name: p.name || `Project ${p.id}`, value: p.id.toString() }));
1417
+ }
1418
+ return [];
1419
+ }
1420
+ catch (error) {
1421
+ return [];
1422
+ }
1423
+ },
1424
+ async getTaskWorkflows() {
1425
+ try {
1426
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1427
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/task/workflows/list', apiKey);
1428
+ if (response?.response?.items) {
1429
+ return response.response.items.map((wf) => ({ name: wf.name || `Workflow ${wf.id}`, value: wf.id.toString() }));
1430
+ }
1431
+ return [];
1432
+ }
1433
+ catch (error) {
1434
+ return [];
1435
+ }
1436
+ },
1437
+ async getTaskWorkflowStages() {
1438
+ try {
1439
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1440
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/task/stages/list', apiKey);
1441
+ if (response?.response?.items) {
1442
+ return response.response.items.map((s) => ({ name: s.name || `Stage ${s.id}`, value: s.id.toString(), description: `Workflow ID: ${s.workflow_id}` }));
1443
+ }
1444
+ return [];
1445
+ }
1446
+ catch (error) {
1447
+ return [];
1448
+ }
1449
+ },
1450
+ async getPipelines() {
1451
+ try {
1452
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1453
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/pipeline/list', apiKey);
1454
+ if (response?.response?.items) {
1455
+ return response.response.items.map((p) => ({ name: p.name || `Pipeline ${p.id}`, value: p.id.toString() }));
1456
+ }
1457
+ return [];
1458
+ }
1459
+ catch (error) {
1460
+ return [];
1461
+ }
1462
+ },
1463
+ async getPipelineStages() {
1464
+ try {
1465
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1466
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/pipeline_stage/list', apiKey);
1467
+ if (response?.response?.items) {
1468
+ return response.response.items.map((s) => ({ name: s.name || `Stage ${s.id}`, value: s.id.toString(), description: `Pipeline ID: ${s.pipeline_id}` }));
1469
+ }
1470
+ return [];
1471
+ }
1472
+ catch (error) {
1473
+ return [];
1474
+ }
1475
+ },
1476
+ async getFilteredPipelineStages() {
1477
+ try {
1478
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1479
+ let pipelineId;
1480
+ try {
1481
+ pipelineId = this.getNodeParameter('opportunityFilterPipeline');
1482
+ }
1483
+ catch { /* not set yet */ }
1484
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/pipeline_stage/list', apiKey);
1485
+ if (response?.response?.items) {
1486
+ let stages = response.response.items;
1487
+ if (pipelineId) {
1488
+ stages = stages.filter((s) => s.pipeline_id?.toString() === pipelineId);
1489
+ }
1490
+ return stages.map((s) => ({ name: s.name || `Stage ${s.id}`, value: s.id.toString() }));
1491
+ }
1492
+ return [];
1493
+ }
1494
+ catch (error) {
1495
+ return [];
1496
+ }
1497
+ },
1498
+ async getAllCrmAccounts() {
1499
+ try {
1500
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1501
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/account/list', apiKey);
1502
+ if (response?.response?.items) {
1503
+ return response.response.items.map((a) => {
1504
+ const typeLabel = a.type === 1 ? '(Company)' : a.type === 2 ? '(Contact)' : '';
1505
+ return { name: `${a.name || `#${a.id}`} ${typeLabel}`.trim(), value: a.id.toString() };
1506
+ });
1507
+ }
1508
+ return [];
1509
+ }
1510
+ catch (error) {
1511
+ return [];
1512
+ }
1513
+ },
1514
+ async getOpportunitySources() {
1515
+ try {
1516
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1517
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/source/list', apiKey);
1518
+ if (response?.response?.items) {
1519
+ return response.response.items.map((s) => ({ name: s.name || `Source ${s.id}`, value: s.id.toString() }));
1520
+ }
1521
+ return [];
1522
+ }
1523
+ catch (error) {
1524
+ return [];
1525
+ }
1526
+ },
1527
+ async getPortfolios() {
1528
+ try {
1529
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1530
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/st/portfolio/list', apiKey);
1531
+ if (response?.response?.items) {
1532
+ return response.response.items.map((p) => ({ name: p.name || `Portfolio ${p.id}`, value: p.id.toString() }));
1533
+ }
1534
+ return [];
1535
+ }
1536
+ catch (error) {
1537
+ return [];
1538
+ }
1539
+ },
1540
+ async getProjectStages() {
1541
+ try {
1542
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1543
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/st/stages/list', apiKey);
1544
+ if (response?.response?.items) {
1545
+ return response.response.items.map((s) => ({ name: s.name || `Stage ${s.id}`, value: s.id.toString() }));
1546
+ }
1547
+ return [];
1548
+ }
1549
+ catch (error) {
1550
+ return [];
1551
+ }
1552
+ },
1553
+ async getRecordLists() {
1554
+ try {
1555
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1556
+ const response = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customlists/lists/list', apiKey);
1557
+ if (response?.response?.items) {
1558
+ return response.response.items.map((l) => ({ name: l.name || `List ${l.id}`, value: l.id.toString() }));
1559
+ }
1560
+ return [];
1561
+ }
1562
+ catch (error) {
1563
+ return [];
1564
+ }
1565
+ },
1566
+ async getRecordListFields() {
1567
+ try {
1568
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1569
+ // Get the selected list ID from the current node parameters
1570
+ let listId;
1571
+ try {
1572
+ listId = this.getNodeParameter('recordListId');
1573
+ }
1574
+ catch {
1575
+ return [];
1576
+ }
1577
+ if (!listId)
1578
+ return [];
1579
+ // Find the fieldset for this list (customlists/items with group_id matching list_id)
1580
+ const fieldsetsResponse = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customfields/fieldsets/list', apiKey);
1581
+ const fieldsetIds = [];
1582
+ if (fieldsetsResponse?.response?.items) {
1583
+ for (const fs of fieldsetsResponse.response.items) {
1584
+ if (fs.module === 'customlists' && fs.model === 'items' && fs.group_id?.toString() === listId) {
1585
+ fieldsetIds.push(fs.id.toString());
1586
+ }
1587
+ }
1588
+ }
1589
+ if (fieldsetIds.length === 0)
1590
+ return [];
1591
+ // Get fields for those fieldsets
1592
+ const fieldsResponse = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customfields/fields/list', apiKey);
1593
+ if (fieldsResponse?.response?.items) {
1594
+ // Also fetch all fieldsets so we can resolve custom list link names
1595
+ const allFieldsets = fieldsetsResponse?.response?.items || [];
1596
+ return fieldsResponse.response.items
1597
+ .filter((f) => fieldsetIds.includes(f.fieldset_id?.toString()))
1598
+ .filter((f) => f.active !== 0)
1599
+ .map((f) => {
1600
+ const cfKey = (f.api_use_alias && f.alias) ? `cf_${f.alias}` : `cf_${f.id}`;
1601
+ let label = f.name || cfKey;
1602
+ let desc = '';
1603
+ // Detect link fields (model.any) and show what they link to
1604
+ if (f.type === 'model.any' && f.module && f.model) {
1605
+ const linkTargetMap = {
1606
+ 'st/project': 'Project',
1607
+ 'crm/company': 'Company',
1608
+ 'crm/contact': 'Contact',
1609
+ 'crm/client': 'Account',
1610
+ 'crm/leads': 'Opportunity',
1611
+ 'system/user': 'User',
1612
+ };
1613
+ const key = `${f.module}/${f.model}`;
1614
+ if (linkTargetMap[key]) {
1615
+ label = `${f.name} → ${linkTargetMap[key]}`;
1616
+ desc = `Enter the ${linkTargetMap[key]} ID`;
1617
+ }
1618
+ else if (f.module === 'customlists' && f.model === 'items' && f.group_id) {
1619
+ // Link to another custom list - find its name
1620
+ const targetList = allFieldsets.find((fs) => fs.module === 'customlists' && fs.model === 'items' && fs.group_id?.toString() === f.group_id.toString());
1621
+ const listName = targetList?.name || `List #${f.group_id}`;
1622
+ label = `${f.name} → ${listName}`;
1623
+ desc = `Enter the Record ID from "${listName}"`;
1624
+ }
1625
+ else {
1626
+ label = `${f.name} → ${f.module}/${f.model}`;
1627
+ desc = `Enter the linked record ID`;
1628
+ }
1629
+ }
1630
+ const option = { name: label, value: cfKey };
1631
+ if (desc)
1632
+ option.description = desc;
1633
+ return option;
1634
+ });
1635
+ }
1636
+ return [];
1637
+ }
1638
+ catch (error) {
1639
+ return [];
1640
+ }
1641
+ },
1642
+ async getTaskCustomFields() {
1643
+ return loadCustomFieldsForEntity(this, 'task', 'task');
1644
+ },
1645
+ async getContactCustomFields() {
1646
+ return loadCustomFieldsForEntity(this, 'crm', 'contact');
1647
+ },
1648
+ async getProjectCustomFields() {
1649
+ return loadCustomFieldsForEntity(this, 'st', 'project');
1650
+ },
1651
+ async getOpportunityCustomFields() {
1652
+ return loadCustomFieldsForEntity(this, 'crm', 'leads');
1653
+ },
1654
+ },
1655
+ resourceMapping: {
1656
+ async getContactColumns() {
1657
+ return loadEntityCustomFieldColumns.call(this, 'crm', 'contact');
1658
+ },
1659
+ async getOpportunityColumns() {
1660
+ return loadEntityCustomFieldColumns.call(this, 'crm', 'leads');
1661
+ },
1662
+ async getProjectColumns() {
1663
+ return loadEntityCustomFieldColumns.call(this, 'st', 'project');
1664
+ },
1665
+ async getTaskColumns() {
1666
+ return loadEntityCustomFieldColumns.call(this, 'task', 'task');
1667
+ },
1668
+ async getRecordListColumns() {
1669
+ try {
1670
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1671
+ let listId;
1672
+ try {
1673
+ listId = this.getNodeParameter('recordListId');
1674
+ }
1675
+ catch {
1676
+ return { fields: [] };
1677
+ }
1678
+ if (!listId)
1679
+ return { fields: [] };
1680
+ // Get fieldsets to find this list's fieldset
1681
+ const fieldsetsResponse = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customfields/fieldsets/list', apiKey);
1682
+ const allFieldsets = fieldsetsResponse?.response?.items || [];
1683
+ const fieldsetIds = [];
1684
+ for (const fs of allFieldsets) {
1685
+ if (fs.module === 'customlists' && fs.model === 'items' && fs.group_id?.toString() === listId) {
1686
+ fieldsetIds.push(fs.id.toString());
1687
+ }
1688
+ }
1689
+ if (fieldsetIds.length === 0)
1690
+ return { fields: [] };
1691
+ // Get all custom fields
1692
+ const fieldsResponse = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customfields/fields/list', apiKey);
1693
+ if (!fieldsResponse?.response?.items)
1694
+ return { fields: [] };
1695
+ // Map of linked entity endpoints for fetching dropdown options
1696
+ const linkEndpoints = {
1697
+ 'st/project': '/api/v1/module/st/projects/list',
1698
+ 'crm/company': '/api/v1/module/crm/account/list',
1699
+ 'crm/contact': '/api/v1/module/crm/account/list',
1700
+ 'crm/client': '/api/v1/module/crm/account/list',
1701
+ 'crm/leads': '/api/v1/module/crm/lead/list',
1702
+ 'system/user': '/api/v1/module/core/user/list',
1703
+ };
1704
+ const linkLabels = {
1705
+ 'st/project': 'Project',
1706
+ 'crm/company': 'Company',
1707
+ 'crm/contact': 'Contact',
1708
+ 'crm/client': 'Account',
1709
+ 'crm/leads': 'Opportunity',
1710
+ 'system/user': 'User',
1711
+ };
1712
+ // Pre-fetch linked record options for all link field types we encounter
1713
+ const linkOptionsCache = {};
1714
+ const filteredFields = fieldsResponse.response.items
1715
+ .filter((f) => fieldsetIds.includes(f.fieldset_id?.toString()))
1716
+ .filter((f) => f.active !== 0);
1717
+ for (const f of filteredFields) {
1718
+ if (f.type === 'model.any' && f.module && f.model) {
1719
+ const key = `${f.module}/${f.model}`;
1720
+ if (linkEndpoints[key] && !linkOptionsCache[key]) {
1721
+ try {
1722
+ const qs = {};
1723
+ if (key === 'st/project')
1724
+ qs['filter[is_archive]'] = '0';
1725
+ if (key === 'crm/contact')
1726
+ qs['filter[type]'] = '2';
1727
+ if (key === 'crm/company')
1728
+ qs['filter[type]'] = '1';
1729
+ if (key === 'system/user')
1730
+ qs['filter[role_login]'] = '1';
1731
+ const resp = await flowluApiRequest.call(this, 'GET', baseUrl, linkEndpoints[key], apiKey, undefined, qs);
1732
+ linkOptionsCache[key] = (resp?.response?.items || []).map((item) => ({
1733
+ name: item.name || item.email || `#${item.id}`,
1734
+ value: item.id,
1735
+ }));
1736
+ }
1737
+ catch {
1738
+ linkOptionsCache[key] = [];
1739
+ }
1740
+ }
1741
+ // Handle links to other custom lists
1742
+ if (f.module === 'customlists' && f.model === 'items' && f.group_id) {
1743
+ const clKey = `customlists/${f.group_id}`;
1744
+ if (!linkOptionsCache[clKey]) {
1745
+ try {
1746
+ const resp = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customlists/items/list', apiKey, undefined, {
1747
+ 'filter[list_id]': f.group_id.toString(),
1748
+ });
1749
+ // Find the label field for this list
1750
+ const labelFieldId = f.group_label_field_id;
1751
+ linkOptionsCache[clKey] = (resp?.response?.items || []).map((item) => {
1752
+ const label = labelFieldId ? (item[`cf_${labelFieldId}`] || `#${item.id}`) : `#${item.id}`;
1753
+ return { name: label, value: item.id };
1754
+ });
1755
+ }
1756
+ catch {
1757
+ linkOptionsCache[clKey] = [];
1758
+ }
1759
+ }
1760
+ }
1761
+ }
1762
+ }
1763
+ const fields = filteredFields.map((f) => {
1764
+ const cfKey = (f.api_use_alias && f.alias) ? `cf_${f.alias}` : `cf_${f.id}`;
1765
+ let displayName = f.name || cfKey;
1766
+ let fieldType = 'string';
1767
+ let fieldOptions;
1768
+ if (f.type === 'int')
1769
+ fieldType = 'number';
1770
+ else if (f.type === 'boolean')
1771
+ fieldType = 'boolean';
1772
+ else if (f.type === 'date' || f.type === 'datetime')
1773
+ fieldType = 'dateTime';
1774
+ else if (f.type === 'price' || f.type === 'decimal')
1775
+ fieldType = 'number';
1776
+ // Dropdown fields (select.single)
1777
+ if (f.type === 'select.single') {
1778
+ fieldType = 'options';
1779
+ try {
1780
+ const opts = JSON.parse(f.extra_fields || '{}');
1781
+ const selectOptions = JSON.parse(opts.select_options || '[]');
1782
+ fieldOptions = selectOptions
1783
+ .filter((o) => o.value)
1784
+ .map((o) => ({ name: o.value, value: o.id?.toString() || o.value }));
1785
+ }
1786
+ catch { /* ignore parse errors */ }
1787
+ }
1788
+ // Link fields - use dropdown with fetched options
1789
+ if (f.type === 'model.any' && f.module && f.model) {
1790
+ const key = `${f.module}/${f.model}`;
1791
+ if (linkLabels[key]) {
1792
+ displayName = `${f.name} → ${linkLabels[key]}`;
1793
+ }
1794
+ else if (f.module === 'customlists' && f.model === 'items' && f.group_id) {
1795
+ const targetList = allFieldsets.find((fs) => fs.module === 'customlists' && fs.model === 'items' && fs.group_id?.toString() === f.group_id.toString());
1796
+ displayName = `${f.name} → ${targetList?.name || `List #${f.group_id}`}`;
1797
+ }
1798
+ const cacheKey = (f.module === 'customlists' && f.model === 'items' && f.group_id)
1799
+ ? `customlists/${f.group_id}`
1800
+ : key;
1801
+ if (linkOptionsCache[cacheKey]?.length) {
1802
+ fieldType = 'options';
1803
+ fieldOptions = linkOptionsCache[cacheKey];
1804
+ }
1805
+ else {
1806
+ fieldType = 'number';
1807
+ }
1808
+ }
1809
+ const field = {
1810
+ id: cfKey,
1811
+ displayName,
1812
+ required: f.required === 1,
1813
+ defaultMatch: false,
1814
+ display: true,
1815
+ type: fieldType,
1816
+ canBeUsedToMatch: false,
1817
+ };
1818
+ if (fieldOptions)
1819
+ field.options = fieldOptions;
1820
+ return field;
1821
+ });
1822
+ return { fields };
1823
+ }
1824
+ catch (error) {
1825
+ return { fields: [] };
1826
+ }
1827
+ },
1828
+ },
1829
+ };
1830
+ }
1831
+ async execute() {
1832
+ const items = this.getInputData();
1833
+ const returnData = [];
1834
+ const resource = this.getNodeParameter('resource', 0);
1835
+ const operation = this.getNodeParameter('operation', 0);
1836
+ for (let i = 0; i < items.length; i++) {
1837
+ try {
1838
+ const { baseUrl, apiKey } = await getFlowluCredentials(this);
1839
+ let responseData;
1840
+ // ============================
1841
+ // CONTACT
1842
+ // ============================
1843
+ if (resource === 'contact') {
1844
+ if (operation === 'create') {
1845
+ const body = {
1846
+ type: '2',
1847
+ first_name: this.getNodeParameter('first_name', i),
1848
+ last_name: this.getNodeParameter('last_name', i),
1849
+ };
1850
+ const email = this.getNodeParameter('email', i);
1851
+ if (email)
1852
+ body.email = email;
1853
+ const phone = this.getNodeParameter('phone', i);
1854
+ if (phone)
1855
+ body.phone = phone;
1856
+ const additional = this.getNodeParameter('contactAdditionalFields', i);
1857
+ const contactSimpleFields = [
1858
+ 'middle_name', 'description', 'owner_id', 'web',
1859
+ 'phone2', 'phone3', 'email_personal',
1860
+ 'social_network_link_1', 'social_network_link_2',
1861
+ 'social_network_link_3', 'social_network_link_4',
1862
+ 'social_network_link_5', 'social_network_link_6',
1863
+ 'address', 'shipping_address_line_1', 'shipping_city',
1864
+ 'shipping_state', 'shipping_zip', 'shipping_country',
1865
+ 'billing_address_line_1', 'billing_city', 'billing_state',
1866
+ 'billing_zip', 'billing_country', 'VAT', 'timezone',
1867
+ ];
1868
+ for (const field of contactSimpleFields) {
1869
+ if (additional[field] !== undefined && additional[field] !== '') {
1870
+ body[field] = additional[field];
1871
+ }
1872
+ }
1873
+ if (additional.birth_date) {
1874
+ body.birth_date = new Date(additional.birth_date).toISOString().split('T')[0];
1875
+ }
1876
+ applyResourceMapperFields(this, body, 'contactCustomFields', i);
1877
+ if (additional.includeLinkToWorkflow) {
1878
+ appendWorkflowFooter.call(this, body);
1879
+ }
1880
+ responseData = await flowluApiRequest.call(this, 'POST', baseUrl, '/api/v1/module/crm/account/create', apiKey, body);
1881
+ }
1882
+ else if (operation === 'get') {
1883
+ const id = this.getNodeParameter('contactId', i);
1884
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, `/api/v1/module/crm/account/get/${id}`, apiKey);
1885
+ }
1886
+ else if (operation === 'getAll') {
1887
+ const limit = this.getNodeParameter('limit', i);
1888
+ const filters = this.getNodeParameter('contactFilters', i);
1889
+ const qs = { limit: limit.toString(), 'filter[type]': '2' };
1890
+ if (filters.name)
1891
+ qs['filter[name]'] = filters.name;
1892
+ if (filters.email)
1893
+ qs['filter[email]'] = filters.email;
1894
+ if (filters.phone)
1895
+ qs['filter[phone]'] = filters.phone;
1896
+ if (filters.owner_id)
1897
+ qs['filter[owner_id]'] = filters.owner_id;
1898
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/account/list', apiKey, undefined, qs);
1899
+ for (const item of (responseData?.response?.items || [])) {
1900
+ returnData.push({ json: item });
1901
+ }
1902
+ continue;
1903
+ }
1904
+ else if (operation === 'update') {
1905
+ const id = this.getNodeParameter('contactId', i);
1906
+ const updateFields = this.getNodeParameter('contactUpdateFields', i);
1907
+ const body = { id };
1908
+ const contactUpdateSimpleFields = [
1909
+ 'first_name', 'last_name', 'middle_name', 'email', 'phone',
1910
+ 'phone2', 'phone3', 'description', 'owner_id', 'web',
1911
+ 'email_personal',
1912
+ 'social_network_link_1', 'social_network_link_2',
1913
+ 'social_network_link_3', 'social_network_link_4',
1914
+ 'social_network_link_5', 'social_network_link_6',
1915
+ 'address', 'VAT',
1916
+ ];
1917
+ for (const field of contactUpdateSimpleFields) {
1918
+ if (updateFields[field] !== undefined && updateFields[field] !== '') {
1919
+ body[field] = updateFields[field];
1920
+ }
1921
+ }
1922
+ if (updateFields.birth_date) {
1923
+ body.birth_date = new Date(updateFields.birth_date).toISOString().split('T')[0];
1924
+ }
1925
+ applyResourceMapperFields(this, body, 'contactUpdateCustomFields', i);
1926
+ if (updateFields.includeLinkToWorkflow && body.description) {
1927
+ appendWorkflowFooter.call(this, body);
1928
+ }
1929
+ responseData = await flowluApiRequest.call(this, 'POST', baseUrl, `/api/v1/module/crm/account/update/${id}`, apiKey, body);
1930
+ }
1931
+ else if (operation === 'delete') {
1932
+ const id = this.getNodeParameter('contactId', i);
1933
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, `/api/v1/module/crm/account/delete/${id}`, apiKey);
1934
+ }
1935
+ }
1936
+ // ============================
1937
+ // OPPORTUNITY
1938
+ // ============================
1939
+ if (resource === 'opportunity') {
1940
+ if (operation === 'create') {
1941
+ const body = {
1942
+ name: this.getNodeParameter('opportunityName', i),
1943
+ pipeline_id: this.getNodeParameter('pipeline_id', i),
1944
+ pipeline_stage_id: this.getNodeParameter('pipeline_stage_id', i),
1945
+ };
1946
+ const assignee = this.getNodeParameter('opportunityAssignee', i);
1947
+ if (assignee)
1948
+ body.assignee_id = assignee;
1949
+ const additional = this.getNodeParameter('opportunityAdditionalFields', i);
1950
+ const oppSimpleFields = [
1951
+ 'budget', 'description', 'contact_name', 'contact_email',
1952
+ 'contact_phone', 'contact_mobile', 'contact_company',
1953
+ 'contact_position', 'contact_web', 'source_id',
1954
+ ];
1955
+ for (const field of oppSimpleFields) {
1956
+ if (additional[field] !== undefined && additional[field] !== '') {
1957
+ body[field] = additional[field];
1958
+ }
1959
+ }
1960
+ if (additional.start_date) {
1961
+ body.start_date = new Date(additional.start_date).toISOString().split('T')[0];
1962
+ }
1963
+ if (additional.deadline) {
1964
+ body.deadline = new Date(additional.deadline).toISOString().split('T')[0];
1965
+ }
1966
+ applyResourceMapperFields(this, body, 'opportunityCustomFields', i);
1967
+ if (additional.includeLinkToWorkflow) {
1968
+ appendWorkflowFooter.call(this, body);
1969
+ }
1970
+ responseData = await flowluApiRequest.call(this, 'POST', baseUrl, '/api/v1/module/crm/lead/create', apiKey, body);
1971
+ // Link to CRM accounts/contacts after creation
1972
+ const leadId = responseData?.response?.id;
1973
+ if (leadId) {
1974
+ if (additional.link_account_id) {
1975
+ await flowluApiRequest.call(this, 'POST', baseUrl, '/api/v1/module/crm/lead_accounts/create', apiKey, {
1976
+ lead_id: leadId,
1977
+ account_id: additional.link_account_id,
1978
+ });
1979
+ }
1980
+ if (additional.link_contact_id) {
1981
+ await flowluApiRequest.call(this, 'POST', baseUrl, '/api/v1/module/crm/lead_accounts/create', apiKey, {
1982
+ lead_id: leadId,
1983
+ account_id: additional.link_contact_id,
1984
+ });
1985
+ }
1986
+ }
1987
+ }
1988
+ else if (operation === 'get') {
1989
+ const id = this.getNodeParameter('opportunityId', i);
1990
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, `/api/v1/module/crm/lead/get/${id}`, apiKey);
1991
+ }
1992
+ else if (operation === 'getAll') {
1993
+ const limit = this.getNodeParameter('limit', i);
1994
+ const pipelineId = this.getNodeParameter('opportunityFilterPipeline', i);
1995
+ const stageId = this.getNodeParameter('opportunityFilterStage', i);
1996
+ const filters = this.getNodeParameter('opportunityFilters', i);
1997
+ const qs = { limit: limit.toString() };
1998
+ if (pipelineId)
1999
+ qs['filter[pipeline_id]'] = pipelineId;
2000
+ if (stageId)
2001
+ qs['filter[pipeline_stage_id]'] = stageId;
2002
+ if (filters.name)
2003
+ qs['filter[name]'] = filters.name;
2004
+ if (filters.assignee_id)
2005
+ qs['filter[assignee_id]'] = filters.assignee_id;
2006
+ if (filters.company_id)
2007
+ qs['filter[company_id]'] = filters.company_id;
2008
+ if (filters.active !== undefined)
2009
+ qs['filter[active]'] = filters.active.toString();
2010
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/crm/lead/list', apiKey, undefined, qs);
2011
+ for (const item of (responseData?.response?.items || [])) {
2012
+ returnData.push({ json: item });
2013
+ }
2014
+ continue;
2015
+ }
2016
+ else if (operation === 'update') {
2017
+ const id = this.getNodeParameter('opportunityId', i);
2018
+ const updateFields = this.getNodeParameter('opportunityUpdateFields', i);
2019
+ const body = { id };
2020
+ const oppUpdateFields = [
2021
+ 'name', 'pipeline_id', 'pipeline_stage_id', 'assignee_id',
2022
+ 'budget', 'description', 'active', 'closing_comment',
2023
+ 'contact_name', 'contact_email', 'contact_phone',
2024
+ ];
2025
+ for (const field of oppUpdateFields) {
2026
+ if (updateFields[field] !== undefined && updateFields[field] !== '') {
2027
+ body[field] = updateFields[field];
2028
+ }
2029
+ }
2030
+ if (updateFields.start_date) {
2031
+ body.start_date = new Date(updateFields.start_date).toISOString().split('T')[0];
2032
+ }
2033
+ if (updateFields.deadline) {
2034
+ body.deadline = new Date(updateFields.deadline).toISOString().split('T')[0];
2035
+ }
2036
+ if (updateFields.active === 2 || updateFields.active === 3) {
2037
+ body.closing_date = new Date().toISOString().split('T')[0];
2038
+ }
2039
+ applyResourceMapperFields(this, body, 'opportunityUpdateCustomFields', i);
2040
+ if (updateFields.includeLinkToWorkflow && body.description) {
2041
+ appendWorkflowFooter.call(this, body);
2042
+ }
2043
+ responseData = await flowluApiRequest.call(this, 'POST', baseUrl, `/api/v1/module/crm/lead/update/${id}`, apiKey, body);
2044
+ }
2045
+ else if (operation === 'delete') {
2046
+ const id = this.getNodeParameter('opportunityId', i);
2047
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, `/api/v1/module/crm/lead/delete/${id}`, apiKey);
2048
+ }
2049
+ }
2050
+ // ============================
2051
+ // PROJECT
2052
+ // ============================
2053
+ if (resource === 'project') {
2054
+ if (operation === 'create') {
2055
+ const body = {
2056
+ name: this.getNodeParameter('projectName', i),
2057
+ };
2058
+ const managerId = this.getNodeParameter('manager_id', i);
2059
+ if (managerId)
2060
+ body.manager_id = managerId;
2061
+ const desc = this.getNodeParameter('projectDescription', i);
2062
+ if (desc)
2063
+ body.description = desc;
2064
+ const additional = this.getNodeParameter('projectAdditionalFields', i);
2065
+ const projSimpleFields = [
2066
+ 'priority', 'estimated_revenue', 'estimated_expenses',
2067
+ 'customer_id', 'customer_crm_contact_id', 'briefcase_id',
2068
+ 'stage_id', 'tasks_workflow_id', 'billing_type', 'crm_lead_id',
2069
+ ];
2070
+ for (const field of projSimpleFields) {
2071
+ if (additional[field] !== undefined && additional[field] !== '' && additional[field] !== 0) {
2072
+ body[field] = additional[field];
2073
+ }
2074
+ }
2075
+ if (additional.startdate) {
2076
+ body.startdate = new Date(additional.startdate).toISOString().split('T')[0];
2077
+ }
2078
+ if (additional.enddate) {
2079
+ body.enddate = new Date(additional.enddate).toISOString().split('T')[0];
2080
+ }
2081
+ applyResourceMapperFields(this, body, 'projectCustomFields', i);
2082
+ if (additional.includeLinkToWorkflow) {
2083
+ appendWorkflowFooter.call(this, body);
2084
+ }
2085
+ responseData = await flowluApiRequest.call(this, 'POST', baseUrl, '/api/v1/module/st/projects/create', apiKey, body);
2086
+ }
2087
+ else if (operation === 'get') {
2088
+ const id = this.getNodeParameter('projectId', i);
2089
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, `/api/v1/module/st/projects/get/${id}`, apiKey);
2090
+ }
2091
+ else if (operation === 'getAll') {
2092
+ const limit = this.getNodeParameter('limit', i);
2093
+ const filters = this.getNodeParameter('projectFilters', i);
2094
+ const qs = { limit: limit.toString() };
2095
+ if (filters.name)
2096
+ qs['filter[name]'] = filters.name;
2097
+ if (filters.manager_id)
2098
+ qs['filter[manager_id]'] = filters.manager_id;
2099
+ if (filters.customer_id)
2100
+ qs['filter[customer_id]'] = filters.customer_id;
2101
+ if (!filters.is_archive)
2102
+ qs['filter[is_archive]'] = '0';
2103
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/st/projects/list', apiKey, undefined, qs);
2104
+ for (const item of (responseData?.response?.items || [])) {
2105
+ returnData.push({ json: item });
2106
+ }
2107
+ continue;
2108
+ }
2109
+ else if (operation === 'update') {
2110
+ const id = this.getNodeParameter('projectId', i);
2111
+ const updateFields = this.getNodeParameter('projectUpdateFields', i);
2112
+ const body = { id };
2113
+ const projUpdateFields = [
2114
+ 'name', 'description', 'manager_id', 'priority',
2115
+ 'estimated_revenue', 'estimated_expenses', 'stage_id',
2116
+ ];
2117
+ for (const field of projUpdateFields) {
2118
+ if (updateFields[field] !== undefined && updateFields[field] !== '') {
2119
+ body[field] = updateFields[field];
2120
+ }
2121
+ }
2122
+ if (updateFields.startdate) {
2123
+ body.startdate = new Date(updateFields.startdate).toISOString().split('T')[0];
2124
+ }
2125
+ if (updateFields.enddate) {
2126
+ body.enddate = new Date(updateFields.enddate).toISOString().split('T')[0];
2127
+ }
2128
+ if (updateFields.is_archive !== undefined) {
2129
+ body.is_archive = updateFields.is_archive ? 1 : 0;
2130
+ }
2131
+ applyResourceMapperFields(this, body, 'projectUpdateCustomFields', i);
2132
+ if (updateFields.includeLinkToWorkflow && body.description) {
2133
+ appendWorkflowFooter.call(this, body);
2134
+ }
2135
+ responseData = await flowluApiRequest.call(this, 'POST', baseUrl, `/api/v1/module/st/projects/update/${id}`, apiKey, body);
2136
+ }
2137
+ else if (operation === 'delete') {
2138
+ const id = this.getNodeParameter('projectId', i);
2139
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, `/api/v1/module/st/projects/delete/${id}`, apiKey);
2140
+ }
2141
+ }
2142
+ // ============================
2143
+ // RECORD LIST
2144
+ // ============================
2145
+ if (resource === 'recordList') {
2146
+ const listId = this.getNodeParameter('recordListId', i);
2147
+ if (operation === 'create') {
2148
+ const body = { list_id: listId };
2149
+ const mappingValues = this.getNodeParameter('recordFields.value', i);
2150
+ for (const [key, value] of Object.entries(mappingValues)) {
2151
+ if (value !== undefined && value !== null && value !== '') {
2152
+ body[key] = value;
2153
+ }
2154
+ }
2155
+ responseData = await flowluApiRequest.call(this, 'POST', baseUrl, '/api/v1/module/customlists/items/create', apiKey, body);
2156
+ }
2157
+ else if (operation === 'get') {
2158
+ const id = this.getNodeParameter('recordId', i);
2159
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, `/api/v1/module/customlists/items/get/${id}`, apiKey);
2160
+ }
2161
+ else if (operation === 'getAll') {
2162
+ const limit = this.getNodeParameter('limit', i);
2163
+ const filters = this.getNodeParameter('recordListFilters', i);
2164
+ const qs = { limit: limit.toString(), 'filter[list_id]': listId };
2165
+ if (filters.search)
2166
+ qs['search'] = filters.search;
2167
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/customlists/items/list', apiKey, undefined, qs);
2168
+ for (const item of (responseData?.response?.items || [])) {
2169
+ returnData.push({ json: item });
2170
+ }
2171
+ continue;
2172
+ }
2173
+ else if (operation === 'update') {
2174
+ const id = this.getNodeParameter('recordId', i);
2175
+ const body = { id };
2176
+ const mappingValues = this.getNodeParameter('recordUpdateFields.value', i);
2177
+ for (const [key, value] of Object.entries(mappingValues)) {
2178
+ if (value !== undefined && value !== null && value !== '') {
2179
+ body[key] = value;
2180
+ }
2181
+ }
2182
+ responseData = await flowluApiRequest.call(this, 'POST', baseUrl, `/api/v1/module/customlists/items/update/${id}`, apiKey, body);
2183
+ }
2184
+ else if (operation === 'delete') {
2185
+ const id = this.getNodeParameter('recordId', i);
2186
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, `/api/v1/module/customlists/items/delete/${id}`, apiKey);
2187
+ }
2188
+ }
2189
+ // ============================
2190
+ // TASK
2191
+ // ============================
2192
+ if (resource === 'task') {
2193
+ if (operation === 'create') {
2194
+ const body = {
2195
+ name: this.getNodeParameter('taskName', i),
2196
+ responsible_id: this.getNodeParameter('responsible_id', i),
2197
+ };
2198
+ const description = this.getNodeParameter('description', i);
2199
+ if (description)
2200
+ body.description = description;
2201
+ const additional = this.getNodeParameter('taskAdditionalFields', i);
2202
+ body.workflow_id = additional.workflow_id || '1';
2203
+ body.workflow_stage_id = additional.workflow_stage_id || '1';
2204
+ if (additional.owner_id)
2205
+ body.owner_id = additional.owner_id;
2206
+ if (additional.contact_id)
2207
+ body.crm_account_id = parseInt(additional.contact_id, 10);
2208
+ if (additional.priority !== undefined)
2209
+ body.priority = additional.priority;
2210
+ if (additional.status !== undefined)
2211
+ body.status = additional.status;
2212
+ if (additional.plan_start_date) {
2213
+ body.plan_start_date = new Date(additional.plan_start_date).toISOString().split('T')[0];
2214
+ }
2215
+ if (additional.deadline) {
2216
+ body.deadline = new Date(additional.deadline).toISOString().split('T')[0];
2217
+ }
2218
+ if (additional.time_estimate && additional.time_estimate > 0) {
2219
+ body.time_estimate = Math.round(additional.time_estimate * 60);
2220
+ }
2221
+ if (additional.cost)
2222
+ body.cost = additional.cost;
2223
+ if (additional.price)
2224
+ body.price = additional.price;
2225
+ if (additional.task_checkbyowner !== undefined) {
2226
+ body.task_checkbyowner = additional.task_checkbyowner ? 1 : 0;
2227
+ }
2228
+ if (additional.deadline_allowchange !== undefined) {
2229
+ body.deadline_allowchange = additional.deadline_allowchange ? 1 : 0;
2230
+ }
2231
+ if (additional.parent_id && additional.parent_id > 0) {
2232
+ body.parent_id = additional.parent_id;
2233
+ }
2234
+ if (additional.model_id) {
2235
+ body.model_id = additional.model_id;
2236
+ body.model = 'project';
2237
+ body.module = 'st';
2238
+ }
2239
+ applyResourceMapperFields(this, body, 'taskCustomFields', i);
2240
+ if (additional.includeLinkToWorkflow) {
2241
+ appendWorkflowFooter.call(this, body);
2242
+ }
2243
+ responseData = await flowluApiRequest.call(this, 'POST', baseUrl, '/api/v1/module/task/tasks/create', apiKey, body);
2244
+ }
2245
+ else if (operation === 'get') {
2246
+ const id = this.getNodeParameter('taskId', i);
2247
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, `/api/v1/module/task/tasks/get/${id}`, apiKey);
2248
+ }
2249
+ else if (operation === 'getAll') {
2250
+ const limit = this.getNodeParameter('limit', i);
2251
+ const filters = this.getNodeParameter('taskFilters', i);
2252
+ const qs = { limit: limit.toString() };
2253
+ if (filters.name)
2254
+ qs['filter[name]'] = filters.name;
2255
+ if (filters.responsible_id)
2256
+ qs['filter[responsible_id]'] = filters.responsible_id;
2257
+ if (filters.model_id) {
2258
+ qs['filter[model_id]'] = filters.model_id;
2259
+ qs['filter[module]'] = 'st';
2260
+ qs['filter[model]'] = 'project';
2261
+ }
2262
+ if (filters.priority !== undefined)
2263
+ qs['filter[priority]'] = filters.priority.toString();
2264
+ if (filters.status !== undefined)
2265
+ qs['filter[status]'] = filters.status.toString();
2266
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/task/tasks/list', apiKey, undefined, qs);
2267
+ for (const item of (responseData?.response?.items || [])) {
2268
+ returnData.push({ json: item });
2269
+ }
2270
+ continue;
2271
+ }
2272
+ else if (operation === 'update') {
2273
+ const id = this.getNodeParameter('taskId', i);
2274
+ const updateFields = this.getNodeParameter('taskUpdateFields', i);
2275
+ const body = { id };
2276
+ if (updateFields.name)
2277
+ body.name = updateFields.name;
2278
+ if (updateFields.description)
2279
+ body.description = updateFields.description;
2280
+ if (updateFields.responsible_id)
2281
+ body.responsible_id = updateFields.responsible_id;
2282
+ if (updateFields.owner_id)
2283
+ body.owner_id = updateFields.owner_id;
2284
+ if (updateFields.workflow_id)
2285
+ body.workflow_id = updateFields.workflow_id;
2286
+ if (updateFields.workflow_stage_id)
2287
+ body.workflow_stage_id = updateFields.workflow_stage_id;
2288
+ if (updateFields.priority !== undefined)
2289
+ body.priority = updateFields.priority;
2290
+ if (updateFields.status !== undefined)
2291
+ body.status = updateFields.status;
2292
+ if (updateFields.plan_start_date) {
2293
+ body.plan_start_date = new Date(updateFields.plan_start_date).toISOString().split('T')[0];
2294
+ }
2295
+ if (updateFields.deadline) {
2296
+ body.deadline = new Date(updateFields.deadline).toISOString().split('T')[0];
2297
+ }
2298
+ if (updateFields.time_estimate && updateFields.time_estimate > 0) {
2299
+ body.time_estimate = Math.round(updateFields.time_estimate * 60);
2300
+ }
2301
+ if (updateFields.cost)
2302
+ body.cost = updateFields.cost;
2303
+ if (updateFields.price)
2304
+ body.price = updateFields.price;
2305
+ if (updateFields.parent_id && updateFields.parent_id > 0) {
2306
+ body.parent_id = updateFields.parent_id;
2307
+ }
2308
+ if (updateFields.model_id) {
2309
+ body.model_id = updateFields.model_id;
2310
+ body.model = 'project';
2311
+ body.module = 'st';
2312
+ }
2313
+ applyResourceMapperFields(this, body, 'taskUpdateCustomFields', i);
2314
+ if (updateFields.includeLinkToWorkflow && body.description) {
2315
+ appendWorkflowFooter.call(this, body);
2316
+ }
2317
+ responseData = await flowluApiRequest.call(this, 'POST', baseUrl, `/api/v1/module/task/tasks/update/${id}`, apiKey, body);
2318
+ }
2319
+ else if (operation === 'delete') {
2320
+ const id = this.getNodeParameter('taskId', i);
2321
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, `/api/v1/module/task/tasks/delete/${id}`, apiKey);
2322
+ }
2323
+ }
2324
+ // ============================
2325
+ // TAG
2326
+ // ============================
2327
+ if (resource === 'tag') {
2328
+ if (operation === 'add') {
2329
+ const entityType = this.getNodeParameter('tagEntityType', i);
2330
+ const entityId = this.getNodeParameter('tagEntityId', i);
2331
+ const tagName = this.getNodeParameter('tagName', i);
2332
+ responseData = await flowluApiRequest.call(this, 'POST', baseUrl, `/api/v1/module/${entityType}/${entityId}/global_tags/create`, apiKey, { name: tagName });
2333
+ }
2334
+ else if (operation === 'getAll') {
2335
+ const entityType = this.getNodeParameter('tagEntityType', i);
2336
+ const entityId = this.getNodeParameter('tagEntityId', i);
2337
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, `/api/v1/module/${entityType}/${entityId}/global_tags/list`, apiKey);
2338
+ for (const item of (responseData?.response?.items || [])) {
2339
+ returnData.push({ json: item });
2340
+ }
2341
+ continue;
2342
+ }
2343
+ else if (operation === 'listAll') {
2344
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, '/api/v1/module/core/tag/list', apiKey);
2345
+ for (const item of (responseData?.response?.items || [])) {
2346
+ returnData.push({ json: item });
2347
+ }
2348
+ continue;
2349
+ }
2350
+ else if (operation === 'remove') {
2351
+ const entityType = this.getNodeParameter('tagEntityType', i);
2352
+ const entityId = this.getNodeParameter('tagEntityId', i);
2353
+ const tagId = this.getNodeParameter('tagIdToRemove', i);
2354
+ responseData = await flowluApiRequest.call(this, 'POST', baseUrl, `/api/v1/module/${entityType}/${entityId}/global_tags/delete`, apiKey, { id: tagId });
2355
+ }
2356
+ }
2357
+ // ============================
2358
+ // COMMENT
2359
+ // ============================
2360
+ if (resource === 'comment') {
2361
+ if (operation === 'create') {
2362
+ const entityType = this.getNodeParameter('commentEntityType', i);
2363
+ const entityId = this.getNodeParameter('commentEntityId', i);
2364
+ const includeFooter = this.getNodeParameter('commentIncludeLinkToWorkflow', i, false);
2365
+ const body = { text: this.getNodeParameter('commentText', i) };
2366
+ if (includeFooter) {
2367
+ appendWorkflowFooter.call(this, body, 'text');
2368
+ }
2369
+ responseData = await flowluApiRequest.call(this, 'POST', baseUrl, `/api/v1/module/${entityType}/${entityId}/comments/create`, apiKey, body);
2370
+ }
2371
+ else if (operation === 'getAll') {
2372
+ const entityType = this.getNodeParameter('commentEntityType', i);
2373
+ const entityId = this.getNodeParameter('commentEntityId', i);
2374
+ const limit = this.getNodeParameter('limit', i);
2375
+ responseData = await flowluApiRequest.call(this, 'GET', baseUrl, `/api/v1/module/${entityType}/${entityId}/comments/list`, apiKey, undefined, { limit: limit.toString() });
2376
+ for (const item of (responseData?.response?.items || [])) {
2377
+ returnData.push({ json: item });
2378
+ }
2379
+ continue;
2380
+ }
2381
+ }
2382
+ if (responseData !== undefined) {
2383
+ returnData.push({ json: responseData });
2384
+ }
2385
+ }
2386
+ catch (error) {
2387
+ if (this.continueOnFail()) {
2388
+ returnData.push({ json: { error: error instanceof Error ? error.message : String(error) } });
2389
+ continue;
2390
+ }
2391
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error instanceof Error ? error : new Error(String(error)));
2392
+ }
2393
+ }
2394
+ return [this.helpers.returnJsonArray(returnData)];
2395
+ }
2396
+ }
2397
+ exports.Flowlu = Flowlu;