@trg-admin/n8n-nodes-zoho-desk 0.1.23 → 0.1.25

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.
package/README.md CHANGED
@@ -33,15 +33,19 @@ When you create the credential in n8n, the default scope is:
33
33
 
34
34
  ```
35
35
  ZohoCRM.modules.ALL Desk.tickets.ALL Desk.contacts.ALL Desk.search.READ
36
+ Desk.tasks.ALL Desk.basic.READ Desk.fields.READ Desk.settings.READ
37
+ Desk.webhooks.CREATE Desk.webhooks.READ Desk.webhooks.UPDATE Desk.webhooks.DELETE
36
38
  ```
37
39
 
38
40
  **Available Zoho Desk Scopes:**
39
41
  - `Desk.tickets.ALL` - Full access to tickets
40
42
  - `Desk.contacts.ALL` - Full access to contacts
41
43
  - `Desk.search.READ` - Search across modules
44
+ - `Desk.fields.READ` - Read field definitions (required for the Classification / Category / Sub-Category and Channel dropdowns)
42
45
  - `Desk.settings.ALL` - Access to settings and configurations
43
46
  - `Desk.tasks.ALL` - Task management
44
47
  - `Desk.basic.READ` - Read-only access to basic information
48
+ - `Desk.webhooks.*` - Webhook subscription management (required by the Zoho Desk Trigger node)
45
49
 
46
50
  You can customize the scope field in the credential to add or remove permissions as needed.
47
51
 
@@ -110,7 +110,7 @@ class ZohoCRMDeskOAuth2Api {
110
110
  displayName: 'Scope',
111
111
  name: 'scope',
112
112
  type: 'string',
113
- default: 'ZohoCRM.modules.ALL Desk.tickets.ALL Desk.contacts.ALL Desk.search.READ Desk.tasks.ALL Desk.basic.READ Desk.settings.READ Desk.webhooks.CREATE Desk.webhooks.READ Desk.webhooks.UPDATE Desk.webhooks.DELETE',
113
+ default: 'ZohoCRM.modules.ALL Desk.tickets.ALL Desk.contacts.ALL Desk.search.READ Desk.tasks.ALL Desk.basic.READ Desk.fields.READ Desk.settings.READ Desk.webhooks.CREATE Desk.webhooks.READ Desk.webhooks.UPDATE Desk.webhooks.DELETE',
114
114
  required: true,
115
115
  description: 'Space-separated OAuth scopes. Default includes both CRM and Desk permissions. Add or remove scopes as needed (e.g., Desk.settings.ALL, Desk.tasks.ALL).',
116
116
  },
@@ -1,4 +1,24 @@
1
1
  import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, ILoadOptionsFunctions } from 'n8n-workflow';
2
+ /**
3
+ * Load a ticket taxonomy picklist (classification / category / subCategory) for a dropdown.
4
+ *
5
+ * Zoho Desk has no /classifications, /categories or /subcategories endpoints — ticket
6
+ * taxonomy is exposed through the ticket module's organization field definitions.
7
+ *
8
+ * The ticket payload takes the taxonomy *name string* (there is no classificationId /
9
+ * categoryId / subCategoryId on a ticket), so the option value is the value itself.
10
+ *
11
+ * ponytail: the three levels are listed independently rather than cascaded. Whether a
12
+ * subCategory must be mapped under the chosen category is configured per-tenant (field
13
+ * dependencies) and is not documented as an API rule, so we list all values and let the
14
+ * API answer. Upgrade to GET /layouts/{layoutId}/fields/{fieldId}/dependentValues if users
15
+ * hit rejections in tenants that do enforce the mapping.
16
+ */
17
+ export declare function readDepartmentScope(context: ILoadOptionsFunctions): string;
18
+ export declare function loadTaxonomyOptions(context: ILoadOptionsFunctions, apiName: string): Promise<Array<{
19
+ name: string;
20
+ value: string;
21
+ }>>;
2
22
  export declare class ZohoDesk implements INodeType {
3
23
  description: INodeTypeDescription;
4
24
  methods: {
@@ -11,6 +31,18 @@ export declare class ZohoDesk implements INodeType {
11
31
  name: string;
12
32
  value: string;
13
33
  }[]>;
34
+ getClassifications(this: ILoadOptionsFunctions): Promise<{
35
+ name: string;
36
+ value: string;
37
+ }[]>;
38
+ getCategories(this: ILoadOptionsFunctions): Promise<{
39
+ name: string;
40
+ value: string;
41
+ }[]>;
42
+ getSubCategories(this: ILoadOptionsFunctions): Promise<{
43
+ name: string;
44
+ value: string;
45
+ }[]>;
14
46
  };
15
47
  };
16
48
  execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ZohoDesk = void 0;
4
+ exports.readDepartmentScope = readDepartmentScope;
5
+ exports.loadTaxonomyOptions = loadTaxonomyOptions;
4
6
  const n8n_workflow_1 = require("n8n-workflow");
5
7
  const GenericFunctions_1 = require("./GenericFunctions");
6
8
  function cleanCommaSeparated(value) {
@@ -13,6 +15,83 @@ function cleanCommaSeparated(value) {
13
15
  .filter(Boolean)
14
16
  .join(',');
15
17
  }
18
+ /**
19
+ * Load a ticket taxonomy picklist (classification / category / subCategory) for a dropdown.
20
+ *
21
+ * Zoho Desk has no /classifications, /categories or /subcategories endpoints — ticket
22
+ * taxonomy is exposed through the ticket module's organization field definitions.
23
+ *
24
+ * The ticket payload takes the taxonomy *name string* (there is no classificationId /
25
+ * categoryId / subCategoryId on a ticket), so the option value is the value itself.
26
+ *
27
+ * ponytail: the three levels are listed independently rather than cascaded. Whether a
28
+ * subCategory must be mapped under the chosen category is configured per-tenant (field
29
+ * dependencies) and is not documented as an API rule, so we list all values and let the
30
+ * API answer. Upgrade to GET /layouts/{layoutId}/fields/{fieldId}/dependentValues if users
31
+ * hit rejections in tenants that do enforce the mapping.
32
+ */
33
+ function readDepartmentScope(context) {
34
+ const ctx = context;
35
+ // Top level: the ticket create form carries departmentId directly.
36
+ try {
37
+ const current = ctx.getCurrentNodeParameter?.('departmentId');
38
+ if (current !== undefined && current !== null && current !== '') {
39
+ return String(current).trim();
40
+ }
41
+ }
42
+ catch {
43
+ // Not available in this context; fall through.
44
+ }
45
+ // Nested: on update, Department lives in the Additional Fields collection. Values inside
46
+ // a collection are invisible to getCurrentNodeParameter, so read the raw parameters.
47
+ try {
48
+ const nested = ctx.getCurrentNodeParameters?.()?.additionalFields
49
+ ?.departmentId;
50
+ if (nested !== undefined && nested !== null && nested !== '') {
51
+ return String(nested).trim();
52
+ }
53
+ }
54
+ catch {
55
+ // Treat as unscoped.
56
+ }
57
+ return '';
58
+ }
59
+ async function loadTaxonomyOptions(context, apiName) {
60
+ // Taxonomy is department-scoped: without a department the API returns the whole org,
61
+ // which offers values that the target department's layout may reject.
62
+ const departmentId = readDepartmentScope(context);
63
+ const qs = { module: 'tickets', apiNames: apiName };
64
+ if (departmentId) {
65
+ qs.departmentId = departmentId;
66
+ }
67
+ try {
68
+ const responseData = await GenericFunctions_1.zohoDeskApiRequest.call(context, 'GET', '/organizationFields', {}, qs, 0);
69
+ const fields = Array.isArray(responseData?.data)
70
+ ? responseData.data
71
+ : Array.isArray(responseData)
72
+ ? responseData
73
+ : [];
74
+ const field = fields.find((f) => String(f.apiName || '').toLowerCase() === apiName.toLowerCase());
75
+ // Field definitions carry picklist values under either key depending on tenant.
76
+ const rawValues = (field?.pickListValues ?? field?.allowedValues ?? []);
77
+ const seen = new Set();
78
+ const options = [];
79
+ for (const entry of rawValues) {
80
+ const value = typeof entry === 'string'
81
+ ? entry
82
+ : (entry.value || entry.displayValue || '');
83
+ if (!value || seen.has(value)) {
84
+ continue;
85
+ }
86
+ seen.add(value);
87
+ options.push({ name: value, value });
88
+ }
89
+ return options.sort((a, b) => a.name.localeCompare(b.name));
90
+ }
91
+ catch (error) {
92
+ throw new Error(`Failed to load "${apiName}" values from the ticket layout. Error: ${error.message}`);
93
+ }
94
+ }
16
95
  class ZohoDesk {
17
96
  constructor() {
18
97
  this.description = {
@@ -185,10 +264,57 @@ class ZohoDesk {
185
264
  type: 'options',
186
265
  typeOptions: {
187
266
  loadOptionsMethod: 'getChannels',
188
- loadOptionsDependsOn: ['organizationId'],
267
+ loadOptionsDependsOn: ['organizationId', 'departmentId'],
189
268
  },
190
269
  default: 'Email',
191
- description: 'Channel through which the ticket was received or created. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
270
+ description: 'Channel through which the ticket was received or created, scoped to the selected Department. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
271
+ },
272
+ {
273
+ // Create already has a top-level required Department, so only offer this
274
+ // here for Update - it both scopes the taxonomy values and moves the ticket.
275
+ displayName: 'Department',
276
+ name: 'departmentId',
277
+ type: 'options',
278
+ typeOptions: {
279
+ loadOptionsMethod: 'getDepartments',
280
+ loadOptionsDependsOn: ['organizationId'],
281
+ },
282
+ default: '',
283
+ displayOptions: { show: { resource: ['ticket'], operation: ['update'] } },
284
+ description: 'Department whose layout defines the values offered by the Classification, Category and Sub-Category fields below. Leave blank to list values from every department in the organization. Note: setting this also moves the ticket into that department, so set it to the ticket\'s current department unless you intend to move it.',
285
+ },
286
+ {
287
+ displayName: 'Classification',
288
+ name: 'classification',
289
+ type: 'options',
290
+ typeOptions: {
291
+ loadOptionsMethod: 'getClassifications',
292
+ loadOptionsDependsOn: ['organizationId', 'departmentId'],
293
+ },
294
+ default: '',
295
+ description: 'Top-level ticket classification, as configured on the department\'s ticket layout. Choose from the list, or specify a value using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
296
+ },
297
+ {
298
+ displayName: 'Category',
299
+ name: 'category',
300
+ type: 'options',
301
+ typeOptions: {
302
+ loadOptionsMethod: 'getCategories',
303
+ loadOptionsDependsOn: ['organizationId', 'departmentId'],
304
+ },
305
+ default: '',
306
+ description: 'Ticket category, as configured on the department\'s ticket layout. Choose from the list, or specify a value using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
307
+ },
308
+ {
309
+ displayName: 'Sub-Category',
310
+ name: 'subCategory',
311
+ type: 'options',
312
+ typeOptions: {
313
+ loadOptionsMethod: 'getSubCategories',
314
+ loadOptionsDependsOn: ['organizationId', 'departmentId'],
315
+ },
316
+ default: '',
317
+ description: 'Ticket sub-category, as configured on the department\'s ticket layout. Choose from the list, or specify a value using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
192
318
  },
193
319
  {
194
320
  displayName: 'Contact ID',
@@ -836,7 +962,13 @@ class ZohoDesk {
836
962
  }
837
963
  catch {
838
964
  try {
839
- const fieldsResponse = await GenericFunctions_1.zohoDeskApiRequest.call(this, 'GET', '/organizationFields', {}, { module: 'tickets' }, 0);
965
+ // Channel is department-scoped too, so honour the same scope as the taxonomy.
966
+ const channelQs = { module: 'tickets', apiNames: 'channel' };
967
+ const channelDepartmentId = readDepartmentScope(this);
968
+ if (channelDepartmentId) {
969
+ channelQs.departmentId = channelDepartmentId;
970
+ }
971
+ const fieldsResponse = await GenericFunctions_1.zohoDeskApiRequest.call(this, 'GET', '/organizationFields', {}, channelQs, 0);
840
972
  const fields = Array.isArray(fieldsResponse.data)
841
973
  ? fieldsResponse.data
842
974
  : Array.isArray(fieldsResponse)
@@ -875,6 +1007,15 @@ class ZohoDesk {
875
1007
  }
876
1008
  return fallbackChannels;
877
1009
  },
1010
+ async getClassifications() {
1011
+ return loadTaxonomyOptions(this, 'classification');
1012
+ },
1013
+ async getCategories() {
1014
+ return loadTaxonomyOptions(this, 'category');
1015
+ },
1016
+ async getSubCategories() {
1017
+ return loadTaxonomyOptions(this, 'subCategory');
1018
+ },
878
1019
  },
879
1020
  };
880
1021
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trg-admin/n8n-nodes-zoho-desk",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Community n8n node starter for Zoho Desk using built-in Zoho OAuth2 auth behavior",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -10,7 +10,7 @@
10
10
  "copy-assets": "cp nodes/ZohoDesk/zohodesk.png dist/nodes/ZohoDesk/",
11
11
  "lint": "echo 'No linter configured'",
12
12
  "typecheck": "tsc --noEmit",
13
- "test": "node scripts.validate.js",
13
+ "test": "node scripts.validate.js && node test/Taxonomy.test.js",
14
14
  "prepublishOnly": "npm run typecheck && npm run build"
15
15
  },
16
16
  "files": [