first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const commentOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['comment'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add Comment',
|
||||
value: 'addComment',
|
||||
description: 'Add a comment to an issue',
|
||||
action: 'Add a comment to an issue',
|
||||
},
|
||||
],
|
||||
default: 'addComment',
|
||||
},
|
||||
];
|
||||
|
||||
export const commentFields: INodeProperties[] = [
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* comment:addComment */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Issue ID',
|
||||
name: 'issueId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['comment'],
|
||||
operation: ['addComment'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Comment',
|
||||
name: 'comment',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['comment'],
|
||||
operation: ['addComment'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['comment'],
|
||||
operation: ['addComment'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Parent Comment ID',
|
||||
name: 'parentId',
|
||||
type: 'string',
|
||||
description: 'ID of the parent comment if this is a reply',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,134 @@
|
||||
import get from 'lodash/get';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialTestFunctions,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IHookFunctions,
|
||||
IWebhookFunctions,
|
||||
IHttpRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { query } from './Queries';
|
||||
|
||||
export async function linearApiRequest(
|
||||
this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions,
|
||||
|
||||
body: any = {},
|
||||
option: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const endpoint = 'https://api.linear.app/graphql';
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0, 'apiToken') as string;
|
||||
|
||||
let options: IHttpRequestOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body,
|
||||
url: endpoint,
|
||||
json: true,
|
||||
};
|
||||
options = Object.assign({}, options, option);
|
||||
try {
|
||||
const response = await this.helpers.httpRequestWithAuthentication.call(
|
||||
this,
|
||||
authenticationMethod === 'apiToken' ? 'linearApi' : 'linearOAuth2Api',
|
||||
options,
|
||||
);
|
||||
|
||||
if (response?.errors) {
|
||||
throw new NodeApiError(this.getNode(), response.errors, {
|
||||
message: response.errors[0].message ?? 'Unknown API Error',
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new NodeApiError(
|
||||
this.getNode(),
|
||||
{},
|
||||
{
|
||||
message:
|
||||
error.errorResponse?.[0]?.message ||
|
||||
error.context.data.errors[0]?.message ||
|
||||
'Unknown API error',
|
||||
description:
|
||||
error.errorResponse?.[0]?.extensions?.userPresentableMessage ||
|
||||
error.context.data.errors[0]?.extensions?.userPresentableMessage,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function capitalizeFirstLetter(data: string) {
|
||||
return data.charAt(0).toUpperCase() + data.slice(1);
|
||||
}
|
||||
|
||||
export async function linearApiRequestAllItems(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||
propertyName: string,
|
||||
body: any = {},
|
||||
limit?: number,
|
||||
): Promise<any> {
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let responseData;
|
||||
body.variables.first = limit && limit < 50 ? limit : 50;
|
||||
body.variables.after = null;
|
||||
|
||||
const propertyPath = propertyName.split('.');
|
||||
const nodesPath = [...propertyPath, 'nodes'];
|
||||
const endCursorPath = [...propertyPath, 'pageInfo', 'endCursor'];
|
||||
const hasNextPagePath = [...propertyPath, 'pageInfo', 'hasNextPage'];
|
||||
|
||||
do {
|
||||
responseData = await linearApiRequest.call(this, body);
|
||||
const nodes = get(responseData, nodesPath) as IDataObject[];
|
||||
returnData.push(...nodes);
|
||||
body.variables.after = get(responseData, endCursorPath);
|
||||
if (limit && returnData.length >= limit) {
|
||||
return returnData;
|
||||
}
|
||||
} while (get(responseData, hasNextPagePath));
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function validateCredentials(
|
||||
this: ICredentialTestFunctions,
|
||||
decryptedCredentials: ICredentialDataDecryptedObject,
|
||||
): Promise<any> {
|
||||
const credentials = decryptedCredentials;
|
||||
|
||||
const options: IHttpRequestOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: credentials.apiKey,
|
||||
},
|
||||
method: 'POST',
|
||||
body: {
|
||||
query: query.getIssues(),
|
||||
variables: {
|
||||
first: 1,
|
||||
},
|
||||
},
|
||||
url: 'https://api.linear.app/graphql',
|
||||
json: true,
|
||||
};
|
||||
|
||||
return await this.helpers.request(options);
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
export const sort = (a, b) => {
|
||||
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
@@ -0,0 +1,423 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const issueOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['issue'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add Link',
|
||||
value: 'addLink',
|
||||
description: 'Add a link to an issue',
|
||||
action: 'Add a link to an issue',
|
||||
},
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create an issue',
|
||||
action: 'Create an issue',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete an issue',
|
||||
action: 'Delete an issue',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get an issue',
|
||||
action: 'Get an issue',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
description: 'Get many issues',
|
||||
action: 'Get many issues',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update an issue',
|
||||
action: 'Update an issue',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
];
|
||||
|
||||
export const issueFields: INodeProperties[] = [
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* issue:create */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Team Name or ID',
|
||||
name: 'teamId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['issue'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTeams',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['issue'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['issue'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Assignee Name or ID',
|
||||
name: 'assigneeId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Priority',
|
||||
name: 'priorityId',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Urgent',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: 'High',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
name: 'Normal',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: 'Low',
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
name: 'No Priority',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
displayName: 'State Name or ID',
|
||||
name: 'stateId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getStates',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* issue:delete */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Issue ID',
|
||||
name: 'issueId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['issue'],
|
||||
operation: ['addLink', 'get', 'delete'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* issue:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['issue'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
default: 50,
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['issue'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* issue:update */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Issue ID',
|
||||
name: 'issueId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['issue'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['issue'],
|
||||
operation: ['update'],
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Assignee Name or ID',
|
||||
name: 'assigneeId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Priority Name/ID',
|
||||
name: 'priorityId',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Urgent',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: 'High',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
name: 'Medium',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: 'Low',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: 'No Priority',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
displayName: 'State Name or ID',
|
||||
name: 'stateId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getStates',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Team Name or ID',
|
||||
name: 'teamId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTeams',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['issue'],
|
||||
operation: ['update'],
|
||||
},
|
||||
hide: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Assignee Name or ID',
|
||||
name: 'assigneeId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Priority Name/ID',
|
||||
name: 'priorityId',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Urgent',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: 'High',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
name: 'Medium',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: 'Low',
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
name: 'No Priority',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
displayName: 'State Name or ID',
|
||||
name: 'stateId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getStates',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Team Name or ID',
|
||||
name: 'teamId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTeams',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* issue:addLink */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Link',
|
||||
name: 'link',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['issue'],
|
||||
operation: ['addLink'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.linear",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Productivity"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/linear/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.linear/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
import {
|
||||
type IExecuteFunctions,
|
||||
type ICredentialDataDecryptedObject,
|
||||
type ICredentialsDecrypted,
|
||||
type ICredentialTestFunctions,
|
||||
type IDataObject,
|
||||
type ILoadOptionsFunctions,
|
||||
type INodeCredentialTestResult,
|
||||
type INodeExecutionData,
|
||||
type INodePropertyOptions,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type JsonObject,
|
||||
NodeConnectionTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { commentFields, commentOperations } from './CommentDescription';
|
||||
import {
|
||||
linearApiRequest,
|
||||
linearApiRequestAllItems,
|
||||
sort,
|
||||
validateCredentials,
|
||||
} from './GenericFunctions';
|
||||
import { issueFields, issueOperations } from './IssueDescription';
|
||||
import { query } from './Queries';
|
||||
interface IGraphqlBody {
|
||||
query: string;
|
||||
variables: IDataObject;
|
||||
}
|
||||
export class Linear implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Linear',
|
||||
name: 'linear',
|
||||
icon: 'file:linear.svg',
|
||||
group: ['output'],
|
||||
version: [1, 1.1],
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume Linear API',
|
||||
defaults: {
|
||||
name: 'Linear',
|
||||
},
|
||||
usableAsTool: true,
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'linearApi',
|
||||
required: true,
|
||||
testedBy: 'linearApiTest',
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['apiToken'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linearOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['oAuth2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'API Token',
|
||||
value: 'apiToken',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'apiToken',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Comment',
|
||||
value: 'comment',
|
||||
},
|
||||
{
|
||||
name: 'Issue',
|
||||
value: 'issue',
|
||||
},
|
||||
],
|
||||
default: 'issue',
|
||||
},
|
||||
...commentOperations,
|
||||
...commentFields,
|
||||
...issueOperations,
|
||||
...issueFields,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
credentialTest: {
|
||||
async linearApiTest(
|
||||
this: ICredentialTestFunctions,
|
||||
credential: ICredentialsDecrypted,
|
||||
): Promise<INodeCredentialTestResult> {
|
||||
try {
|
||||
await validateCredentials.call(this, credential.data as ICredentialDataDecryptedObject);
|
||||
} catch (error) {
|
||||
const { error: err } = error as JsonObject;
|
||||
const errors = (err as IDataObject).errors as [{ extensions: { code: string } }];
|
||||
const authenticationError = Boolean(
|
||||
errors.filter((e) => e.extensions.code === 'AUTHENTICATION_ERROR').length,
|
||||
);
|
||||
if (authenticationError) {
|
||||
return {
|
||||
status: 'Error',
|
||||
message: 'The security token included in the request is invalid',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'OK',
|
||||
message: 'Connection successful!',
|
||||
};
|
||||
},
|
||||
},
|
||||
loadOptions: {
|
||||
async getTeams(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const body = {
|
||||
query: query.getTeams(),
|
||||
variables: {
|
||||
$first: 10,
|
||||
},
|
||||
};
|
||||
const teams = await linearApiRequestAllItems.call(this, 'data.teams', body);
|
||||
|
||||
for (const team of teams) {
|
||||
returnData.push({
|
||||
name: team.name,
|
||||
value: team.id,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
async getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const body = {
|
||||
query: query.getUsers(),
|
||||
variables: {
|
||||
$first: 10,
|
||||
},
|
||||
};
|
||||
const users = await linearApiRequestAllItems.call(this, 'data.users', body);
|
||||
|
||||
for (const user of users) {
|
||||
returnData.push({
|
||||
name: user.name,
|
||||
value: user.id,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
async getStates(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
let teamId = this.getNodeParameter('teamId', null) as string;
|
||||
// Handle Updates
|
||||
if (!teamId) {
|
||||
const updateFields = this.getNodeParameter('updateFields', null) as IDataObject;
|
||||
// If not updating the team look up the current team
|
||||
if (!updateFields.teamId) {
|
||||
const issueId = this.getNodeParameter('issueId');
|
||||
const body = {
|
||||
query: query.getIssueTeam(),
|
||||
variables: {
|
||||
issueId,
|
||||
},
|
||||
};
|
||||
const responseData = await linearApiRequest.call(this, body);
|
||||
teamId = responseData?.data?.issue?.team?.id;
|
||||
} else {
|
||||
teamId = updateFields.teamId as string;
|
||||
}
|
||||
}
|
||||
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const body = {
|
||||
query: query.getStates(),
|
||||
variables: {
|
||||
$first: 10,
|
||||
filter: {
|
||||
team: {
|
||||
id: {
|
||||
eq: teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const states = await linearApiRequestAllItems.call(this, 'data.workflowStates', body);
|
||||
|
||||
for (const state of states) {
|
||||
returnData.push({
|
||||
name: state.name,
|
||||
value: state.id,
|
||||
});
|
||||
}
|
||||
return returnData.sort(sort);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const length = items.length;
|
||||
let responseData;
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
if (resource === 'issue') {
|
||||
if (operation === 'create') {
|
||||
const teamId = this.getNodeParameter('teamId', i) as string;
|
||||
const title = this.getNodeParameter('title', i) as string;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
const body: IGraphqlBody = {
|
||||
query: query.createIssue(),
|
||||
variables: {
|
||||
teamId,
|
||||
title,
|
||||
...additionalFields,
|
||||
},
|
||||
};
|
||||
|
||||
responseData = await linearApiRequest.call(this, body);
|
||||
responseData = responseData.data.issueCreate?.issue;
|
||||
}
|
||||
if (operation === 'delete') {
|
||||
const issueId = this.getNodeParameter('issueId', i) as string;
|
||||
const body: IGraphqlBody = {
|
||||
query: query.deleteIssue(),
|
||||
variables: {
|
||||
issueId,
|
||||
},
|
||||
};
|
||||
|
||||
responseData = await linearApiRequest.call(this, body);
|
||||
responseData = responseData?.data?.issueDelete;
|
||||
}
|
||||
if (operation === 'get') {
|
||||
const issueId = this.getNodeParameter('issueId', i) as string;
|
||||
const body: IGraphqlBody = {
|
||||
query: query.getIssue(),
|
||||
variables: {
|
||||
issueId,
|
||||
},
|
||||
};
|
||||
|
||||
responseData = await linearApiRequest.call(this, body);
|
||||
responseData = responseData.data.issue;
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
const returnAll = this.getNodeParameter('returnAll', i);
|
||||
const body: IGraphqlBody = {
|
||||
query: query.getIssues(),
|
||||
variables: {
|
||||
first: 50,
|
||||
},
|
||||
};
|
||||
if (returnAll) {
|
||||
responseData = await linearApiRequestAllItems.call(this, 'data.issues', body);
|
||||
} else {
|
||||
const limit = this.getNodeParameter('limit', 0);
|
||||
responseData = await linearApiRequestAllItems.call(this, 'data.issues', body, limit);
|
||||
}
|
||||
}
|
||||
if (operation === 'update') {
|
||||
const issueId = this.getNodeParameter('issueId', i) as string;
|
||||
const updateFields = this.getNodeParameter('updateFields', i);
|
||||
const body: IGraphqlBody = {
|
||||
query: query.updateIssue(),
|
||||
variables: {
|
||||
issueId,
|
||||
...updateFields,
|
||||
},
|
||||
};
|
||||
|
||||
responseData = await linearApiRequest.call(this, body);
|
||||
responseData = responseData?.data?.issueUpdate?.issue;
|
||||
}
|
||||
if (operation === 'addLink') {
|
||||
const issueId = this.getNodeParameter('issueId', i) as string;
|
||||
const body: IGraphqlBody = {
|
||||
query: query.addIssueLink(),
|
||||
variables: {
|
||||
issueId,
|
||||
url: this.getNodeParameter('link', i),
|
||||
},
|
||||
};
|
||||
|
||||
responseData = await linearApiRequest.call(this, body);
|
||||
responseData = responseData?.data?.attachmentLinkURL;
|
||||
}
|
||||
} else if (resource === 'comment') {
|
||||
if (operation === 'addComment') {
|
||||
const issueId = this.getNodeParameter('issueId', i) as string;
|
||||
const body = this.getNodeParameter('comment', i) as string;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
const requestBody: IGraphqlBody = {
|
||||
query: query.addComment(),
|
||||
variables: {
|
||||
issueId,
|
||||
body,
|
||||
},
|
||||
};
|
||||
|
||||
if (additionalFields.parentId && (additionalFields.parentId as string).trim() !== '') {
|
||||
requestBody.variables.parentId = additionalFields.parentId as string;
|
||||
}
|
||||
|
||||
responseData = await linearApiRequest.call(this, requestBody);
|
||||
responseData = responseData?.data?.commentCreate;
|
||||
}
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.linearTrigger",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Productivity"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/linear/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.lineartrigger/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import {
|
||||
type IHookFunctions,
|
||||
type IWebhookFunctions,
|
||||
type ILoadOptionsFunctions,
|
||||
type INodePropertyOptions,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type IWebhookResponseData,
|
||||
NodeConnectionTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { capitalizeFirstLetter, linearApiRequest } from './GenericFunctions';
|
||||
|
||||
export class LinearTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Linear Trigger',
|
||||
name: 'linearTrigger',
|
||||
icon: 'file:linear.svg',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["triggerOn"]}}',
|
||||
description: 'Starts the workflow when Linear events occur',
|
||||
defaults: {
|
||||
name: 'Linear Trigger',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'linearApi',
|
||||
required: true,
|
||||
testedBy: 'linearApiTest',
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['apiToken'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'linearOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['oAuth2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'onReceived',
|
||||
path: 'webhook',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'API Token',
|
||||
value: 'apiToken',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'apiToken',
|
||||
},
|
||||
{
|
||||
displayName: 'Make sure your credential has the "Admin" scope to create webhooks.',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Team Name or ID',
|
||||
name: 'teamId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTeams',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Listen to Resources',
|
||||
name: 'resources',
|
||||
type: 'multiOptions',
|
||||
options: [
|
||||
{
|
||||
name: 'Comment Reaction',
|
||||
value: 'reaction',
|
||||
},
|
||||
{
|
||||
name: 'Cycle',
|
||||
value: 'cycle',
|
||||
},
|
||||
/* It's still on Alpha stage
|
||||
{
|
||||
name: 'Issue Attachment',
|
||||
value: 'attachment',
|
||||
},*/
|
||||
{
|
||||
name: 'Issue',
|
||||
value: 'issue',
|
||||
},
|
||||
{
|
||||
name: 'Issue Comment',
|
||||
value: 'comment',
|
||||
},
|
||||
{
|
||||
name: 'Issue Label',
|
||||
value: 'issueLabel',
|
||||
},
|
||||
{
|
||||
name: 'Project',
|
||||
value: 'project',
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
async getTeams(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const body = {
|
||||
query: `query Teams {
|
||||
teams {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}`,
|
||||
};
|
||||
const {
|
||||
data: {
|
||||
teams: { nodes },
|
||||
},
|
||||
} = await linearApiRequest.call(this, body);
|
||||
|
||||
for (const node of nodes) {
|
||||
returnData.push({
|
||||
name: node.name,
|
||||
value: node.id,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
webhookMethods = {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const teamId = this.getNodeParameter('teamId') as string;
|
||||
const body = {
|
||||
query: `query {
|
||||
webhooks {
|
||||
nodes {
|
||||
id
|
||||
url
|
||||
enabled
|
||||
team {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
};
|
||||
// Check all the webhooks which exist already if it is identical to the
|
||||
// one that is supposed to get created.
|
||||
const {
|
||||
data: {
|
||||
webhooks: { nodes },
|
||||
},
|
||||
} = await linearApiRequest.call(this, body);
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.url === webhookUrl && node.team.id === teamId && node.enabled === true) {
|
||||
webhookData.webhookId = node.id as string;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
const teamId = this.getNodeParameter('teamId') as string;
|
||||
const resources = this.getNodeParameter('resources') as string[];
|
||||
const body = {
|
||||
query: `
|
||||
mutation webhookCreate($url: String!, $teamId: String!, $resources: [String!]!) {
|
||||
webhookCreate(
|
||||
input: {
|
||||
url: $url
|
||||
teamId: $teamId
|
||||
resourceTypes: $resources
|
||||
}
|
||||
) {
|
||||
success
|
||||
webhook {
|
||||
id
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
url: webhookUrl,
|
||||
teamId,
|
||||
resources: resources.map(capitalizeFirstLetter),
|
||||
},
|
||||
};
|
||||
|
||||
const {
|
||||
data: {
|
||||
webhookCreate: {
|
||||
success,
|
||||
webhook: { id },
|
||||
},
|
||||
},
|
||||
} = await linearApiRequest.call(this, body);
|
||||
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
webhookData.webhookId = id as string;
|
||||
|
||||
return true;
|
||||
},
|
||||
async delete(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
if (webhookData.webhookId !== undefined) {
|
||||
const body = {
|
||||
query: `
|
||||
mutation webhookDelete($id: String!){
|
||||
webhookDelete(
|
||||
id: $id
|
||||
) {
|
||||
success
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
id: webhookData.webhookId,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await linearApiRequest.call(this, body);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
// Remove from the static workflow data so that it is clear
|
||||
// that no webhooks are registered anymore
|
||||
delete webhookData.webhookId;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const bodyData = this.getBodyData();
|
||||
return {
|
||||
workflowData: [this.helpers.returnJsonArray(bodyData)],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
export const query = {
|
||||
getUsers() {
|
||||
return `query Users ($first: Int, $after: String){
|
||||
users (first: $first, after: $after){
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
},
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}}`;
|
||||
},
|
||||
getTeams() {
|
||||
return `query Teams ($first: Int, $after: String){
|
||||
teams (first: $first, after: $after){
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}}`;
|
||||
},
|
||||
getStates() {
|
||||
return `query States ($first: Int, $after: String, $filter: WorkflowStateFilter){
|
||||
workflowStates (first: $first, after: $after, filter: $filter){
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
},
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}}`;
|
||||
},
|
||||
createIssue() {
|
||||
return `mutation IssueCreate (
|
||||
$title: String!,
|
||||
$teamId: String!,
|
||||
$description: String,
|
||||
$assigneeId: String,
|
||||
$priorityId: Int,
|
||||
$stateId: String){
|
||||
issueCreate(
|
||||
input: {
|
||||
title: $title
|
||||
description: $description
|
||||
teamId: $teamId
|
||||
assigneeId: $assigneeId
|
||||
priority: $priorityId
|
||||
stateId: $stateId
|
||||
}
|
||||
) {
|
||||
success
|
||||
issue {
|
||||
id,
|
||||
identifier,
|
||||
title,
|
||||
priority
|
||||
archivedAt
|
||||
assignee {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
state {
|
||||
id
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
creator {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
description
|
||||
dueDate
|
||||
cycle {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
},
|
||||
deleteIssue() {
|
||||
return `mutation IssueDelete ($issueId: String!) {
|
||||
issueDelete(id: $issueId) {
|
||||
success
|
||||
}
|
||||
}`;
|
||||
},
|
||||
getIssue() {
|
||||
return `query Issue($issueId: String!) {
|
||||
issue(id: $issueId) {
|
||||
id,
|
||||
identifier,
|
||||
title,
|
||||
priority,
|
||||
archivedAt,
|
||||
assignee {
|
||||
id,
|
||||
displayName
|
||||
}
|
||||
state {
|
||||
id
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
creator {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
description
|
||||
dueDate
|
||||
cycle {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}`;
|
||||
},
|
||||
getIssueTeam() {
|
||||
return `query Issue($issueId: String!) {
|
||||
issue(id: $issueId) {
|
||||
team {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`;
|
||||
},
|
||||
getIssues() {
|
||||
return `query Issue ($first: Int, $after: String){
|
||||
issues (first: $first, after: $after){
|
||||
nodes {
|
||||
id,
|
||||
identifier,
|
||||
title,
|
||||
priority
|
||||
archivedAt
|
||||
assignee {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
state {
|
||||
id
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
creator {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
description
|
||||
dueDate
|
||||
cycle {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}`;
|
||||
},
|
||||
updateIssue() {
|
||||
return `mutation IssueUpdate (
|
||||
$issueId: String!,
|
||||
$title: String,
|
||||
$teamId: String,
|
||||
$description: String,
|
||||
$assigneeId: String,
|
||||
$priorityId: Int,
|
||||
$stateId: String){
|
||||
issueUpdate(
|
||||
id: $issueId,
|
||||
input: {
|
||||
title: $title
|
||||
description: $description
|
||||
teamId: $teamId
|
||||
assigneeId: $assigneeId
|
||||
priority: $priorityId
|
||||
stateId: $stateId
|
||||
}
|
||||
) {
|
||||
success
|
||||
issue {
|
||||
id,
|
||||
identifier,
|
||||
title,
|
||||
priority
|
||||
archivedAt
|
||||
assignee {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
state {
|
||||
id
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
creator {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
description
|
||||
dueDate
|
||||
cycle {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
},
|
||||
addComment() {
|
||||
return `mutation CommentCreate ($issueId: String!, $body: String!, $parentId: String) {
|
||||
commentCreate(input: {issueId: $issueId, body: $body, parentId: $parentId}) {
|
||||
success
|
||||
comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`;
|
||||
},
|
||||
addIssueLink() {
|
||||
return `mutation AttachmentLinkURL($url: String!, $issueId: String!) {
|
||||
attachmentLinkURL(url: $url, issueId: $issueId) {
|
||||
success
|
||||
}
|
||||
}`;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archivedAt": {
|
||||
"type": "null"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"creator": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dueDate": {
|
||||
"type": "null"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"state": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 2
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"creator": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"state": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archivedAt": {
|
||||
"type": "null"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"creator": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"state": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 3
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"creator": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"state": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archivedAt": {
|
||||
"type": "null"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"creator": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dueDate": {
|
||||
"type": "null"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"state": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"creator": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"state": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"archivedAt": {
|
||||
"type": "null"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"creator": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"state": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="none" style="width:24px;height:24px;margin-right:12px"><path fill="#5E6AD2" d="m.403 37.4 26.198 26.197C13.223 61.336 2.664 50.777.403 37.399M0 30.287 33.713 64a32 32 0 0 0 5.852-.858L.858 24.435c-.46 1.89-.75 3.847-.858 5.852M2.536 19.404l42.06 42.06a32 32 0 0 0 4.4-2.31l-44.15-44.15a32 32 0 0 0-2.31 4.4M7.695 11.145C13.568 4.32 22.268 0 31.977 0 49.663 0 64 14.337 64 32.023c0 9.71-4.32 18.41-11.145 24.282z"/></svg>
|
||||
|
After Width: | Height: | Size: 489 B |
@@ -0,0 +1,176 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { capitalizeFirstLetter, linearApiRequest, sort } from '../GenericFunctions';
|
||||
|
||||
describe('Linear -> GenericFunctions', () => {
|
||||
const mockHttpRequestWithAuthentication = jest.fn();
|
||||
|
||||
describe('linearApiRequest', () => {
|
||||
let mockExecuteFunctions:
|
||||
| IExecuteFunctions
|
||||
| IWebhookFunctions
|
||||
| IHookFunctions
|
||||
| ILoadOptionsFunctions;
|
||||
|
||||
const setupMockFunctions = (authentication: string) => {
|
||||
mockExecuteFunctions = {
|
||||
getNodeParameter: jest.fn().mockReturnValue(authentication),
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: mockHttpRequestWithAuthentication,
|
||||
},
|
||||
getNode: jest.fn().mockReturnValue({}),
|
||||
} as unknown as
|
||||
| IExecuteFunctions
|
||||
| IWebhookFunctions
|
||||
| IHookFunctions
|
||||
| ILoadOptionsFunctions;
|
||||
jest.clearAllMocks();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setupMockFunctions('apiToken');
|
||||
});
|
||||
|
||||
it('should make a successful API request', async () => {
|
||||
const response = { data: { success: true } };
|
||||
|
||||
mockHttpRequestWithAuthentication.mockResolvedValue(response);
|
||||
|
||||
const result = await linearApiRequest.call(mockExecuteFunctions, {
|
||||
query: '{ viewer { id } }',
|
||||
});
|
||||
|
||||
expect(result).toEqual(response);
|
||||
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'linearApi',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
url: 'https://api.linear.app/graphql',
|
||||
json: true,
|
||||
body: { query: '{ viewer { id } }' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle API request errors', async () => {
|
||||
const errorResponse = {
|
||||
errors: [
|
||||
{
|
||||
message: 'Access denied',
|
||||
extensions: {
|
||||
userPresentableMessage: 'You need to have the "Admin" scope to create webhooks.',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockHttpRequestWithAuthentication.mockResolvedValue(errorResponse);
|
||||
|
||||
await expect(
|
||||
linearApiRequest.call(mockExecuteFunctions, { query: '{ viewer { id } }' }),
|
||||
).rejects.toThrow(NodeApiError);
|
||||
|
||||
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'linearApi',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
url: 'https://api.linear.app/graphql',
|
||||
json: true,
|
||||
body: { query: '{ viewer { id } }' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('capitalizeFirstLetter', () => {
|
||||
it('should capitalize the first letter of a string', () => {
|
||||
expect(capitalizeFirstLetter('hello')).toBe('Hello');
|
||||
expect(capitalizeFirstLetter('world')).toBe('World');
|
||||
expect(capitalizeFirstLetter('capitalize')).toBe('Capitalize');
|
||||
});
|
||||
|
||||
it('should return an empty string if input is empty', () => {
|
||||
expect(capitalizeFirstLetter('')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle single character strings', () => {
|
||||
expect(capitalizeFirstLetter('a')).toBe('A');
|
||||
expect(capitalizeFirstLetter('b')).toBe('B');
|
||||
});
|
||||
|
||||
it('should not change the case of the rest of the string', () => {
|
||||
expect(capitalizeFirstLetter('hELLO')).toBe('HELLO');
|
||||
expect(capitalizeFirstLetter('wORLD')).toBe('WORLD');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sort', () => {
|
||||
it('should correctly sort objects by name in ascending order', () => {
|
||||
// Test data
|
||||
const items = [
|
||||
{ name: 'Banana', id: 3 },
|
||||
{ name: 'apple', id: 2 },
|
||||
{ name: 'Cherry', id: 1 },
|
||||
{ name: 'date', id: 4 },
|
||||
];
|
||||
|
||||
// Execute sort
|
||||
const sorted = [...items].sort(sort);
|
||||
|
||||
// Verify sorted order (case-insensitive)
|
||||
expect(sorted.map((item) => item.id)).toEqual([2, 3, 1, 4]);
|
||||
expect(sorted.map((item) => item.name)).toEqual(['apple', 'Banana', 'Cherry', 'date']);
|
||||
});
|
||||
|
||||
it('should treat uppercase and lowercase names as equal', () => {
|
||||
const a = { name: 'apple' };
|
||||
const b = { name: 'APPLE' };
|
||||
|
||||
expect(sort(a, b)).toBe(0);
|
||||
expect(sort(b, a)).toBe(0);
|
||||
});
|
||||
|
||||
it('should return -1 when first name comes before second name alphabetically', () => {
|
||||
const a = { name: 'apple' };
|
||||
const b = { name: 'banana' };
|
||||
|
||||
expect(sort(a, b)).toBe(-1);
|
||||
});
|
||||
|
||||
it('should return 1 when first name comes after second name alphabetically', () => {
|
||||
const a = { name: 'cherry' };
|
||||
const b = { name: 'banana' };
|
||||
|
||||
expect(sort(a, b)).toBe(1);
|
||||
});
|
||||
|
||||
it('should return 0 for identical names', () => {
|
||||
const a = { name: 'apple' };
|
||||
const b = { name: 'apple' };
|
||||
|
||||
expect(sort(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle mixed case properly', () => {
|
||||
// Test data
|
||||
const items = [
|
||||
{ name: 'abc', id: 1 },
|
||||
{ name: 'ABC', id: 2 },
|
||||
{ name: 'Abc', id: 3 },
|
||||
{ name: 'aBC', id: 4 },
|
||||
];
|
||||
|
||||
// They should all be considered equal in terms of sorting
|
||||
const sorted = [...items].sort(sort);
|
||||
|
||||
// Original order should be maintained for equal values
|
||||
expect(sorted.map((item) => item.id)).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,525 @@
|
||||
{
|
||||
"name": "Linear Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-80, 260],
|
||||
"id": "4dbe018c-edec-46d3-b18d-f4517435e1f8",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [400, -400],
|
||||
"id": "512f2fec-df9f-46ac-8df3-e84fd352f7ff",
|
||||
"name": "Create Comment - Output"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "comment",
|
||||
"issueId": "test-17",
|
||||
"comment": "test",
|
||||
"additionalFields": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.linear",
|
||||
"typeVersion": 1,
|
||||
"position": [200, -400],
|
||||
"id": "be60984b-28a5-4c5a-8926-87824300e63e",
|
||||
"name": "Add Comment",
|
||||
"credentials": {
|
||||
"linearApi": {
|
||||
"id": "6bJTI5tzXOk9m6cv",
|
||||
"name": "86-88"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "comment",
|
||||
"issueId": "test-17",
|
||||
"comment": "Add to parent",
|
||||
"additionalFields": {
|
||||
"parentId": "ff12069e-fac8-4b18-8455-cc6b29fa1e77"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.linear",
|
||||
"typeVersion": 1,
|
||||
"position": [200, -220],
|
||||
"id": "328939ed-41ac-442e-819c-f470ce0986b4",
|
||||
"name": "Add Comment - with parent",
|
||||
"credentials": {
|
||||
"linearApi": {
|
||||
"id": "6bJTI5tzXOk9m6cv",
|
||||
"name": "86-88"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [400, -220],
|
||||
"id": "d28e9fe2-4a7b-4714-aa15-a6c6732c4e0d",
|
||||
"name": "Create Comment with Parent"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "addLink",
|
||||
"issueId": "test-17",
|
||||
"link": "https://n8n.io"
|
||||
},
|
||||
"type": "n8n-nodes-base.linear",
|
||||
"typeVersion": 1,
|
||||
"position": [200, -20],
|
||||
"id": "6ec9fdbc-eed8-4e9a-850b-973f72f6f3a5",
|
||||
"name": "Add link to issue",
|
||||
"credentials": {
|
||||
"linearApi": {
|
||||
"id": "6bJTI5tzXOk9m6cv",
|
||||
"name": "86-88"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [400, -20],
|
||||
"id": "229e5b09-8867-44f5-a71c-da31d97ab1b5",
|
||||
"name": "Add Link output"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"teamId": "0a2994c1-5d99-48aa-ab22-8b5ba4711ebc",
|
||||
"title": "This is a test issue",
|
||||
"additionalFields": {
|
||||
"assigneeId": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
|
||||
"description": "test description",
|
||||
"priorityId": 3,
|
||||
"stateId": "65a87a3a-5729-4d82-96bf-badccbeb49af"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.linear",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 160],
|
||||
"id": "3262089e-902a-4922-b5e9-1bd2dae4915f",
|
||||
"name": "Create Issue",
|
||||
"credentials": {
|
||||
"linearApi": {
|
||||
"id": "6bJTI5tzXOk9m6cv",
|
||||
"name": "86-88"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [400, 160],
|
||||
"id": "3515db04-1fb9-4c15-904b-1a34c7bd63fb",
|
||||
"name": "Create Issue Response"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "getAll",
|
||||
"limit": 1
|
||||
},
|
||||
"type": "n8n-nodes-base.linear",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 520],
|
||||
"id": "912a5680-2d83-4d24-921b-f622cc8be0be",
|
||||
"name": "Issue Get Many",
|
||||
"credentials": {
|
||||
"linearApi": {
|
||||
"id": "6bJTI5tzXOk9m6cv",
|
||||
"name": "86-88"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "get",
|
||||
"issueId": "test-18"
|
||||
},
|
||||
"type": "n8n-nodes-base.linear",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 340],
|
||||
"id": "8604ce38-89aa-4793-bd2d-13c7d4fce215",
|
||||
"name": "Get Issue",
|
||||
"credentials": {
|
||||
"linearApi": {
|
||||
"id": "6bJTI5tzXOk9m6cv",
|
||||
"name": "86-88"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [400, 520],
|
||||
"id": "d01f72ce-4f5f-4a3f-9805-7975e67d041d",
|
||||
"name": "Get Many Issues"
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [400, 340],
|
||||
"id": "a2ad027f-8507-4488-84f5-f339ad826120",
|
||||
"name": "Get Issue Output"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "update",
|
||||
"issueId": "test-18",
|
||||
"updateFields": {
|
||||
"assigneeId": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
|
||||
"description": "New Description",
|
||||
"priorityId": 3,
|
||||
"stateId": "622493c0-f4ee-456d-af65-49a7611ede7a",
|
||||
"teamId": "0a2994c1-5d99-48aa-ab22-8b5ba4711ebc",
|
||||
"title": "New Title"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.linear",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 700],
|
||||
"id": "141d4f10-6812-40b4-a146-fa9cf929f86a",
|
||||
"name": "Update Issue",
|
||||
"credentials": {
|
||||
"linearApi": {
|
||||
"id": "6bJTI5tzXOk9m6cv",
|
||||
"name": "86-88"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [400, 700],
|
||||
"id": "b08bba99-9d96-43be-8875-c72c7b51f734",
|
||||
"name": "Update Issue Response"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "delete",
|
||||
"issueId": "test-18"
|
||||
},
|
||||
"type": "n8n-nodes-base.linear",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 880],
|
||||
"id": "d30a2ff2-f8cd-4277-b5c1-6f2df7985872",
|
||||
"name": "Delete Issue",
|
||||
"credentials": {
|
||||
"linearApi": {
|
||||
"id": "6bJTI5tzXOk9m6cv",
|
||||
"name": "86-88"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [400, 880],
|
||||
"id": "8769421e-01ea-41ee-9e35-432408879869",
|
||||
"name": "Delete Issue Response"
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Create Comment - Output": [
|
||||
{
|
||||
"json": {
|
||||
"success": true,
|
||||
"comment": {
|
||||
"id": "ff12069e-fac8-4b18-8455-cc6b29fa1e77"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Create Comment with Parent": [
|
||||
{
|
||||
"json": {
|
||||
"success": true,
|
||||
"comment": {
|
||||
"id": "bd0e4d70-7964-4877-aa30-d81534027f44"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Add Link output": [
|
||||
{
|
||||
"json": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"Create Issue Response": [
|
||||
{
|
||||
"json": {
|
||||
"id": "3c7316e3-4224-424d-8cc8-1dd3b96764b8",
|
||||
"identifier": "TEST-18",
|
||||
"title": "This is a test issue",
|
||||
"priority": 3,
|
||||
"archivedAt": null,
|
||||
"assignee": {
|
||||
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
|
||||
"displayName": "nathan"
|
||||
},
|
||||
"state": {
|
||||
"id": "65a87a3a-5729-4d82-96bf-badccbeb49af",
|
||||
"name": "Backlog"
|
||||
},
|
||||
"createdAt": "2025-06-12T10:38:35.296Z",
|
||||
"creator": {
|
||||
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
|
||||
"displayName": "nathan"
|
||||
},
|
||||
"description": "test description",
|
||||
"dueDate": null,
|
||||
"cycle": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"Get Issue Output": [
|
||||
{
|
||||
"json": {
|
||||
"id": "3c7316e3-4224-424d-8cc8-1dd3b96764b8",
|
||||
"identifier": "TEST-18",
|
||||
"title": "This is a test issue",
|
||||
"priority": 3,
|
||||
"archivedAt": null,
|
||||
"assignee": {
|
||||
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
|
||||
"displayName": "nathan"
|
||||
},
|
||||
"state": {
|
||||
"id": "65a87a3a-5729-4d82-96bf-badccbeb49af",
|
||||
"name": "Backlog"
|
||||
},
|
||||
"createdAt": "2025-06-12T10:38:35.296Z",
|
||||
"creator": {
|
||||
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
|
||||
"displayName": "nathan"
|
||||
},
|
||||
"description": "test description",
|
||||
"dueDate": null,
|
||||
"cycle": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"Get Many Issues": [
|
||||
{
|
||||
"json": {
|
||||
"id": "3c7316e3-4224-424d-8cc8-1dd3b96764b8",
|
||||
"identifier": "TEST-18",
|
||||
"title": "This is a test issue",
|
||||
"priority": 3,
|
||||
"archivedAt": null,
|
||||
"assignee": {
|
||||
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
|
||||
"displayName": "nathan"
|
||||
},
|
||||
"state": {
|
||||
"id": "65a87a3a-5729-4d82-96bf-badccbeb49af",
|
||||
"name": "Backlog"
|
||||
},
|
||||
"createdAt": "2025-06-12T10:38:35.296Z",
|
||||
"creator": {
|
||||
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
|
||||
"displayName": "nathan"
|
||||
},
|
||||
"description": "test description",
|
||||
"dueDate": null,
|
||||
"cycle": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"Update Issue Response": [
|
||||
{
|
||||
"json": {
|
||||
"id": "3c7316e3-4224-424d-8cc8-1dd3b96764b8",
|
||||
"identifier": "TEST-18",
|
||||
"title": "New Title",
|
||||
"priority": 3,
|
||||
"archivedAt": null,
|
||||
"assignee": {
|
||||
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
|
||||
"displayName": "nathan"
|
||||
},
|
||||
"state": {
|
||||
"id": "622493c0-f4ee-456d-af65-49a7611ede7a",
|
||||
"name": "Canceled"
|
||||
},
|
||||
"createdAt": "2025-06-12T10:38:35.296Z",
|
||||
"creator": {
|
||||
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
|
||||
"displayName": "nathan"
|
||||
},
|
||||
"description": "New Description",
|
||||
"dueDate": null,
|
||||
"cycle": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"Delete Issue Response": [
|
||||
{
|
||||
"json": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Add Comment",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Add Comment - with parent",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Add link to issue",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Create Issue",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Get Issue",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Issue Get Many",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Update Issue",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Delete Issue",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Add Comment": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create Comment - Output",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Add Comment - with parent": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create Comment with Parent",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Add link to issue": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Add Link output",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Create Issue": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create Issue Response",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get Issue": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get Issue Output",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Issue Get Many": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get Many Issues",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Update Issue": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update Issue Response",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Delete Issue": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete Issue Response",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "2cc012ac-e157-4f3e-97d8-15fb2da556ef",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "0fa937d34dcabeff4bd6480d3b42cc95edf3bc20e6810819086ef1ce2623639d"
|
||||
},
|
||||
"id": "6FHfNEY4tcmoXVeg",
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import {
|
||||
addCommentRequest,
|
||||
addCommentWithParentRequest,
|
||||
addCommentLink,
|
||||
issueCreateRequest,
|
||||
getIssueRequest,
|
||||
getManyIssuesRequest,
|
||||
updateIssueRequest,
|
||||
deleteIssueRequest,
|
||||
} from './apiRequest';
|
||||
import {
|
||||
commentCreateResponse,
|
||||
commentCreateWithParentResponse,
|
||||
attachmentLinkURLResponse,
|
||||
issueCreateResponse,
|
||||
getIssueResponse,
|
||||
getManyIssueResponse,
|
||||
issueUpdateResponse,
|
||||
deleteIssueResponse,
|
||||
} from './apiResponses';
|
||||
|
||||
describe('Linear', () => {
|
||||
describe('Run Test Workflow', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://api.linear.app');
|
||||
mock.post('/graphql', addCommentRequest).reply(200, commentCreateResponse);
|
||||
mock.post('/graphql', addCommentLink).reply(200, attachmentLinkURLResponse);
|
||||
mock
|
||||
.post('/graphql', addCommentWithParentRequest)
|
||||
.reply(200, commentCreateWithParentResponse);
|
||||
mock.post('/graphql', issueCreateRequest).reply(200, issueCreateResponse);
|
||||
mock.post('/graphql', getIssueRequest).reply(200, getIssueResponse);
|
||||
mock.post('/graphql', getManyIssuesRequest).reply(200, getManyIssueResponse);
|
||||
mock.post('/graphql', updateIssueRequest).reply(200, issueUpdateResponse);
|
||||
mock.post('/graphql', deleteIssueRequest).reply(200, deleteIssueResponse);
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
export const addCommentRequest = {
|
||||
query: `mutation CommentCreate ($issueId: String!, $body: String!, $parentId: String) {
|
||||
commentCreate(input: {issueId: $issueId, body: $body, parentId: $parentId}) {
|
||||
success
|
||||
comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
issueId: 'test-17',
|
||||
body: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
export const addCommentWithParentRequest = {
|
||||
query: `mutation CommentCreate ($issueId: String!, $body: String!, $parentId: String) {
|
||||
commentCreate(input: {issueId: $issueId, body: $body, parentId: $parentId}) {
|
||||
success
|
||||
comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
issueId: 'test-17',
|
||||
body: 'Add to parent',
|
||||
parentId: 'ff12069e-fac8-4b18-8455-cc6b29fa1e77',
|
||||
},
|
||||
};
|
||||
|
||||
export const addCommentLink = {
|
||||
query: `mutation AttachmentLinkURL($url: String!, $issueId: String!) {
|
||||
attachmentLinkURL(url: $url, issueId: $issueId) {
|
||||
success
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
issueId: 'test-17',
|
||||
url: 'https://n8n.io',
|
||||
},
|
||||
};
|
||||
|
||||
export const issueCreateRequest = {
|
||||
query: `mutation IssueCreate (
|
||||
$title: String!,
|
||||
$teamId: String!,
|
||||
$description: String,
|
||||
$assigneeId: String,
|
||||
$priorityId: Int,
|
||||
$stateId: String){
|
||||
issueCreate(
|
||||
input: {
|
||||
title: $title
|
||||
description: $description
|
||||
teamId: $teamId
|
||||
assigneeId: $assigneeId
|
||||
priority: $priorityId
|
||||
stateId: $stateId
|
||||
}
|
||||
) {
|
||||
success
|
||||
issue {
|
||||
id,
|
||||
identifier,
|
||||
title,
|
||||
priority
|
||||
archivedAt
|
||||
assignee {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
state {
|
||||
id
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
creator {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
description
|
||||
dueDate
|
||||
cycle {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
teamId: '0a2994c1-5d99-48aa-ab22-8b5ba4711ebc',
|
||||
title: 'This is a test issue',
|
||||
assigneeId: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
|
||||
description: 'test description',
|
||||
priorityId: 3,
|
||||
stateId: '65a87a3a-5729-4d82-96bf-badccbeb49af',
|
||||
},
|
||||
};
|
||||
|
||||
export const getIssueRequest = {
|
||||
query: `query Issue($issueId: String!) {
|
||||
issue(id: $issueId) {
|
||||
id,
|
||||
identifier,
|
||||
title,
|
||||
priority,
|
||||
archivedAt,
|
||||
assignee {
|
||||
id,
|
||||
displayName
|
||||
}
|
||||
state {
|
||||
id
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
creator {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
description
|
||||
dueDate
|
||||
cycle {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
issueId: 'test-18',
|
||||
},
|
||||
};
|
||||
|
||||
export const getManyIssuesRequest = {
|
||||
query: `query Issue ($first: Int, $after: String){
|
||||
issues (first: $first, after: $after){
|
||||
nodes {
|
||||
id,
|
||||
identifier,
|
||||
title,
|
||||
priority
|
||||
archivedAt
|
||||
assignee {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
state {
|
||||
id
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
creator {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
description
|
||||
dueDate
|
||||
cycle {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
first: 1,
|
||||
after: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const updateIssueRequest = {
|
||||
query: `mutation IssueUpdate (
|
||||
$issueId: String!,
|
||||
$title: String,
|
||||
$teamId: String,
|
||||
$description: String,
|
||||
$assigneeId: String,
|
||||
$priorityId: Int,
|
||||
$stateId: String){
|
||||
issueUpdate(
|
||||
id: $issueId,
|
||||
input: {
|
||||
title: $title
|
||||
description: $description
|
||||
teamId: $teamId
|
||||
assigneeId: $assigneeId
|
||||
priority: $priorityId
|
||||
stateId: $stateId
|
||||
}
|
||||
) {
|
||||
success
|
||||
issue {
|
||||
id,
|
||||
identifier,
|
||||
title,
|
||||
priority
|
||||
archivedAt
|
||||
assignee {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
state {
|
||||
id
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
creator {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
description
|
||||
dueDate
|
||||
cycle {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
issueId: 'test-18',
|
||||
assigneeId: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
|
||||
description: 'New Description',
|
||||
priorityId: 3,
|
||||
stateId: '622493c0-f4ee-456d-af65-49a7611ede7a',
|
||||
teamId: '0a2994c1-5d99-48aa-ab22-8b5ba4711ebc',
|
||||
title: 'New Title',
|
||||
},
|
||||
};
|
||||
|
||||
export const deleteIssueRequest = {
|
||||
query: `mutation IssueDelete ($issueId: String!) {
|
||||
issueDelete(id: $issueId) {
|
||||
success
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
issueId: 'test-18',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
export const commentCreateResponse = {
|
||||
data: {
|
||||
commentCreate: {
|
||||
success: true,
|
||||
comment: {
|
||||
id: 'ff12069e-fac8-4b18-8455-cc6b29fa1e77',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const commentCreateWithParentResponse = {
|
||||
data: {
|
||||
commentCreate: {
|
||||
success: true,
|
||||
comment: {
|
||||
id: 'bd0e4d70-7964-4877-aa30-d81534027f44',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const attachmentLinkURLResponse = {
|
||||
data: {
|
||||
attachmentLinkURL: {
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const issueCreateResponse = {
|
||||
data: {
|
||||
issueCreate: {
|
||||
success: true,
|
||||
issue: {
|
||||
id: '3c7316e3-4224-424d-8cc8-1dd3b96764b8',
|
||||
identifier: 'TEST-18',
|
||||
title: 'This is a test issue',
|
||||
priority: 3,
|
||||
archivedAt: null,
|
||||
assignee: {
|
||||
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
|
||||
displayName: 'nathan',
|
||||
},
|
||||
state: {
|
||||
id: '65a87a3a-5729-4d82-96bf-badccbeb49af',
|
||||
name: 'Backlog',
|
||||
},
|
||||
createdAt: '2025-06-12T10:38:35.296Z',
|
||||
creator: {
|
||||
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
|
||||
displayName: 'nathan',
|
||||
},
|
||||
description: 'test description',
|
||||
dueDate: null,
|
||||
cycle: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getIssueResponse = {
|
||||
data: {
|
||||
issue: {
|
||||
id: '3c7316e3-4224-424d-8cc8-1dd3b96764b8',
|
||||
identifier: 'TEST-18',
|
||||
title: 'This is a test issue',
|
||||
priority: 3,
|
||||
archivedAt: null,
|
||||
assignee: {
|
||||
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
|
||||
displayName: 'nathan',
|
||||
},
|
||||
state: {
|
||||
id: '65a87a3a-5729-4d82-96bf-badccbeb49af',
|
||||
name: 'Backlog',
|
||||
},
|
||||
createdAt: '2025-06-12T10:38:35.296Z',
|
||||
creator: {
|
||||
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
|
||||
displayName: 'nathan',
|
||||
},
|
||||
description: 'test description',
|
||||
dueDate: null,
|
||||
cycle: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getManyIssueResponse = {
|
||||
data: {
|
||||
issues: {
|
||||
nodes: [
|
||||
{
|
||||
id: '3c7316e3-4224-424d-8cc8-1dd3b96764b8',
|
||||
identifier: 'TEST-18',
|
||||
title: 'This is a test issue',
|
||||
priority: 3,
|
||||
archivedAt: null,
|
||||
assignee: {
|
||||
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
|
||||
displayName: 'nathan',
|
||||
},
|
||||
state: {
|
||||
id: '65a87a3a-5729-4d82-96bf-badccbeb49af',
|
||||
name: 'Backlog',
|
||||
},
|
||||
createdAt: '2025-06-12T10:38:35.296Z',
|
||||
creator: {
|
||||
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
|
||||
displayName: 'nathan',
|
||||
},
|
||||
description: 'test description',
|
||||
dueDate: null,
|
||||
cycle: null,
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
endCursor: '3c7316e3-4224-424d-8cc8-1dd3b96764b8',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const issueUpdateResponse = {
|
||||
data: {
|
||||
issueUpdate: {
|
||||
success: true,
|
||||
issue: {
|
||||
id: '3c7316e3-4224-424d-8cc8-1dd3b96764b8',
|
||||
identifier: 'TEST-18',
|
||||
title: 'New Title',
|
||||
priority: 3,
|
||||
archivedAt: null,
|
||||
assignee: {
|
||||
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
|
||||
displayName: 'nathan',
|
||||
},
|
||||
state: {
|
||||
id: '622493c0-f4ee-456d-af65-49a7611ede7a',
|
||||
name: 'Canceled',
|
||||
},
|
||||
createdAt: '2025-06-12T10:38:35.296Z',
|
||||
creator: {
|
||||
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
|
||||
displayName: 'nathan',
|
||||
},
|
||||
description: 'New Description',
|
||||
dueDate: null,
|
||||
cycle: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const deleteIssueResponse = {
|
||||
data: {
|
||||
issueDelete: {
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user