Skip to content

Commit c8fe12f

Browse files
chore: adding milestone models and resources (#29)
* chore: adding milestone models and resources * fix: formatting --------- Co-authored-by: Surya Prashanth <prashantsurya002@gmail.com>
1 parent a6d8ace commit c8fe12f

File tree

6 files changed

+271
-0
lines changed

6 files changed

+271
-0
lines changed

src/api/Milestones.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { BaseResource } from "./BaseResource";
2+
import { Configuration } from "../Configuration";
3+
import { PaginatedResponse } from "../models/common";
4+
5+
import { Milestone, CreateMilestoneRequest, UpdateMilestoneRequest, MilestoneWorkItem } from "../models/Milestone";
6+
7+
/**
8+
* Milestones API resource
9+
* Handles all milestone related operations
10+
*/
11+
export class Milestones extends BaseResource {
12+
constructor(config: Configuration) {
13+
super(config);
14+
}
15+
16+
/**
17+
* Create a new milestone
18+
*/
19+
async create(workspaceSlug: string, projectId: string, createMilestone: CreateMilestoneRequest): Promise<Milestone> {
20+
return this.post<Milestone>(`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/`, createMilestone);
21+
}
22+
23+
/**
24+
* Retrieve a milestone by ID
25+
*/
26+
async retrieve(workspaceSlug: string, projectId: string, milestoneId: string): Promise<Milestone> {
27+
return this.get<Milestone>(`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/`);
28+
}
29+
30+
/**
31+
* Update a milestone
32+
*/
33+
async update(
34+
workspaceSlug: string,
35+
projectId: string,
36+
milestoneId: string,
37+
updateMilestone: UpdateMilestoneRequest
38+
): Promise<Milestone> {
39+
return this.patch<Milestone>(
40+
`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/`,
41+
updateMilestone
42+
);
43+
}
44+
45+
/**
46+
* Delete a milestone
47+
*/
48+
async delete(workspaceSlug: string, projectId: string, milestoneId: string): Promise<void> {
49+
return this.httpDelete(`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/`);
50+
}
51+
52+
/**
53+
* List milestones with optional filtering
54+
*/
55+
async list(workspaceSlug: string, projectId: string, params?: any): Promise<PaginatedResponse<Milestone>> {
56+
return this.get<PaginatedResponse<Milestone>>(
57+
`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/`,
58+
params
59+
);
60+
}
61+
62+
/**
63+
* Add work items to a milestone
64+
*/
65+
async addWorkItems(workspaceSlug: string, projectId: string, milestoneId: string, issueIds: string[]): Promise<void> {
66+
return this.post<void>(`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/work-items/`, {
67+
issues: issueIds,
68+
});
69+
}
70+
71+
/**
72+
* Remove work items from a milestone
73+
*/
74+
async removeWorkItems(
75+
workspaceSlug: string,
76+
projectId: string,
77+
milestoneId: string,
78+
issueIds: string[]
79+
): Promise<void> {
80+
return this.httpDelete(`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/work-items/`, {
81+
issues: issueIds,
82+
});
83+
}
84+
85+
/**
86+
* List work items in a milestone
87+
*/
88+
async listWorkItems(
89+
workspaceSlug: string,
90+
projectId: string,
91+
milestoneId: string,
92+
params?: any
93+
): Promise<PaginatedResponse<MilestoneWorkItem>> {
94+
return this.get<PaginatedResponse<MilestoneWorkItem>>(
95+
`/workspaces/${workspaceSlug}/projects/${projectId}/milestones/${milestoneId}/work-items/`,
96+
params
97+
);
98+
}
99+
}

src/client/plane-client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Intake } from "../api/Intake";
1818
import { Stickies } from "../api/Stickies";
1919
import { Teamspaces } from "../api/Teamspaces";
2020
import { Initiatives } from "../api/Initiatives";
21+
import { Milestones } from "../api/Milestones";
2122
import { AgentRuns } from "../api/AgentRuns";
2223

2324
/**
@@ -44,6 +45,7 @@ export class PlaneClient {
4445
public intake: Intake;
4546
public stickies: Stickies;
4647
public teamspaces: Teamspaces;
48+
public milestones: Milestones;
4749
public initiatives: Initiatives;
4850
public agentRuns: AgentRuns;
4951

@@ -77,6 +79,7 @@ export class PlaneClient {
7779
this.intake = new Intake(this.config);
7880
this.stickies = new Stickies(this.config);
7981
this.teamspaces = new Teamspaces(this.config);
82+
this.milestones = new Milestones(this.config);
8083
this.initiatives = new Initiatives(this.config);
8184
this.agentRuns = new AgentRuns(this.config);
8285
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export { Epics } from "./api/Epics";
2929
export { Intake } from "./api/Intake";
3030
export { Stickies } from "./api/Stickies";
3131
export { Teamspaces } from "./api/Teamspaces";
32+
export { Milestones } from "./api/Milestones";
3233
export { Initiatives } from "./api/Initiatives";
3334
export { AgentRuns } from "./api/AgentRuns";
3435

src/models/Milestone.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { BaseModel } from "./common";
2+
3+
export interface Milestone extends BaseModel {
4+
title: string;
5+
// YYYY-MM-DD format
6+
target_date?: string;
7+
project: string;
8+
workspace: string;
9+
}
10+
11+
export interface MilestoneLite {
12+
id?: string;
13+
title: string;
14+
// YYYY-MM-DD format
15+
target_date?: string;
16+
external_source?: string;
17+
external_id?: string;
18+
created_at?: string;
19+
updated_at?: string;
20+
}
21+
22+
export interface CreateMilestoneRequest {
23+
title: string;
24+
// YYYY-MM-DD format
25+
target_date?: string;
26+
external_source?: string;
27+
external_id?: string;
28+
}
29+
30+
export interface UpdateMilestoneRequest {
31+
title?: string;
32+
// YYYY-MM-DD format
33+
target_date?: string;
34+
external_source?: string;
35+
external_id?: string;
36+
}
37+
38+
export interface MilestoneWorkItem {
39+
id?: string;
40+
issue?: string;
41+
milestone?: string;
42+
}

src/models/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from "./InitiativeLabel";
1010
export * from "./Intake";
1111
export * from "./Label";
1212
export * from "./Link";
13+
export * from "./Milestone";
1314
export * from "./Module";
1415
export * from "./OAuth";
1516
export * from "./Page";

tests/unit/milestone.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { PlaneClient } from "../../src/client/plane-client";
2+
import { Milestone, UpdateMilestoneRequest, MilestoneWorkItem } from "../../src/models";
3+
import { config } from "./constants";
4+
import { createTestClient } from "../helpers/test-utils";
5+
import { describeIf as describe } from "../helpers/conditional-tests";
6+
7+
describe(!!(config.workspaceSlug && config.projectId && config.workItemId), "Milestone API Tests", () => {
8+
let client: PlaneClient;
9+
let workspaceSlug: string;
10+
let projectId: string;
11+
let workItemId: string;
12+
let milestone: Milestone;
13+
14+
beforeAll(async () => {
15+
client = createTestClient();
16+
workspaceSlug = config.workspaceSlug;
17+
projectId = config.projectId;
18+
workItemId = config.workItemId;
19+
});
20+
21+
afterAll(async () => {
22+
// Clean up created resources
23+
if (milestone?.id) {
24+
try {
25+
await client.milestones.delete(workspaceSlug, projectId, milestone.id);
26+
} catch (error) {
27+
console.warn("Failed to delete milestone:", error);
28+
}
29+
}
30+
});
31+
32+
it("should create a milestone", async () => {
33+
milestone = await client.milestones.create(workspaceSlug, projectId, {
34+
title: "Test Milestone",
35+
});
36+
37+
expect(milestone).toBeDefined();
38+
expect(milestone.id).toBeDefined();
39+
expect(milestone.title).toBe("Test Milestone");
40+
});
41+
42+
it("should retrieve a milestone", async () => {
43+
const retrievedMilestone = await client.milestones.retrieve(workspaceSlug, projectId, milestone.id);
44+
45+
expect(retrievedMilestone).toBeDefined();
46+
expect(retrievedMilestone.id).toBe(milestone.id);
47+
expect(retrievedMilestone.title).toBe(milestone.title);
48+
});
49+
50+
it("should update a milestone", async () => {
51+
const updateData: UpdateMilestoneRequest = {
52+
title: "Updated Test Milestone",
53+
target_date: "2026-12-31",
54+
};
55+
56+
const updatedMilestone = await client.milestones.update(
57+
workspaceSlug,
58+
projectId,
59+
milestone.id,
60+
updateData
61+
);
62+
63+
expect(updatedMilestone).toBeDefined();
64+
expect(updatedMilestone.id).toBe(milestone.id);
65+
expect(updatedMilestone.title).toBe("Updated Test Milestone");
66+
expect(updatedMilestone.target_date).toBe("2026-12-31");
67+
});
68+
69+
it("should list milestones", async () => {
70+
const milestones = await client.milestones.list(workspaceSlug, projectId);
71+
72+
expect(milestones).toBeDefined();
73+
expect(Array.isArray(milestones.results)).toBe(true);
74+
expect(milestones.results.length).toBeGreaterThan(0);
75+
76+
const foundMilestone = milestones.results.find((m) => m.id === milestone.id);
77+
expect(foundMilestone).toBeDefined();
78+
expect(foundMilestone?.title).toBe("Updated Test Milestone");
79+
});
80+
81+
it("should add work items to milestone", async () => {
82+
await client.milestones.addWorkItems(workspaceSlug, projectId, milestone.id, [workItemId]);
83+
84+
const workItems = await client.milestones.listWorkItems(workspaceSlug, projectId, milestone.id);
85+
86+
expect(workItems).toBeDefined();
87+
expect(Array.isArray(workItems.results)).toBe(true);
88+
expect(workItems.results.length).toBeGreaterThan(0);
89+
});
90+
91+
it("should list work items in milestone", async () => {
92+
const workItems = await client.milestones.listWorkItems(workspaceSlug, projectId, milestone.id);
93+
94+
expect(workItems).toBeDefined();
95+
expect(Array.isArray(workItems.results)).toBe(true);
96+
expect(workItems.results.length).toBeGreaterThan(0);
97+
});
98+
99+
it("should remove work items from milestone", async () => {
100+
await client.milestones.removeWorkItems(workspaceSlug, projectId, milestone.id, [workItemId]);
101+
102+
const workItems = await client.milestones.listWorkItems(workspaceSlug, projectId, milestone.id);
103+
104+
expect(workItems).toBeDefined();
105+
expect(Array.isArray(workItems.results)).toBe(true);
106+
107+
const foundWorkItem = workItems.results.find((item: MilestoneWorkItem) => item.issue === workItemId);
108+
expect(foundWorkItem).toBeUndefined();
109+
});
110+
111+
it("should delete a milestone", async () => {
112+
await client.milestones.delete(workspaceSlug, projectId, milestone.id);
113+
114+
// Verify it's deleted by trying to retrieve it
115+
try {
116+
await client.milestones.retrieve(workspaceSlug, projectId, milestone.id);
117+
fail("Expected an error when retrieving a deleted milestone");
118+
} catch (error) {
119+
expect(error).toBeDefined();
120+
}
121+
122+
// Prevent afterAll from trying to delete again
123+
milestone = undefined as any;
124+
});
125+
});

0 commit comments

Comments
 (0)