Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
2546 lines
70 KiB
TypeScript
2546 lines
70 KiB
TypeScript
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
|
|
import type {
|
|
IDataObject,
|
|
IExecuteFunctions,
|
|
ILoadOptionsFunctions,
|
|
INodeExecutionData,
|
|
INodeListSearchResult,
|
|
INodePropertyOptions,
|
|
INodeType,
|
|
INodeTypeDescription,
|
|
JsonObject,
|
|
} from 'n8n-workflow';
|
|
import {
|
|
buildContactCreatePayload,
|
|
buildContactListQuery,
|
|
buildContactUpdatePayload,
|
|
buildProjectTaskCreatePayload,
|
|
buildProjectTaskListQuery,
|
|
buildProjectTaskUpdatePayload,
|
|
buildTicketCommentListQuery,
|
|
buildTicketCommentPayload,
|
|
buildTicketCreatePayload,
|
|
buildTicketListQuery,
|
|
buildTicketSearchQuery,
|
|
buildTicketUpdatePayload,
|
|
compactObject,
|
|
ensureNonEmpty,
|
|
ensureUuid,
|
|
extractResourceLocatorValue,
|
|
formatAlgaApiError,
|
|
normalizeDeleteSuccess,
|
|
normalizeSuccessResponse,
|
|
parseCsvList,
|
|
UUID_REGEX,
|
|
} from './helpers';
|
|
import { algaApiRequest } from './transport';
|
|
|
|
type Resource =
|
|
| 'ticket'
|
|
| 'contact'
|
|
| 'projectTask'
|
|
| 'client'
|
|
| 'board'
|
|
| 'status'
|
|
| 'priority';
|
|
type StatusType = 'ticket' | 'project' | 'project_task' | 'interaction';
|
|
type ContactOperation = 'create' | 'get' | 'list' | 'update' | 'delete';
|
|
type ProjectTaskOperation = 'create' | 'get' | 'list' | 'update' | 'delete';
|
|
type TicketOperation =
|
|
| 'create'
|
|
| 'get'
|
|
| 'list'
|
|
| 'listComments'
|
|
| 'search'
|
|
| 'update'
|
|
| 'addComment'
|
|
| 'updateStatus'
|
|
| 'updateAssignment'
|
|
| 'delete';
|
|
|
|
const LOOKUP_PAGE_SIZE = 100;
|
|
type HelperResource = Exclude<Resource, 'ticket' | 'contact' | 'projectTask'>;
|
|
|
|
function ensureDataArray(response: unknown): IDataObject[] {
|
|
const normalized = normalizeSuccessResponse(response);
|
|
const data = normalized.data;
|
|
if (Array.isArray(data)) {
|
|
return data as IDataObject[];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function getLookupLabel(record: IDataObject, candidates: string[]): string {
|
|
for (const candidate of candidates) {
|
|
const value = record[candidate];
|
|
if (value !== undefined && value !== null && String(value).trim()) {
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
async function loadLookup(
|
|
context: ILoadOptionsFunctions,
|
|
endpoint: string,
|
|
idField: string,
|
|
labelFields: string[],
|
|
filter?: string,
|
|
extraQuery?: IDataObject,
|
|
): Promise<INodeListSearchResult> {
|
|
try {
|
|
const query = compactObject({
|
|
page: 1,
|
|
limit: LOOKUP_PAGE_SIZE,
|
|
search: filter?.trim(),
|
|
...(extraQuery ?? {}),
|
|
});
|
|
|
|
const response = await algaApiRequest(context, 'GET', endpoint, query);
|
|
const records = ensureDataArray(response);
|
|
|
|
return {
|
|
results: records
|
|
.map((record) => {
|
|
const id = record[idField];
|
|
if (!id) {
|
|
return null;
|
|
}
|
|
|
|
const label = getLookupLabel(record, labelFields) || String(id);
|
|
return {
|
|
name: label,
|
|
value: String(id),
|
|
};
|
|
})
|
|
.filter((entry): entry is { name: string; value: string } => entry !== null),
|
|
};
|
|
} catch {
|
|
// Allow manual ID fallback when lookups are unavailable.
|
|
return { results: [] };
|
|
}
|
|
}
|
|
|
|
function getCurrentStatusLookupType(context: ILoadOptionsFunctions): StatusType | undefined {
|
|
const currentParams = context.getCurrentNodeParameters?.();
|
|
const resource = currentParams?.resource as Resource | undefined;
|
|
|
|
if (resource === 'ticket') {
|
|
return 'ticket';
|
|
}
|
|
|
|
if (resource === 'status') {
|
|
return currentParams?.helperStatusType as StatusType | undefined;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function getCurrentProjectLookupId(context: ILoadOptionsFunctions): string | undefined {
|
|
const currentParams = context.getCurrentNodeParameters?.();
|
|
const value = extractResourceLocatorValue(currentParams?.projectTaskProjectId);
|
|
return value && UUID_REGEX.test(value) ? value : undefined;
|
|
}
|
|
|
|
function getCurrentStatusBoardId(context: ILoadOptionsFunctions): string | undefined {
|
|
const currentParams = context.getCurrentNodeParameters?.();
|
|
if (!currentParams) {
|
|
return undefined;
|
|
}
|
|
|
|
const resource = currentParams.resource as Resource | undefined;
|
|
|
|
if (resource === 'ticket') {
|
|
const ticketOperation = currentParams.ticketOperation as TicketOperation | undefined;
|
|
|
|
if (ticketOperation === 'create') {
|
|
return extractResourceLocatorValue(currentParams.board_id) || undefined;
|
|
}
|
|
|
|
if (ticketOperation === 'update') {
|
|
const updateAdditional = currentParams.updateAdditionalFields as IDataObject | undefined;
|
|
return extractResourceLocatorValue(updateAdditional?.board_id) || undefined;
|
|
}
|
|
|
|
if (ticketOperation === 'updateStatus') {
|
|
return extractResourceLocatorValue(currentParams.statusFilterBoardId) || undefined;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
if (resource === 'status') {
|
|
return extractResourceLocatorValue(currentParams.helperBoardId) || undefined;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function getOperationParameterName(resource: Resource): string {
|
|
switch (resource) {
|
|
case 'ticket':
|
|
return 'ticketOperation';
|
|
case 'contact':
|
|
return 'contactOperation';
|
|
case 'projectTask':
|
|
return 'projectTaskOperation';
|
|
case 'client':
|
|
return 'clientOperation';
|
|
case 'board':
|
|
return 'boardOperation';
|
|
case 'status':
|
|
return 'statusOperation';
|
|
case 'priority':
|
|
return 'priorityOperation';
|
|
}
|
|
}
|
|
|
|
function getHelperEndpoint(resource: HelperResource): string {
|
|
switch (resource) {
|
|
case 'client':
|
|
return '/api/v1/clients';
|
|
case 'board':
|
|
return '/api/v1/boards';
|
|
case 'status':
|
|
return '/api/v1/statuses';
|
|
case 'priority':
|
|
return '/api/v1/priorities';
|
|
}
|
|
}
|
|
|
|
export class AlgaPsa implements INodeType {
|
|
description: INodeTypeDescription = {
|
|
displayName: 'Alga PSA',
|
|
name: 'algaPsa',
|
|
icon: 'file:avatar-purple.png',
|
|
group: ['transform'],
|
|
version: 1,
|
|
subtitle:
|
|
'={{$parameter["resource"] + ": " + ($parameter["ticketOperation"] || $parameter["contactOperation"] || $parameter["projectTaskOperation"] || $parameter["clientOperation"] || $parameter["boardOperation"] || $parameter["statusOperation"] || $parameter["priorityOperation"])}}',
|
|
description: 'Create and manage Alga PSA tickets, contacts, project tasks, and lookup resources',
|
|
defaults: {
|
|
name: 'Alga PSA',
|
|
},
|
|
inputs: ['main'],
|
|
outputs: ['main'],
|
|
credentials: [
|
|
{
|
|
name: 'algaPsaApi',
|
|
required: true,
|
|
},
|
|
],
|
|
properties: [
|
|
{
|
|
displayName: 'Resource',
|
|
name: 'resource',
|
|
type: 'options',
|
|
noDataExpression: true,
|
|
options: [
|
|
{ name: 'Ticket', value: 'ticket' },
|
|
{ name: 'Contact', value: 'contact' },
|
|
{ name: 'Project Task', value: 'projectTask' },
|
|
{ name: 'Client', value: 'client' },
|
|
{ name: 'Board', value: 'board' },
|
|
{ name: 'Status', value: 'status' },
|
|
{ name: 'Priority', value: 'priority' },
|
|
],
|
|
default: 'ticket',
|
|
},
|
|
{
|
|
displayName: 'Operation',
|
|
name: 'ticketOperation',
|
|
type: 'options',
|
|
noDataExpression: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
},
|
|
},
|
|
options: [
|
|
{ name: 'Create', value: 'create', action: 'Create a ticket' },
|
|
{ name: 'Get', value: 'get', action: 'Get a ticket' },
|
|
{ name: 'List', value: 'list', action: 'List tickets' },
|
|
{
|
|
name: 'List Comments',
|
|
value: 'listComments',
|
|
action: 'List comments for a ticket',
|
|
},
|
|
{ name: 'Search', value: 'search', action: 'Search tickets' },
|
|
{ name: 'Update', value: 'update', action: 'Update a ticket' },
|
|
{
|
|
name: 'Add Comment',
|
|
value: 'addComment',
|
|
action: 'Add a comment to a ticket',
|
|
},
|
|
{
|
|
name: 'Update Status',
|
|
value: 'updateStatus',
|
|
action: 'Update ticket status',
|
|
},
|
|
{
|
|
name: 'Update Assignment',
|
|
value: 'updateAssignment',
|
|
action: 'Update ticket assignment',
|
|
},
|
|
{ name: 'Delete', value: 'delete', action: 'Delete a ticket' },
|
|
],
|
|
default: 'create',
|
|
},
|
|
{
|
|
displayName: 'Operation',
|
|
name: 'contactOperation',
|
|
type: 'options',
|
|
noDataExpression: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['contact'],
|
|
},
|
|
},
|
|
options: [
|
|
{ name: 'Create', value: 'create', action: 'Create a contact' },
|
|
{ name: 'Get', value: 'get', action: 'Get a contact' },
|
|
{ name: 'List', value: 'list', action: 'List contacts' },
|
|
{ name: 'Update', value: 'update', action: 'Update a contact' },
|
|
{ name: 'Delete', value: 'delete', action: 'Delete a contact' },
|
|
],
|
|
default: 'create',
|
|
},
|
|
{
|
|
displayName: 'Operation',
|
|
name: 'projectTaskOperation',
|
|
type: 'options',
|
|
noDataExpression: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['projectTask'],
|
|
},
|
|
},
|
|
options: [
|
|
{ name: 'Create', value: 'create', action: 'Create a project task' },
|
|
{ name: 'Get', value: 'get', action: 'Get a project task' },
|
|
{ name: 'List', value: 'list', action: 'List project tasks' },
|
|
{ name: 'Update', value: 'update', action: 'Update a project task' },
|
|
{ name: 'Delete', value: 'delete', action: 'Delete a project task' },
|
|
],
|
|
default: 'create',
|
|
},
|
|
{
|
|
displayName: 'Operation',
|
|
name: 'clientOperation',
|
|
type: 'options',
|
|
noDataExpression: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['client'],
|
|
},
|
|
},
|
|
options: [{ name: 'List', value: 'list', action: 'List clients' }],
|
|
default: 'list',
|
|
},
|
|
{
|
|
displayName: 'Operation',
|
|
name: 'boardOperation',
|
|
type: 'options',
|
|
noDataExpression: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['board'],
|
|
},
|
|
},
|
|
options: [{ name: 'List', value: 'list', action: 'List boards' }],
|
|
default: 'list',
|
|
},
|
|
{
|
|
displayName: 'Operation',
|
|
name: 'statusOperation',
|
|
type: 'options',
|
|
noDataExpression: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['status'],
|
|
},
|
|
},
|
|
options: [{ name: 'List', value: 'list', action: 'List statuses' }],
|
|
default: 'list',
|
|
},
|
|
{
|
|
displayName: 'Operation',
|
|
name: 'priorityOperation',
|
|
type: 'options',
|
|
noDataExpression: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['priority'],
|
|
},
|
|
},
|
|
options: [{ name: 'List', value: 'list', action: 'List priorities' }],
|
|
default: 'list',
|
|
},
|
|
|
|
// Ticket fields
|
|
{
|
|
displayName: 'Title',
|
|
name: 'title',
|
|
type: 'string',
|
|
required: true,
|
|
default: '',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['create'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Client ID',
|
|
name: 'client_id',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
required: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['create'],
|
|
},
|
|
},
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchClients',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
description: 'Select a client or enter a client UUID manually',
|
|
},
|
|
{
|
|
displayName: 'Board ID',
|
|
name: 'board_id',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
required: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['create'],
|
|
},
|
|
},
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchBoards',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
description: 'Select a board or enter a board UUID manually',
|
|
},
|
|
{
|
|
displayName: 'Board ID (for Status Picker)',
|
|
name: 'statusFilterBoardId',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['updateStatus'],
|
|
},
|
|
},
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchBoards',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
description:
|
|
"Optional — scopes the Status ID dropdown to one board. Ticket statuses are board-owned, so the picker needs a board to list options. Not sent in the request; the server validates against the ticket's existing board.",
|
|
},
|
|
{
|
|
displayName: 'Status ID',
|
|
name: 'status_id',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
required: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['create', 'updateStatus'],
|
|
},
|
|
},
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchStatuses',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
description:
|
|
'Select a status or enter a status UUID manually. Ticket statuses are board-owned, so the dropdown is filtered by the selected board',
|
|
},
|
|
{
|
|
displayName: 'Priority ID',
|
|
name: 'priority_id',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
required: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['create'],
|
|
},
|
|
},
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchPriorities',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
description: 'Select a priority or enter a priority UUID manually',
|
|
},
|
|
{
|
|
displayName: 'Ticket ID',
|
|
name: 'ticketId',
|
|
type: 'string',
|
|
required: true,
|
|
default: '',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: [
|
|
'get',
|
|
'update',
|
|
'listComments',
|
|
'addComment',
|
|
'updateStatus',
|
|
'updateAssignment',
|
|
'delete',
|
|
],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Comment List Options',
|
|
name: 'commentListOptions',
|
|
type: 'collection',
|
|
default: {},
|
|
placeholder: 'Add Option',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['listComments'],
|
|
},
|
|
},
|
|
options: [
|
|
{
|
|
displayName: 'Limit',
|
|
name: 'limit',
|
|
type: 'number',
|
|
default: 50,
|
|
typeOptions: {
|
|
minValue: 1,
|
|
maxValue: 200,
|
|
numberPrecision: 0,
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Offset',
|
|
name: 'offset',
|
|
type: 'number',
|
|
default: 0,
|
|
typeOptions: {
|
|
minValue: 0,
|
|
numberPrecision: 0,
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Order',
|
|
name: 'order',
|
|
type: 'options',
|
|
default: 'asc',
|
|
options: [
|
|
{ name: 'Ascending', value: 'asc' },
|
|
{ name: 'Descending', value: 'desc' },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Comment Text',
|
|
name: 'commentText',
|
|
type: 'string',
|
|
required: true,
|
|
default: '',
|
|
typeOptions: {
|
|
rows: 4,
|
|
},
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['addComment'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Comment Additional Fields',
|
|
name: 'commentAdditionalFields',
|
|
type: 'collection',
|
|
default: {},
|
|
placeholder: 'Add Field',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['addComment'],
|
|
},
|
|
},
|
|
options: [
|
|
{
|
|
displayName: 'Is Internal',
|
|
name: 'is_internal',
|
|
type: 'boolean',
|
|
default: false,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Assignment Action',
|
|
name: 'assignmentAction',
|
|
type: 'options',
|
|
default: 'assign',
|
|
options: [
|
|
{ name: 'Assign User', value: 'assign' },
|
|
{ name: 'Clear Assignment', value: 'clear' },
|
|
],
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['updateAssignment'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Assigned To (User ID)',
|
|
name: 'assigned_to',
|
|
type: 'string',
|
|
required: true,
|
|
default: '',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['updateAssignment'],
|
|
assignmentAction: ['assign'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Page',
|
|
name: 'page',
|
|
type: 'number',
|
|
default: 1,
|
|
typeOptions: {
|
|
minValue: 1,
|
|
numberPrecision: 0,
|
|
},
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['list'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Limit',
|
|
name: 'limit',
|
|
type: 'number',
|
|
default: 25,
|
|
typeOptions: {
|
|
minValue: 1,
|
|
maxValue: 100,
|
|
numberPrecision: 0,
|
|
},
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['list'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Sort',
|
|
name: 'sort',
|
|
type: 'string',
|
|
default: 'entered_at',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['list'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Order',
|
|
name: 'order',
|
|
type: 'options',
|
|
default: 'desc',
|
|
options: [
|
|
{ name: 'Ascending', value: 'asc' },
|
|
{ name: 'Descending', value: 'desc' },
|
|
],
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['list'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'List Filters',
|
|
name: 'listFilters',
|
|
type: 'collection',
|
|
default: {},
|
|
placeholder: 'Add Filter',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['list'],
|
|
},
|
|
},
|
|
options: [
|
|
{ displayName: 'Title', name: 'title', type: 'string', default: '' },
|
|
{
|
|
displayName: 'Ticket Number',
|
|
name: 'ticket_number',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{ displayName: 'Client ID', name: 'client_id', type: 'string', default: '' },
|
|
{ displayName: 'Board ID', name: 'board_id', type: 'string', default: '' },
|
|
{ displayName: 'Status ID', name: 'status_id', type: 'string', default: '' },
|
|
{
|
|
displayName: 'Priority ID',
|
|
name: 'priority_id',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Assigned To',
|
|
name: 'assigned_to',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Is Open',
|
|
name: 'is_open',
|
|
type: 'boolean',
|
|
default: false,
|
|
},
|
|
{
|
|
displayName: 'Is Closed',
|
|
name: 'is_closed',
|
|
type: 'boolean',
|
|
default: false,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Search Query',
|
|
name: 'query',
|
|
type: 'string',
|
|
required: true,
|
|
default: '',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['search'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Search Limit',
|
|
name: 'searchLimit',
|
|
type: 'number',
|
|
default: 25,
|
|
typeOptions: {
|
|
minValue: 1,
|
|
maxValue: 100,
|
|
numberPrecision: 0,
|
|
},
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['search'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Search Filters',
|
|
name: 'searchAdditionalFields',
|
|
type: 'collection',
|
|
default: {},
|
|
placeholder: 'Add Search Filter',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['search'],
|
|
},
|
|
},
|
|
options: [
|
|
{
|
|
displayName: 'Include Closed',
|
|
name: 'include_closed',
|
|
type: 'boolean',
|
|
default: false,
|
|
},
|
|
{
|
|
displayName: 'Fields',
|
|
name: 'fields',
|
|
type: 'multiOptions',
|
|
default: [],
|
|
options: [
|
|
{ name: 'Title', value: 'title' },
|
|
{ name: 'Ticket Number', value: 'ticket_number' },
|
|
{ name: 'Client Name', value: 'client_name' },
|
|
{ name: 'Contact Name', value: 'contact_name' },
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Status IDs',
|
|
name: 'status_ids',
|
|
type: 'string',
|
|
default: '',
|
|
description: 'Comma-separated UUID list',
|
|
},
|
|
{
|
|
displayName: 'Priority IDs',
|
|
name: 'priority_ids',
|
|
type: 'string',
|
|
default: '',
|
|
description: 'Comma-separated UUID list',
|
|
},
|
|
{
|
|
displayName: 'Client IDs',
|
|
name: 'client_ids',
|
|
type: 'string',
|
|
default: '',
|
|
description: 'Comma-separated UUID list',
|
|
},
|
|
{
|
|
displayName: 'Assigned To IDs',
|
|
name: 'assigned_to_ids',
|
|
type: 'string',
|
|
default: '',
|
|
description: 'Comma-separated UUID list',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Create Additional Fields',
|
|
name: 'createAdditionalFields',
|
|
type: 'collection',
|
|
default: {},
|
|
placeholder: 'Add Field',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['create'],
|
|
},
|
|
},
|
|
options: [
|
|
{ displayName: 'URL', name: 'url', type: 'string', default: '' },
|
|
{
|
|
displayName: 'Location ID',
|
|
name: 'location_id',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Contact Name ID',
|
|
name: 'contact_name_id',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Category ID',
|
|
name: 'category_id',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Subcategory ID',
|
|
name: 'subcategory_id',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Assigned To',
|
|
name: 'assigned_to',
|
|
type: 'string',
|
|
default: '',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
{
|
|
displayName: 'Attributes (JSON)',
|
|
name: 'attributes',
|
|
type: 'json',
|
|
default: '{}',
|
|
},
|
|
{
|
|
displayName: 'Tags',
|
|
name: 'tags',
|
|
type: 'string',
|
|
default: '',
|
|
description: 'Comma-separated tags',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Update Additional Fields',
|
|
name: 'updateAdditionalFields',
|
|
type: 'collection',
|
|
default: {},
|
|
placeholder: 'Add Field',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['ticket'],
|
|
ticketOperation: ['update'],
|
|
},
|
|
},
|
|
options: [
|
|
{ displayName: 'Title', name: 'title', type: 'string', default: '' },
|
|
{ displayName: 'URL', name: 'url', type: 'string', default: '' },
|
|
{
|
|
displayName: 'Client ID',
|
|
name: 'client_id',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchClients',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Board ID',
|
|
name: 'board_id',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchBoards',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Status ID',
|
|
name: 'status_id',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchStatuses',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Priority ID',
|
|
name: 'priority_id',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchPriorities',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Location ID',
|
|
name: 'location_id',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Contact Name ID',
|
|
name: 'contact_name_id',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Category ID',
|
|
name: 'category_id',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Subcategory ID',
|
|
name: 'subcategory_id',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Assigned To',
|
|
name: 'assigned_to',
|
|
type: 'string',
|
|
default: '',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
{
|
|
displayName: 'Attributes (JSON)',
|
|
name: 'attributes',
|
|
type: 'json',
|
|
default: '{}',
|
|
},
|
|
{
|
|
displayName: 'Tags',
|
|
name: 'tags',
|
|
type: 'string',
|
|
default: '',
|
|
description: 'Comma-separated tags',
|
|
},
|
|
],
|
|
},
|
|
|
|
// Contact fields
|
|
{
|
|
displayName: 'Full Name',
|
|
name: 'full_name',
|
|
type: 'string',
|
|
required: true,
|
|
default: '',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['contact'],
|
|
contactOperation: ['create'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Contact ID',
|
|
name: 'contactId',
|
|
type: 'string',
|
|
required: true,
|
|
default: '',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['contact'],
|
|
contactOperation: ['get', 'update', 'delete'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Create Additional Fields',
|
|
name: 'contactCreateAdditionalFields',
|
|
type: 'collection',
|
|
default: {},
|
|
placeholder: 'Add Field',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['contact'],
|
|
contactOperation: ['create'],
|
|
},
|
|
},
|
|
options: [
|
|
{ displayName: 'Email', name: 'email', type: 'string', default: '' },
|
|
{
|
|
displayName: 'Primary Email Canonical Type',
|
|
name: 'primary_email_canonical_type',
|
|
type: 'options',
|
|
default: '',
|
|
options: [
|
|
{ name: 'Work', value: 'work' },
|
|
{ name: 'Personal', value: 'personal' },
|
|
{ name: 'Billing', value: 'billing' },
|
|
{ name: 'Other', value: 'other' },
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Primary Email Custom Type',
|
|
name: 'primary_email_custom_type',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Additional Email Addresses (JSON)',
|
|
name: 'additional_email_addresses',
|
|
type: 'json',
|
|
default: '[]',
|
|
description: 'Array of objects with required email_address and optional label metadata',
|
|
},
|
|
{
|
|
displayName: 'Client ID',
|
|
name: 'client_id',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchClients',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
},
|
|
{ displayName: 'Role', name: 'role', type: 'string', default: '' },
|
|
{
|
|
displayName: 'Notes',
|
|
name: 'notes',
|
|
type: 'string',
|
|
default: '',
|
|
typeOptions: {
|
|
rows: 4,
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Is Inactive',
|
|
name: 'is_inactive',
|
|
type: 'boolean',
|
|
default: false,
|
|
},
|
|
{
|
|
displayName: 'Phone Numbers (JSON)',
|
|
name: 'phone_numbers',
|
|
type: 'json',
|
|
default: '[]',
|
|
description: 'Array of objects with required phone_number and optional metadata',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Update Additional Fields',
|
|
name: 'contactUpdateAdditionalFields',
|
|
type: 'collection',
|
|
default: {},
|
|
placeholder: 'Add Field',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['contact'],
|
|
contactOperation: ['update'],
|
|
},
|
|
},
|
|
options: [
|
|
{ displayName: 'Full Name', name: 'full_name', type: 'string', default: '' },
|
|
{ displayName: 'Email', name: 'email', type: 'string', default: '' },
|
|
{
|
|
displayName: 'Primary Email Canonical Type',
|
|
name: 'primary_email_canonical_type',
|
|
type: 'options',
|
|
default: '',
|
|
options: [
|
|
{ name: 'Work', value: 'work' },
|
|
{ name: 'Personal', value: 'personal' },
|
|
{ name: 'Billing', value: 'billing' },
|
|
{ name: 'Other', value: 'other' },
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Primary Email Custom Type',
|
|
name: 'primary_email_custom_type',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Additional Email Addresses (JSON)',
|
|
name: 'additional_email_addresses',
|
|
type: 'json',
|
|
default: '[]',
|
|
description: 'Array of objects with required email_address and optional label metadata',
|
|
},
|
|
{
|
|
displayName: 'Client ID',
|
|
name: 'client_id',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchClients',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
},
|
|
{ displayName: 'Role', name: 'role', type: 'string', default: '' },
|
|
{
|
|
displayName: 'Notes',
|
|
name: 'notes',
|
|
type: 'string',
|
|
default: '',
|
|
typeOptions: {
|
|
rows: 4,
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Is Inactive',
|
|
name: 'is_inactive',
|
|
type: 'boolean',
|
|
default: false,
|
|
},
|
|
{
|
|
displayName: 'Phone Numbers (JSON)',
|
|
name: 'phone_numbers',
|
|
type: 'json',
|
|
default: '[]',
|
|
description: 'Array of objects with required phone_number and optional metadata',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Contact List Filters',
|
|
name: 'contactListFilters',
|
|
type: 'collection',
|
|
default: {},
|
|
placeholder: 'Add Filter',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['contact'],
|
|
contactOperation: ['list'],
|
|
},
|
|
},
|
|
options: [
|
|
{ displayName: 'Client ID', name: 'client_id', type: 'string', default: '' },
|
|
{ displayName: 'Search Term', name: 'search_term', type: 'string', default: '' },
|
|
{
|
|
displayName: 'Is Inactive',
|
|
name: 'is_inactive',
|
|
type: 'boolean',
|
|
default: false,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Page',
|
|
name: 'contactPage',
|
|
type: 'number',
|
|
default: 1,
|
|
typeOptions: {
|
|
minValue: 1,
|
|
numberPrecision: 0,
|
|
},
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['contact'],
|
|
contactOperation: ['list'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Limit',
|
|
name: 'contactLimit',
|
|
type: 'number',
|
|
default: 25,
|
|
typeOptions: {
|
|
minValue: 1,
|
|
maxValue: 100,
|
|
numberPrecision: 0,
|
|
},
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['contact'],
|
|
contactOperation: ['list'],
|
|
},
|
|
},
|
|
},
|
|
|
|
// Project Task fields
|
|
{
|
|
displayName: 'Task Name',
|
|
name: 'task_name',
|
|
type: 'string',
|
|
required: true,
|
|
default: '',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['projectTask'],
|
|
projectTaskOperation: ['create'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Project ID',
|
|
name: 'projectTaskProjectId',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
required: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['projectTask'],
|
|
projectTaskOperation: ['create', 'list'],
|
|
},
|
|
},
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchProjects',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
description: 'Select a project or enter a project UUID manually',
|
|
},
|
|
{
|
|
displayName: 'Phase ID',
|
|
name: 'projectTaskPhaseId',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
required: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['projectTask'],
|
|
projectTaskOperation: ['create'],
|
|
},
|
|
},
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchProjectPhases',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
description: 'Select a phase within the selected project, or enter a phase UUID manually',
|
|
},
|
|
{
|
|
displayName: 'Status Mapping ID',
|
|
name: 'projectTaskStatusMappingId',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
required: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['projectTask'],
|
|
projectTaskOperation: ['create'],
|
|
},
|
|
},
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchProjectTaskStatusMappings',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
description:
|
|
'Project-specific task status mapping UUID. Use the lookup to see valid options for the selected project.',
|
|
},
|
|
{
|
|
displayName: 'Task ID',
|
|
name: 'projectTaskId',
|
|
type: 'string',
|
|
required: true,
|
|
default: '',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['projectTask'],
|
|
projectTaskOperation: ['get', 'update', 'delete'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Create Additional Fields',
|
|
name: 'projectTaskCreateAdditionalFields',
|
|
type: 'collection',
|
|
default: {},
|
|
placeholder: 'Add Field',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['projectTask'],
|
|
projectTaskOperation: ['create'],
|
|
},
|
|
},
|
|
options: [
|
|
{
|
|
displayName: 'Description',
|
|
name: 'description',
|
|
type: 'string',
|
|
default: '',
|
|
typeOptions: {
|
|
rows: 4,
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Assigned To',
|
|
name: 'assigned_to',
|
|
type: 'string',
|
|
default: '',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
{
|
|
displayName: 'Estimated Hours',
|
|
name: 'estimated_hours',
|
|
type: 'number',
|
|
default: 0,
|
|
typeOptions: {
|
|
minValue: 0,
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Due Date',
|
|
name: 'due_date',
|
|
type: 'string',
|
|
default: '',
|
|
placeholder: 'YYYY-MM-DD or ISO 8601 timestamp',
|
|
},
|
|
{
|
|
displayName: 'Priority ID',
|
|
name: 'priority_id',
|
|
type: 'string',
|
|
default: '',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
{
|
|
displayName: 'Task Type Key',
|
|
name: 'task_type_key',
|
|
type: 'string',
|
|
default: '',
|
|
description: 'Optional task type key; server defaults to "general"',
|
|
},
|
|
{
|
|
displayName: 'WBS Code',
|
|
name: 'wbs_code',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Tags',
|
|
name: 'tags',
|
|
type: 'string',
|
|
default: '',
|
|
description: 'Comma-separated tags',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Update Additional Fields',
|
|
name: 'projectTaskUpdateAdditionalFields',
|
|
type: 'collection',
|
|
default: {},
|
|
placeholder: 'Add Field',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['projectTask'],
|
|
projectTaskOperation: ['update'],
|
|
},
|
|
},
|
|
options: [
|
|
{ displayName: 'Task Name', name: 'task_name', type: 'string', default: '' },
|
|
{
|
|
displayName: 'Description',
|
|
name: 'description',
|
|
type: 'string',
|
|
default: '',
|
|
typeOptions: {
|
|
rows: 4,
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Assigned To',
|
|
name: 'assigned_to',
|
|
type: 'string',
|
|
default: '',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
{
|
|
displayName: 'Estimated Hours',
|
|
name: 'estimated_hours',
|
|
type: 'number',
|
|
default: 0,
|
|
typeOptions: {
|
|
minValue: 0,
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Due Date',
|
|
name: 'due_date',
|
|
type: 'string',
|
|
default: '',
|
|
placeholder: 'YYYY-MM-DD or ISO 8601 timestamp',
|
|
},
|
|
{
|
|
displayName: 'Priority ID',
|
|
name: 'priority_id',
|
|
type: 'string',
|
|
default: '',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
{
|
|
displayName: 'Task Type Key',
|
|
name: 'task_type_key',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Status Mapping ID',
|
|
name: 'project_status_mapping_id',
|
|
type: 'string',
|
|
default: '',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
{
|
|
displayName: 'WBS Code',
|
|
name: 'wbs_code',
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
{
|
|
displayName: 'Tags',
|
|
name: 'tags',
|
|
type: 'string',
|
|
default: '',
|
|
description: 'Comma-separated tags',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
displayName: 'Page',
|
|
name: 'projectTaskPage',
|
|
type: 'number',
|
|
default: 1,
|
|
typeOptions: {
|
|
minValue: 1,
|
|
numberPrecision: 0,
|
|
},
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['projectTask'],
|
|
projectTaskOperation: ['list'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Limit',
|
|
name: 'projectTaskLimit',
|
|
type: 'number',
|
|
default: 25,
|
|
typeOptions: {
|
|
minValue: 1,
|
|
maxValue: 100,
|
|
numberPrecision: 0,
|
|
},
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['projectTask'],
|
|
projectTaskOperation: ['list'],
|
|
},
|
|
},
|
|
},
|
|
|
|
// Helper list parameters
|
|
{
|
|
displayName: 'Page',
|
|
name: 'helperPage',
|
|
type: 'number',
|
|
default: 1,
|
|
typeOptions: {
|
|
minValue: 1,
|
|
numberPrecision: 0,
|
|
},
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['client', 'board', 'status', 'priority'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Limit',
|
|
name: 'helperLimit',
|
|
type: 'number',
|
|
default: 25,
|
|
typeOptions: {
|
|
minValue: 1,
|
|
maxValue: 100,
|
|
numberPrecision: 0,
|
|
},
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['client', 'board', 'status', 'priority'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Search',
|
|
name: 'helperSearch',
|
|
type: 'string',
|
|
default: '',
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['client', 'board', 'status', 'priority'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
displayName: 'Status Type',
|
|
name: 'helperStatusType',
|
|
type: 'options',
|
|
default: 'ticket',
|
|
options: [
|
|
{ name: 'Ticket', value: 'ticket' },
|
|
{ name: 'Project', value: 'project' },
|
|
{ name: 'Project Task', value: 'project_task' },
|
|
{ name: 'Interaction', value: 'interaction' },
|
|
],
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['status'],
|
|
statusOperation: ['list'],
|
|
},
|
|
},
|
|
description: 'Filter statuses by entity type',
|
|
},
|
|
{
|
|
displayName: 'Board ID',
|
|
name: 'helperBoardId',
|
|
type: 'resourceLocator',
|
|
default: { mode: 'list', value: '' },
|
|
required: true,
|
|
displayOptions: {
|
|
show: {
|
|
resource: ['status'],
|
|
statusOperation: ['list'],
|
|
helperStatusType: ['ticket'],
|
|
},
|
|
},
|
|
modes: [
|
|
{
|
|
displayName: 'From List',
|
|
name: 'list',
|
|
type: 'list',
|
|
typeOptions: {
|
|
searchListMethod: 'searchBoards',
|
|
},
|
|
},
|
|
{
|
|
displayName: 'By ID',
|
|
name: 'id',
|
|
type: 'string',
|
|
placeholder: '00000000-0000-0000-0000-000000000000',
|
|
},
|
|
],
|
|
description:
|
|
'Required — ticket statuses are board-owned, so listing them requires a board_id',
|
|
},
|
|
],
|
|
};
|
|
|
|
methods = {
|
|
listSearch: {
|
|
async searchClients(this: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult> {
|
|
return loadLookup(this, '/api/v1/clients', 'client_id', ['client_name', 'name'], filter);
|
|
},
|
|
async searchBoards(this: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult> {
|
|
return loadLookup(this, '/api/v1/boards', 'board_id', ['board_name', 'name'], filter);
|
|
},
|
|
async searchStatuses(this: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult> {
|
|
const type = getCurrentStatusLookupType(this);
|
|
const boardId = getCurrentStatusBoardId(this);
|
|
|
|
// Ticket statuses are board-scoped; without a board_id the server returns 400.
|
|
// Return an empty list so the user can fall back to manual UUID entry.
|
|
if (type === 'ticket' && !boardId) {
|
|
return { results: [] };
|
|
}
|
|
|
|
return loadLookup(
|
|
this,
|
|
'/api/v1/statuses',
|
|
'status_id',
|
|
['name', 'status_name'],
|
|
filter,
|
|
compactObject({ type, board_id: boardId }),
|
|
);
|
|
},
|
|
async searchPriorities(this: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult> {
|
|
return loadLookup(
|
|
this,
|
|
'/api/v1/priorities',
|
|
'priority_id',
|
|
['priority_name', 'name'],
|
|
filter,
|
|
);
|
|
},
|
|
async searchProjects(this: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult> {
|
|
return loadLookup(
|
|
this,
|
|
'/api/v1/projects',
|
|
'project_id',
|
|
['project_name', 'name'],
|
|
filter,
|
|
);
|
|
},
|
|
async searchProjectPhases(this: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult> {
|
|
const projectId = getCurrentProjectLookupId(this);
|
|
if (!projectId) {
|
|
return { results: [] };
|
|
}
|
|
|
|
return loadLookup(
|
|
this,
|
|
`/api/v1/projects/${projectId}/phases`,
|
|
'phase_id',
|
|
['phase_name', 'name'],
|
|
filter,
|
|
);
|
|
},
|
|
async searchProjectTaskStatusMappings(
|
|
this: ILoadOptionsFunctions,
|
|
filter?: string,
|
|
): Promise<INodeListSearchResult> {
|
|
const projectId = getCurrentProjectLookupId(this);
|
|
if (!projectId) {
|
|
return { results: [] };
|
|
}
|
|
|
|
return loadLookup(
|
|
this,
|
|
`/api/v1/projects/${projectId}/task-status-mappings`,
|
|
'project_status_mapping_id',
|
|
['custom_name', 'status_name', 'name'],
|
|
filter,
|
|
);
|
|
},
|
|
},
|
|
};
|
|
|
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
const items = this.getInputData();
|
|
const returnData: INodeExecutionData[] = [];
|
|
|
|
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
try {
|
|
const resource = this.getNodeParameter('resource', itemIndex) as Resource;
|
|
const operationParamName = getOperationParameterName(resource);
|
|
const operation = this.getNodeParameter(operationParamName, itemIndex) as string;
|
|
|
|
const responseJson =
|
|
resource === 'ticket'
|
|
? await executeTicketOperation(this, itemIndex, operation as TicketOperation)
|
|
: resource === 'contact'
|
|
? await executeContactOperation(this, itemIndex, operation as ContactOperation)
|
|
: resource === 'projectTask'
|
|
? await executeProjectTaskOperation(
|
|
this,
|
|
itemIndex,
|
|
operation as ProjectTaskOperation,
|
|
)
|
|
: await executeHelperOperation(
|
|
this,
|
|
resource as HelperResource,
|
|
itemIndex,
|
|
operation,
|
|
);
|
|
|
|
returnData.push({
|
|
json: responseJson,
|
|
pairedItem: { item: itemIndex },
|
|
});
|
|
} catch (error) {
|
|
const mappedError = formatAlgaApiError(error);
|
|
const errorPayload = {
|
|
error: {
|
|
code: mappedError.code,
|
|
message: mappedError.message,
|
|
...(mappedError.details !== undefined ? { details: mappedError.details } : {}),
|
|
...(mappedError.statusCode !== undefined
|
|
? { statusCode: mappedError.statusCode }
|
|
: {}),
|
|
},
|
|
} as IDataObject;
|
|
|
|
if (this.continueOnFail()) {
|
|
returnData.push({
|
|
json: errorPayload,
|
|
pairedItem: { item: itemIndex },
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (error instanceof NodeApiError || error instanceof NodeOperationError) {
|
|
throw error;
|
|
}
|
|
|
|
throw new NodeApiError(this.getNode(), (error ?? {}) as JsonObject, {
|
|
itemIndex,
|
|
message: mappedError.message,
|
|
description: JSON.stringify({
|
|
code: mappedError.code,
|
|
details: mappedError.details,
|
|
}),
|
|
...(mappedError.statusCode ? { httpCode: String(mappedError.statusCode) } : {}),
|
|
});
|
|
}
|
|
}
|
|
|
|
return [returnData];
|
|
}
|
|
}
|
|
|
|
async function executeHelperOperation(
|
|
context: IExecuteFunctions,
|
|
resource: HelperResource,
|
|
itemIndex: number,
|
|
operation: string,
|
|
): Promise<IDataObject> {
|
|
if (operation !== 'list') {
|
|
throw new NodeOperationError(context.getNode(), `Unsupported operation: ${operation}`, {
|
|
itemIndex,
|
|
});
|
|
}
|
|
|
|
const endpoint = getHelperEndpoint(resource);
|
|
const page = context.getNodeParameter('helperPage', itemIndex, 1) as number;
|
|
const limit = context.getNodeParameter('helperLimit', itemIndex, 25) as number;
|
|
const search = context.getNodeParameter('helperSearch', itemIndex, '') as string;
|
|
const statusType =
|
|
resource === 'status'
|
|
? (context.getNodeParameter('helperStatusType', itemIndex, 'ticket') as StatusType)
|
|
: undefined;
|
|
|
|
let statusBoardId: string | undefined;
|
|
if (resource === 'status' && statusType === 'ticket') {
|
|
const rawHelperBoard = context.getNodeParameter('helperBoardId', itemIndex, {}) as unknown;
|
|
statusBoardId = requireUuid(
|
|
context,
|
|
extractResourceLocatorValue(rawHelperBoard),
|
|
'helperBoardId',
|
|
itemIndex,
|
|
);
|
|
}
|
|
|
|
const query = compactObject({
|
|
page,
|
|
limit,
|
|
search,
|
|
type: statusType,
|
|
board_id: statusBoardId,
|
|
});
|
|
|
|
const response = await algaApiRequest(context, 'GET', endpoint, query);
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
async function executeContactOperation(
|
|
context: IExecuteFunctions,
|
|
itemIndex: number,
|
|
operation: ContactOperation,
|
|
): Promise<IDataObject> {
|
|
switch (operation) {
|
|
case 'create': {
|
|
const fullName = requireNonEmpty(
|
|
context,
|
|
context.getNodeParameter('full_name', itemIndex) as string,
|
|
'full_name',
|
|
itemIndex,
|
|
);
|
|
const rawAdditionalFields = context.getNodeParameter(
|
|
'contactCreateAdditionalFields',
|
|
itemIndex,
|
|
{},
|
|
) as IDataObject;
|
|
const additionalFields = {
|
|
...rawAdditionalFields,
|
|
client_id: extractResourceLocatorValue(rawAdditionalFields.client_id),
|
|
} as IDataObject;
|
|
|
|
const payload = buildWithOperationValidation(context, itemIndex, () =>
|
|
buildContactCreatePayload({
|
|
fullName,
|
|
additionalFields,
|
|
}),
|
|
);
|
|
|
|
const response = await algaApiRequest(context, 'POST', '/api/v1/contacts', undefined, payload);
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'get': {
|
|
const contactId = requireUuid(
|
|
context,
|
|
context.getNodeParameter('contactId', itemIndex) as string,
|
|
'contactId',
|
|
itemIndex,
|
|
);
|
|
|
|
const response = await algaApiRequest(context, 'GET', `/api/v1/contacts/${contactId}`);
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'list': {
|
|
const page = context.getNodeParameter('contactPage', itemIndex, 1) as number;
|
|
const limit = context.getNodeParameter('contactLimit', itemIndex, 25) as number;
|
|
const filters = context.getNodeParameter('contactListFilters', itemIndex, {}) as IDataObject;
|
|
|
|
const query = buildWithOperationValidation(context, itemIndex, () =>
|
|
buildContactListQuery({
|
|
page,
|
|
limit,
|
|
filters,
|
|
}),
|
|
);
|
|
|
|
const response = await algaApiRequest(context, 'GET', '/api/v1/contacts', query);
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'update': {
|
|
const contactId = requireUuid(
|
|
context,
|
|
context.getNodeParameter('contactId', itemIndex) as string,
|
|
'contactId',
|
|
itemIndex,
|
|
);
|
|
const rawAdditionalFields = context.getNodeParameter(
|
|
'contactUpdateAdditionalFields',
|
|
itemIndex,
|
|
{},
|
|
) as IDataObject;
|
|
const additionalFields = {
|
|
...rawAdditionalFields,
|
|
client_id: extractResourceLocatorValue(rawAdditionalFields.client_id),
|
|
} as IDataObject;
|
|
|
|
const payload = buildWithOperationValidation(context, itemIndex, () =>
|
|
buildContactUpdatePayload(additionalFields),
|
|
);
|
|
|
|
if (Object.keys(payload).length === 0) {
|
|
throw new NodeOperationError(context.getNode(), 'At least one update field is required', {
|
|
itemIndex,
|
|
});
|
|
}
|
|
|
|
const response = await algaApiRequest(
|
|
context,
|
|
'PUT',
|
|
`/api/v1/contacts/${contactId}`,
|
|
undefined,
|
|
payload,
|
|
);
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'delete': {
|
|
const contactId = requireUuid(
|
|
context,
|
|
context.getNodeParameter('contactId', itemIndex) as string,
|
|
'contactId',
|
|
itemIndex,
|
|
);
|
|
|
|
const response = await algaApiRequest(context, 'DELETE', `/api/v1/contacts/${contactId}`);
|
|
return normalizeDeleteSuccess(contactId, response);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function executeProjectTaskOperation(
|
|
context: IExecuteFunctions,
|
|
itemIndex: number,
|
|
operation: ProjectTaskOperation,
|
|
): Promise<IDataObject> {
|
|
switch (operation) {
|
|
case 'create': {
|
|
const taskName = requireNonEmpty(
|
|
context,
|
|
context.getNodeParameter('task_name', itemIndex) as string,
|
|
'task_name',
|
|
itemIndex,
|
|
);
|
|
const projectId = requireUuid(
|
|
context,
|
|
getResourceLocatorId(context, 'projectTaskProjectId', itemIndex),
|
|
'projectTaskProjectId',
|
|
itemIndex,
|
|
);
|
|
const phaseId = requireUuid(
|
|
context,
|
|
getResourceLocatorId(context, 'projectTaskPhaseId', itemIndex),
|
|
'projectTaskPhaseId',
|
|
itemIndex,
|
|
);
|
|
const statusMappingId = requireUuid(
|
|
context,
|
|
getResourceLocatorId(context, 'projectTaskStatusMappingId', itemIndex),
|
|
'projectTaskStatusMappingId',
|
|
itemIndex,
|
|
);
|
|
|
|
const additionalFields = context.getNodeParameter(
|
|
'projectTaskCreateAdditionalFields',
|
|
itemIndex,
|
|
{},
|
|
) as IDataObject;
|
|
|
|
const payload = buildWithOperationValidation(context, itemIndex, () =>
|
|
buildProjectTaskCreatePayload({
|
|
taskName,
|
|
statusMappingId,
|
|
additionalFields,
|
|
}),
|
|
);
|
|
|
|
const response = await algaApiRequest(
|
|
context,
|
|
'POST',
|
|
`/api/v1/projects/${projectId}/phases/${phaseId}/tasks`,
|
|
undefined,
|
|
payload,
|
|
);
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'get': {
|
|
const taskId = requireUuid(
|
|
context,
|
|
context.getNodeParameter('projectTaskId', itemIndex) as string,
|
|
'projectTaskId',
|
|
itemIndex,
|
|
);
|
|
|
|
const response = await algaApiRequest(
|
|
context,
|
|
'GET',
|
|
`/api/v1/projects/tasks/${taskId}`,
|
|
);
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'list': {
|
|
const projectId = requireUuid(
|
|
context,
|
|
getResourceLocatorId(context, 'projectTaskProjectId', itemIndex),
|
|
'projectTaskProjectId',
|
|
itemIndex,
|
|
);
|
|
const page = context.getNodeParameter('projectTaskPage', itemIndex, 1) as number;
|
|
const limit = context.getNodeParameter('projectTaskLimit', itemIndex, 25) as number;
|
|
|
|
const query = buildProjectTaskListQuery({ page, limit });
|
|
|
|
const response = await algaApiRequest(
|
|
context,
|
|
'GET',
|
|
`/api/v1/projects/${projectId}/tasks`,
|
|
query,
|
|
);
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'update': {
|
|
const taskId = requireUuid(
|
|
context,
|
|
context.getNodeParameter('projectTaskId', itemIndex) as string,
|
|
'projectTaskId',
|
|
itemIndex,
|
|
);
|
|
const additionalFields = context.getNodeParameter(
|
|
'projectTaskUpdateAdditionalFields',
|
|
itemIndex,
|
|
{},
|
|
) as IDataObject;
|
|
|
|
const payload = buildWithOperationValidation(context, itemIndex, () =>
|
|
buildProjectTaskUpdatePayload(additionalFields),
|
|
);
|
|
|
|
if (Object.keys(payload).length === 0) {
|
|
throw new NodeOperationError(context.getNode(), 'At least one update field is required', {
|
|
itemIndex,
|
|
});
|
|
}
|
|
|
|
const response = await algaApiRequest(
|
|
context,
|
|
'PUT',
|
|
`/api/v1/projects/tasks/${taskId}`,
|
|
undefined,
|
|
payload,
|
|
);
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'delete': {
|
|
const taskId = requireUuid(
|
|
context,
|
|
context.getNodeParameter('projectTaskId', itemIndex) as string,
|
|
'projectTaskId',
|
|
itemIndex,
|
|
);
|
|
|
|
const response = await algaApiRequest(
|
|
context,
|
|
'DELETE',
|
|
`/api/v1/projects/tasks/${taskId}`,
|
|
);
|
|
return normalizeDeleteSuccess(taskId, response);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function executeTicketOperation(
|
|
context: IExecuteFunctions,
|
|
itemIndex: number,
|
|
operation: TicketOperation,
|
|
): Promise<IDataObject> {
|
|
switch (operation) {
|
|
case 'create': {
|
|
const title = requireNonEmpty(
|
|
context,
|
|
context.getNodeParameter('title', itemIndex) as string,
|
|
'title',
|
|
itemIndex,
|
|
);
|
|
|
|
const clientId = requireUuid(
|
|
context,
|
|
getResourceLocatorId(context, 'client_id', itemIndex),
|
|
'client_id',
|
|
itemIndex,
|
|
);
|
|
const boardId = requireUuid(
|
|
context,
|
|
getResourceLocatorId(context, 'board_id', itemIndex),
|
|
'board_id',
|
|
itemIndex,
|
|
);
|
|
const statusId = requireUuid(
|
|
context,
|
|
getResourceLocatorId(context, 'status_id', itemIndex),
|
|
'status_id',
|
|
itemIndex,
|
|
);
|
|
const priorityId = requireUuid(
|
|
context,
|
|
getResourceLocatorId(context, 'priority_id', itemIndex),
|
|
'priority_id',
|
|
itemIndex,
|
|
);
|
|
|
|
const additionalFields = context.getNodeParameter(
|
|
'createAdditionalFields',
|
|
itemIndex,
|
|
{},
|
|
) as IDataObject;
|
|
|
|
const payload = buildTicketCreatePayload({
|
|
title,
|
|
clientId,
|
|
boardId,
|
|
statusId,
|
|
priorityId,
|
|
additionalFields,
|
|
});
|
|
|
|
const response = await algaApiRequest(context, 'POST', '/api/v1/tickets', undefined, payload);
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'get': {
|
|
const ticketId = requireUuid(
|
|
context,
|
|
context.getNodeParameter('ticketId', itemIndex) as string,
|
|
'ticketId',
|
|
itemIndex,
|
|
);
|
|
|
|
const response = await algaApiRequest(context, 'GET', `/api/v1/tickets/${ticketId}`);
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'list': {
|
|
const page = context.getNodeParameter('page', itemIndex, 1) as number;
|
|
const limit = context.getNodeParameter('limit', itemIndex, 25) as number;
|
|
const sort = context.getNodeParameter('sort', itemIndex, 'entered_at') as string;
|
|
const order = context.getNodeParameter('order', itemIndex, 'desc') as string;
|
|
const listFilters = context.getNodeParameter('listFilters', itemIndex, {}) as IDataObject;
|
|
|
|
const query = buildTicketListQuery({
|
|
page,
|
|
limit,
|
|
sort,
|
|
order,
|
|
filters: compactObject(listFilters),
|
|
});
|
|
|
|
const response = await algaApiRequest(context, 'GET', '/api/v1/tickets', query);
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'listComments': {
|
|
const ticketId = requireUuid(
|
|
context,
|
|
context.getNodeParameter('ticketId', itemIndex) as string,
|
|
'ticketId',
|
|
itemIndex,
|
|
);
|
|
const commentListOptions = context.getNodeParameter(
|
|
'commentListOptions',
|
|
itemIndex,
|
|
{},
|
|
) as IDataObject;
|
|
|
|
const query = buildTicketCommentListQuery(commentListOptions);
|
|
const response = await algaApiRequest(
|
|
context,
|
|
'GET',
|
|
`/api/v1/tickets/${ticketId}/comments`,
|
|
query,
|
|
);
|
|
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'search': {
|
|
const queryText = requireNonEmpty(
|
|
context,
|
|
context.getNodeParameter('query', itemIndex) as string,
|
|
'query',
|
|
itemIndex,
|
|
);
|
|
|
|
const searchLimit = context.getNodeParameter('searchLimit', itemIndex, 25) as number;
|
|
const searchFields = context.getNodeParameter(
|
|
'searchAdditionalFields',
|
|
itemIndex,
|
|
{},
|
|
) as IDataObject;
|
|
|
|
const query = buildTicketSearchQuery({
|
|
query: queryText,
|
|
limit: searchLimit,
|
|
includeClosed: searchFields.include_closed as boolean | undefined,
|
|
fields: (searchFields.fields as string[] | undefined) ?? [],
|
|
statusIds: parseCsvList(searchFields.status_ids),
|
|
priorityIds: parseCsvList(searchFields.priority_ids),
|
|
clientIds: parseCsvList(searchFields.client_ids),
|
|
assignedToIds: parseCsvList(searchFields.assigned_to_ids),
|
|
});
|
|
|
|
const response = await algaApiRequest(context, 'GET', '/api/v1/tickets/search', query);
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'update': {
|
|
const ticketId = requireUuid(
|
|
context,
|
|
context.getNodeParameter('ticketId', itemIndex) as string,
|
|
'ticketId',
|
|
itemIndex,
|
|
);
|
|
const rawAdditionalFields = context.getNodeParameter(
|
|
'updateAdditionalFields',
|
|
itemIndex,
|
|
{},
|
|
) as IDataObject;
|
|
|
|
const normalizedAdditionalFields = {
|
|
...rawAdditionalFields,
|
|
client_id: extractResourceLocatorValue(rawAdditionalFields.client_id),
|
|
board_id: extractResourceLocatorValue(rawAdditionalFields.board_id),
|
|
status_id: extractResourceLocatorValue(rawAdditionalFields.status_id),
|
|
priority_id: extractResourceLocatorValue(rawAdditionalFields.priority_id),
|
|
} as IDataObject;
|
|
|
|
const payload = buildTicketUpdatePayload(normalizedAdditionalFields);
|
|
|
|
if (Object.keys(payload).length === 0) {
|
|
throw new NodeOperationError(context.getNode(), 'At least one update field is required', {
|
|
itemIndex,
|
|
});
|
|
}
|
|
|
|
const response = await algaApiRequest(
|
|
context,
|
|
'PUT',
|
|
`/api/v1/tickets/${ticketId}`,
|
|
undefined,
|
|
payload,
|
|
);
|
|
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'addComment': {
|
|
const ticketId = requireUuid(
|
|
context,
|
|
context.getNodeParameter('ticketId', itemIndex) as string,
|
|
'ticketId',
|
|
itemIndex,
|
|
);
|
|
const commentText = requireNonEmpty(
|
|
context,
|
|
context.getNodeParameter('commentText', itemIndex) as string,
|
|
'commentText',
|
|
itemIndex,
|
|
);
|
|
const commentAdditionalFields = context.getNodeParameter(
|
|
'commentAdditionalFields',
|
|
itemIndex,
|
|
{},
|
|
) as IDataObject;
|
|
|
|
const payload = buildTicketCommentPayload(commentText, commentAdditionalFields);
|
|
const response = await algaApiRequest(
|
|
context,
|
|
'POST',
|
|
`/api/v1/tickets/${ticketId}/comments`,
|
|
undefined,
|
|
payload,
|
|
);
|
|
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'updateStatus': {
|
|
const ticketId = requireUuid(
|
|
context,
|
|
context.getNodeParameter('ticketId', itemIndex) as string,
|
|
'ticketId',
|
|
itemIndex,
|
|
);
|
|
const statusId = requireUuid(
|
|
context,
|
|
getResourceLocatorId(context, 'status_id', itemIndex),
|
|
'status_id',
|
|
itemIndex,
|
|
);
|
|
|
|
const response = await algaApiRequest(
|
|
context,
|
|
'PUT',
|
|
`/api/v1/tickets/${ticketId}/status`,
|
|
undefined,
|
|
{ status_id: statusId },
|
|
);
|
|
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'updateAssignment': {
|
|
const ticketId = requireUuid(
|
|
context,
|
|
context.getNodeParameter('ticketId', itemIndex) as string,
|
|
'ticketId',
|
|
itemIndex,
|
|
);
|
|
|
|
const assignmentAction = context.getNodeParameter(
|
|
'assignmentAction',
|
|
itemIndex,
|
|
) as 'assign' | 'clear';
|
|
const assignedTo =
|
|
assignmentAction === 'assign'
|
|
? requireUuid(
|
|
context,
|
|
context.getNodeParameter('assigned_to', itemIndex) as string,
|
|
'assigned_to',
|
|
itemIndex,
|
|
)
|
|
: null;
|
|
|
|
const response = await algaApiRequest(
|
|
context,
|
|
'PUT',
|
|
`/api/v1/tickets/${ticketId}/assignment`,
|
|
undefined,
|
|
{ assigned_to: assignedTo },
|
|
);
|
|
|
|
return normalizeSuccessResponse(response);
|
|
}
|
|
|
|
case 'delete': {
|
|
const ticketId = requireUuid(
|
|
context,
|
|
context.getNodeParameter('ticketId', itemIndex) as string,
|
|
'ticketId',
|
|
itemIndex,
|
|
);
|
|
|
|
const response = await algaApiRequest(context, 'DELETE', `/api/v1/tickets/${ticketId}`);
|
|
return normalizeDeleteSuccess(ticketId, response);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getResourceLocatorId(
|
|
context: IExecuteFunctions,
|
|
parameterName: string,
|
|
itemIndex: number,
|
|
): string {
|
|
const value = context.getNodeParameter(parameterName, itemIndex) as INodePropertyOptions | IDataObject;
|
|
return extractResourceLocatorValue(value);
|
|
}
|
|
|
|
function requireNonEmpty(
|
|
context: IExecuteFunctions,
|
|
value: unknown,
|
|
fieldName: string,
|
|
itemIndex: number,
|
|
): string {
|
|
try {
|
|
return ensureNonEmpty(value, fieldName);
|
|
} catch (error) {
|
|
throw new NodeOperationError(context.getNode(), (error as Error).message, {
|
|
itemIndex,
|
|
});
|
|
}
|
|
}
|
|
|
|
function requireUuid(
|
|
context: IExecuteFunctions,
|
|
value: unknown,
|
|
fieldName: string,
|
|
itemIndex: number,
|
|
): string {
|
|
try {
|
|
return ensureUuid(value, fieldName);
|
|
} catch (error) {
|
|
throw new NodeOperationError(context.getNode(), (error as Error).message, {
|
|
itemIndex,
|
|
});
|
|
}
|
|
}
|
|
|
|
function buildWithOperationValidation<T>(
|
|
context: IExecuteFunctions,
|
|
itemIndex: number,
|
|
builder: () => T,
|
|
): T {
|
|
try {
|
|
return builder();
|
|
} catch (error) {
|
|
throw new NodeOperationError(context.getNode(), (error as Error).message, {
|
|
itemIndex,
|
|
});
|
|
}
|
|
}
|