Skip to content

Commit

Permalink
Add action to update an existing issue
Browse files Browse the repository at this point in the history
  • Loading branch information
leelasn committed Oct 11, 2024
1 parent d3b272c commit e559bcd
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 6 deletions.
240 changes: 240 additions & 0 deletions src/creates/updateIssue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import { Bundle, ZObject } from "zapier-platform-core";
import { fetchFromLinear } from "../fetchFromLinear";
import { omitBy, uniq } from "lodash";

interface IssueUpdateResponse {
data?: {
issueUpdate: {
issue: {
id: string;
title: string;
url: string;
identifier: string;
};
success: boolean;
};
};
errors?: {
message: string;
extensions?: {
userPresentableMessage?: string;
};
}[];
}

interface IssueResponse {
data: { issue: { labelIds: string[] } };
}

const updateIssueRequest = async (z: ZObject, bundle: Bundle) => {
if (!bundle.inputData.issueIdToUpdate) {
throw new z.errors.HaltedError("You must specify the ID of the issue to update");
}
const priority = bundle.inputData.priority ? parseInt(bundle.inputData.priority) : 0;
const estimate = bundle.inputData.estimate ? parseInt(bundle.inputData.estimate) : undefined;
let labelIds: string[] | undefined = undefined;
if (bundle.inputData.labels && bundle.inputData.labels.length > 0) {
// We need to append new labels to the issue's existing label set
const issueQuery = `
query ZapierIssue($id: String!) {
issue(id: $id) {
id
labelIds
}
}
`;
const response = await fetchFromLinear(z, bundle, issueQuery, { id: bundle.inputData.issueIdToUpdate });
const data = response.json as IssueResponse;
const originalLabelIds = data.data.issue.labelIds;
labelIds = uniq([...originalLabelIds, ...bundle.inputData.labels]);
}

const variables = omitBy(
{
issueIdToUpdate: bundle.inputData.issueIdToUpdate,
teamId: bundle.inputData.teamId,
title: bundle.inputData.title,
description: bundle.inputData.description,
priority: priority,
estimate: estimate,
stateId: bundle.inputData.statusId,
parentId: bundle.inputData.parentId,
assigneeId: bundle.inputData.assigneeId,
projectId: bundle.inputData.projectId,
projectMilestoneId: bundle.inputData.projectMilestoneId,
dueDate: bundle.inputData.dueDate,
labelIds,
},
(v) => v === undefined
);

const query = `
mutation ZapierIssueUpdate(
$issueIdToUpdate: String!,
$teamId: String,
$title: String,
$description: String,
$priority: Int,
$estimate: Int,
$stateId: String,
$parentId: String,
$assigneeId: String,
$projectId: String,
$projectMilestoneId: String,
$dueDate: TimelessDate,
$labelIds: [String!]
) {
issueUpdate(id: $issueIdToUpdate, input: {
teamId: $teamId,
title: $title,
description: $description,
priority: $priority,
estimate: $estimate,
stateId: $stateId,
parentId: $parentId,
assigneeId: $assigneeId,
projectId: $projectId,
projectMilestoneId: $projectMilestoneId,
dueDate: $dueDate,
labelIds: $labelIds
}) {
issue {
id
identifier
title
url
}
success
}
}`;

const response = await fetchFromLinear(z, bundle, query, variables);
const data = response.json as IssueUpdateResponse;
if (data.errors && data.errors.length) {
const error = data.errors[0];
throw new z.errors.Error(
(error.extensions && error.extensions.userPresentableMessage) || error.message,
"invalid_input",
400
);
}

if (data.data && data.data.issueUpdate && data.data.issueUpdate.success) {
return data.data.issueUpdate.issue;
} else {
const error = data.errors ? data.errors[0].message : "Something went wrong";
throw new z.errors.Error("Failed to update the issue", error, 400);
}
};

export const updateIssue = {
key: "updateIssue",
display: {
hidden: false,
description: "Update an existing issue in Linear",
label: "Update Issue",
},
noun: "Issue",
operation: {
perform: updateIssueRequest,
inputFields: [
{
label: "Issue ID",
key: "issueIdToUpdate",
helpText: "The ID of the issue to update",
required: true,
altersDynamicFields: true,
},
{
label: "Team",
key: "teamId",
helpText: "The team to move the issue to. If this is left blank, the issue will stay in its current team.",
dynamic: "team.id.name",
altersDynamicFields: true,
},
{
label: "Title",
helpText: "The new title of the issue",
key: "title",
},
{
label: "Description",
helpText: "The new description of the issue in markdown format",
key: "description",
type: "text",
},
{
label: "Parent Issue",
helpText: "The ID of the parent issue to set",
type: "string",
key: "parentId",
},
{
label: "Status",
helpText:
"The new status of the issue. If you're moving the issue to a new team, this list will be populated with the statuses of the new team.",
key: "statusId",
dynamic: "status.id.name",
},
{
label: "Assignee",
helpText: "The new assignee of the issue",
key: "assigneeId",
dynamic: "user.id.name",
},
{
label: "Priority",
helpText: "The new priority of the issue",
key: "priority",
choices: [
{ value: "0", sample: "0", label: "No priority" },
{ value: "1", sample: "1", label: "Urgent" },
{ value: "2", sample: "2", label: "High" },
{ value: "3", sample: "3", label: "Medium" },
{ value: "4", sample: "4", label: "Low" },
],
},
{
label: "Estimate",
helpText: "The new estimate of the issue",
key: "estimate",
dynamic: "estimate.id.label",
},
{
label: "Labels",
helpText:
"Labels to add to the issue. If you're moving the issue to a new team, this list will be populated with workspace labels and labels of the new team.",
key: "labels",
dynamic: "label.id.name",
list: true,
},
{
label: "Project",
helpText:
"The project to move the issue to. If you're moving the issue to a new team, this list will be populated with the projects of the new team.",
key: "projectId",
dynamic: "project.id.name",
},
{
label: "Project Milestone",
helpText: "The project milestone to move the issue to",
key: "projectMilestoneId",
dynamic: "project_milestone.id.name",
},
{
label: "Due Date",
helpText: "The issue due date in `yyyy-MM-dd` format",
key: "dueDate",
type: "string",
},
],
sample: {
data: {
id: "7b647c45-c528-464d-8634-eecea0f73033",
title: "Do the roar",
url: "https://local.linear.dev/team-best-team/issue/ENG-118/do-the-roar",
identifier: "ENG-118",
},
},
},
};
27 changes: 27 additions & 0 deletions src/fetchFromLinear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,30 @@ export const fetchFromLinear = async (
method: "POST",
});
};

/**
* Retrieves the team ID for an issue from Linear.
*
* @param z Zapier object
* @param bundle Zapier bundle
* @param issueId Linear issue ID
* @returns The Linear team ID for the issue
*/
export const getIssueTeamId = async (z: ZObject, bundle: Bundle, issueId: string) => {
const issueQuery = `
query ZapierIssue($id: String!) {
issue(id: $id) {
team {
id
}
}
}
`;
const response = await fetchFromLinear(z, bundle, issueQuery, { id: issueId });
const data = response.json as IssueResponse;
return data.data.issue.team.id;
};

interface IssueResponse {
data: { issue: { team: { id: string } } };
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { projectStatus } from "./triggers/projectStatus";
import { newProjectInstant } from "./triggers/newProject";
import { createIssueAttachment } from "./creates/createIssueAttachment";
import { createProject } from "./creates/createProject";
import { updateIssue } from "./creates/updateIssue";

const handleErrors = (response: HttpResponse, z: ZObject) => {
if (response.request.url !== "https://api.linear.app/graphql") {
Expand Down Expand Up @@ -52,6 +53,7 @@ const App = {
[createComment.key]: createComment,
[createIssueAttachment.key]: createIssueAttachment,
[createProject.key]: createProject,
[updateIssue.key]: updateIssue,
},
triggers: {
[newIssue.key]: newIssue,
Expand Down
9 changes: 7 additions & 2 deletions src/triggers/label.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ZObject, Bundle } from "zapier-platform-core";
import { getIssueTeamId } from "../fetchFromLinear";

type LabelResponse = {
id: string;
Expand All @@ -17,9 +18,13 @@ type LabelsResponse = {
};

const getLabelList = async (z: ZObject, bundle: Bundle) => {
const teamId = bundle.inputData.teamId || bundle.inputData.team_id;
let teamId = bundle.inputData.teamId || bundle.inputData.team_id;
if (!teamId && bundle.inputData.issueIdToUpdate) {
// For the `updateIssue` action, we populate the labels dropdown using the issue's current team if the zap isn't updating the issue's team
teamId = await getIssueTeamId(z, bundle, bundle.inputData.issueIdToUpdate);
}
if (!teamId) {
throw new z.errors.HaltedError(`Please select the team first`);
throw new z.errors.HaltedError("Please select the team first before selecting the labels");
}
const cursor = bundle.meta.page ? await z.cursor.get() : undefined;

Expand Down
9 changes: 7 additions & 2 deletions src/triggers/project.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ZObject, Bundle } from "zapier-platform-core";
import { getIssueTeamId } from "../fetchFromLinear";

interface TeamProjectsResponse {
data: {
Expand All @@ -23,9 +24,13 @@ interface TeamProjectsResponse {
}

const getProjectList = async (z: ZObject, bundle: Bundle) => {
const teamId = bundle.inputData.teamId || bundle.inputData.team_id;
let teamId = bundle.inputData.teamId || bundle.inputData.team_id;
if (!teamId && bundle.inputData.issueIdToUpdate) {
// For the `updateIssue` action, we populate the project dropdown using the issue's current team if the zap isn't updating the issue's team
teamId = await getIssueTeamId(z, bundle, bundle.inputData.issueIdToUpdate);
}
if (!teamId) {
throw new z.errors.HaltedError(`Please select the team first`);
throw new z.errors.HaltedError("Please select the team first before selecting the project");
}
const cursor = bundle.meta.page ? await z.cursor.get() : undefined;

Expand Down
9 changes: 7 additions & 2 deletions src/triggers/status.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ZObject, Bundle } from "zapier-platform-core";
import { getIssueTeamId } from "../fetchFromLinear";

interface TeamStatesResponse {
data: {
Expand All @@ -19,9 +20,13 @@ interface TeamStatesResponse {
}

const getStatusList = async (z: ZObject, bundle: Bundle) => {
const teamId = bundle.inputData.teamId || bundle.inputData.team_id;
let teamId = bundle.inputData.teamId || bundle.inputData.team_id;
if (!teamId && bundle.inputData.issueIdToUpdate) {
// For the `updateIssue` action, we populate the status dropdown using the issue's current team if the zap isn't updating the issue's team
teamId = await getIssueTeamId(z, bundle, bundle.inputData.issueIdToUpdate);
}
if (!teamId) {
throw new z.errors.HaltedError(`Please select the team first`);
throw new z.errors.HaltedError("Please select the team first before selecting the status");
}
const cursor = bundle.meta.page ? await z.cursor.get() : undefined;

Expand Down

0 comments on commit e559bcd

Please sign in to comment.