Skip to content

Commit

Permalink
Add "new project" trigger
Browse files Browse the repository at this point in the history
  • Loading branch information
leelasn committed Oct 7, 2024
1 parent 617a42c commit 4a798ca
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 1 deletion.
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.0.1",
"version": "4.0.2",
"description": "Linear's Zapier integration",
"main": "index.js",
"license": "MIT",
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { newProjectUpdateCommentInstant } from "./triggers/commentProjectUpdateV
import { newProjectUpdateInstant, updatedProjectUpdateInstant } from "./triggers/projectUpdateV2";
import { projectWithoutTeam } from "./triggers/projectWithoutTeam";
import { newIssueInstant, updatedIssueInstant } from "./triggers/issueV2";
import { initiative } from "./triggers/initiative";
import { projectStatus } from "./triggers/projectStatus";
import { newProjectInstant } from "./triggers/newProject";

const handleErrors = (response: HttpResponse, z: ZObject) => {
if (response.request.url !== "https://api.linear.app/graphql") {
Expand Down Expand Up @@ -69,6 +72,9 @@ const App = {
[label.key]: label,
[user.key]: user,
[estimate.key]: estimate,
[initiative.key]: initiative,
[projectStatus.key]: projectStatus,
[newProjectInstant.key]: newProjectInstant,
},
authentication,
beforeRequest: [addBearerHeader],
Expand Down
77 changes: 77 additions & 0 deletions src/triggers/initiative.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ZObject, Bundle } from "zapier-platform-core";

interface TeamStatesResponse {
data: {
initiatives: {
nodes: {
id: string;
name: string;
}[];
pageInfo: {
hasNextPage: boolean;
endCursor: string;
};
};
};
}

const getInitiativesList = async (z: ZObject, bundle: Bundle) => {
const cursor = bundle.meta.page ? await z.cursor.get() : undefined;

const response = await z.request({
url: "https://api.linear.app/graphql",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
authorization: bundle.authData.api_key,
},
body: {
query: `
query Initiatives($after: String) {
initiatives(
first: 50
after: $after
) {
nodes {
id
name
}
pageInfo {
endCursor
hasNextPage
}
}
}`,
variables: {
after: cursor,
},
},
method: "POST",
});

const data = (response.json as TeamStatesResponse).data;
const initiatives = data.initiatives.nodes;

// Set cursor for pagination
if (data.initiatives.pageInfo.hasNextPage) {
await z.cursor.set(data.initiatives.pageInfo.endCursor);
}

return initiatives;
};

export const initiative = {
key: "initiative",
noun: "Initiative",
display: {
label: "Get initiative",
hidden: true,
description:
"The only purpose of this trigger is to populate the dropdown list of initiatives in the UI, thus, it's hidden.",
},

operation: {
perform: getInitiativesList,
canPaginate: true,
},
};
219 changes: 219 additions & 0 deletions src/triggers/newProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { omit, omitBy, pick } from "lodash";
import { ZObject, Bundle } from "zapier-platform-core";
import sample from "../samples/issue.json";
import { getWebhookData, unsubscribeHook } from "../handleWebhook";
import { jsonToGraphQLQuery, VariableType } from "json-to-graphql-query";
import { fetchFromLinear } from "../fetchFromLinear";

interface IdAndName {
id: string;
name: string;
}

interface ProjectCommon {
id: string;
url: string;
name: string;
description: string;
priority: number;
createdAt: Date;
startDate?: Date;
targetDate?: Date;
status: {
id: string;
name: string;
type: string;
};
}

interface ProjectApi extends ProjectCommon {
teams: {
nodes: IdAndName[];
};
initiatives: {
nodes: IdAndName[];
};
projectMilestones: {
nodes: IdAndName[];
};
}

interface ProjectsResponse {
data: {
projects: {
nodes: ProjectApi[];
};
};
}

interface ProjectWebhook extends ProjectCommon {
teamIds: string[];
milestones: IdAndName[];
initiatives: IdAndName[];
}

const subscribeHook = async (z: ZObject, bundle: Bundle) => {
const inputData =
bundle.inputData && Object.keys(bundle.inputData).length > 0
? omitBy(
{
...pick(bundle.inputData, ["teamId", "statusId", "leadId", "initiativeId"]),
},
(v) => v === undefined
)
: undefined;

const data = {
url: bundle.targetUrl,
inputData,
};

return z
.request({
url: "https://client-api.linear.app/connect/zapier/subscribe/createProject",
method: "POST",
body: data,
})
.then((response) => response.data);
};

const getProjectList =
() =>
async (z: ZObject, bundle: Bundle): Promise<ProjectWebhook[]> => {
const variables: Record<string, string> = {};
const variableSchema: Record<string, string> = {};
const filters: unknown[] = [];
if (bundle.inputData.statusId) {
variableSchema.statusId = "ID";
variables.statusId = bundle.inputData.statusId;
filters.push({ status: { id: { eq: new VariableType("statusId") } } });
}
if (bundle.inputData.leadId) {
variableSchema.leadId = "ID";
variables.leadId = bundle.inputData.leadId;
filters.push({ lead: { id: { eq: new VariableType("leadId") } } });
}
if (bundle.inputData.teamId) {
variableSchema.teamId = "ID!";
variables.teamId = bundle.inputData.teamId;
filters.push({ accessibleTeams: { and: [{ id: { in: [new VariableType("teamId")] } }] } });
}
if (bundle.inputData.initiativeId) {
variableSchema.initiativeId = "ID!";
variables.initiativeId = bundle.inputData.initiativeId;
filters.push({ initiatives: { and: [{ id: { in: [new VariableType("initiativeId")] } }] } });
}
const filter = { and: filters };

const jsonQuery = {
query: {
__variables: variableSchema,
projects: {
__args: {
first: 25,
filter,
},
nodes: {
id: true,
url: true,
name: true,
description: true,
priority: true,
createdAt: true,
startDate: true,
targetDate: true,
status: {
id: true,
name: true,
type: true,
},
teams: {
nodes: {
id: true,
name: true,
},
},
initiatives: {
nodes: {
id: true,
name: true,
},
},
projectMilestones: {
nodes: {
id: true,
name: true,
},
},
},
},
},
};
const query = jsonToGraphQLQuery(jsonQuery);
const response = await fetchFromLinear(z, bundle, query, variables);
const data = (response.json as ProjectsResponse).data;
const projectsRaw = data.projects.nodes;
// We need to map the API schema to the webhook schema
return projectsRaw.map((projectRaw) =>
omit(
{
...projectRaw,
teamIds: projectRaw.teams.nodes.map((team) => team.id),
milestones: projectRaw.projectMilestones.nodes,
initiatives: projectRaw.initiatives.nodes,
},
["teams", "projectMilestones"]
)
);
};

export const newProjectInstant = {
noun: "Project",
key: "newProjectInstant",
display: {
label: "New Project",
description: "Triggers when a new project is created.",
},
operation: {
inputFields: [
{
required: false,
label: "Team",
key: "teamId",
helpText: "The team associated with the project.",
dynamic: "team.id.name",
altersDynamicFields: true,
},
{
required: false,
label: "Status",
key: "statusId",
helpText: "The project status.",
dynamic: "projectStatus.id.name",
altersDynamicFields: true,
},
{
required: false,
label: "Lead",
key: "leadId",
helpText: "The user who is the lead of the project.",
dynamic: "user.id.name",
altersDynamicFields: true,
},
{
required: false,
label: "Initiative",
key: "initiativeId",
helpText: "The initiative this project belongs to.",
dynamic: "initiative.id.name",
altersDynamicFields: true,
},
],
type: "hook",
perform: getWebhookData,
performUnsubscribe: unsubscribeHook,
performList: getProjectList(),
sample,
performSubscribe: subscribeHook,
},
};
51 changes: 51 additions & 0 deletions src/triggers/projectStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ZObject, Bundle } from "zapier-platform-core";

interface ProjectStatusesResponse {
data: {
organization: {
projectStatuses: {
id: string;
name: string;
}[];
};
};
}

const getStatusList = async (z: ZObject, bundle: Bundle) => {
const response = await z.request({
url: "https://api.linear.app/graphql",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
authorization: bundle.authData.api_key,
},
body: {
query: `
query ProjectStatuses {
organization {
projectStatuses {
id
name
}
}
}`,
},
method: "POST",
});
const data = (response.json as ProjectStatusesResponse).data;
return data.organization.projectStatuses;
};

export const projectStatus = {
key: "projectStatus",
noun: "Project Status",
display: {
label: "Get project status",
hidden: true,
description:
"The only purpose of this trigger is to populate the dropdown list of project statuses in the UI, thus, it's hidden.",
},
operation: {
perform: getStatusList,
},
};

0 comments on commit 4a798ca

Please sign in to comment.