Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add action to update an existing issue #77

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "linear-zapier",
"version": "4.3.1",
"version": "4.4.0",
"description": "Linear's Zapier integration",
"main": "index.js",
"license": "MIT",
Expand Down
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just issueId btw?

Copy link
Contributor Author

@leelasn leelasn Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to differentiate since issueId is already used in another action and I didn't want to create any accidental side effects

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]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man it would be nice if we had something in the API to enable adding/removing labels atomicly without having to fetch the entire set 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

}

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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this sample data more realistic and not local.linear.dev 😉

url: "https://linear.app/linear/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
Loading