diff --git a/package.json b/package.json index e578003..883e038 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makeplane/plane-node-sdk", - "version": "0.2.9", + "version": "0.2.10", "description": "Node SDK for Plane", "author": "Plane ", "repository": { diff --git a/src/api/Epics.ts b/src/api/Epics.ts index aac39e9..5f20dbb 100644 --- a/src/api/Epics.ts +++ b/src/api/Epics.ts @@ -1,5 +1,5 @@ import { Configuration } from "../Configuration"; -import { Epic } from "../models/Epic"; +import { Epic, CreateEpic, UpdateEpic, AddEpicWorkItems, EpicIssue } from "../models/Epic"; import { PaginatedResponse } from "../models/common"; import { BaseResource } from "./BaseResource"; @@ -12,6 +12,13 @@ export class Epics extends BaseResource { super(config); } + /** + * Create a new epic in the specified project + */ + async create(workspaceSlug: string, projectId: string, data: CreateEpic): Promise { + return this.post(`/workspaces/${workspaceSlug}/projects/${projectId}/epics/`, data); + } + /** * Retrieve an epic by ID */ @@ -19,10 +26,51 @@ export class Epics extends BaseResource { return this.get(`/workspaces/${workspaceSlug}/projects/${projectId}/epics/${epicId}/`); } + /** + * Partially update an existing epic + */ + async update(workspaceSlug: string, projectId: string, epicId: string, data: UpdateEpic): Promise { + return this.patch(`/workspaces/${workspaceSlug}/projects/${projectId}/epics/${epicId}/`, data); + } + + /** + * Delete an epic + */ + async delete(workspaceSlug: string, projectId: string, epicId: string): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/projects/${projectId}/epics/${epicId}/`); + } + /** * List epics with optional filtering */ async list(workspaceSlug: string, projectId: string, params?: any): Promise> { return this.get>(`/workspaces/${workspaceSlug}/projects/${projectId}/epics/`, params); } + + /** + * List work items under an epic + */ + async listIssues( + workspaceSlug: string, + projectId: string, + epicId: string, + params?: any + ): Promise> { + return this.get>( + `/workspaces/${workspaceSlug}/projects/${projectId}/epics/${epicId}/issues/`, + params + ); + } + + /** + * Add work items as sub-issues under an epic + */ + async addIssues( + workspaceSlug: string, + projectId: string, + epicId: string, + data: AddEpicWorkItems + ): Promise { + return this.post(`/workspaces/${workspaceSlug}/projects/${projectId}/epics/${epicId}/issues/`, data); + } } diff --git a/src/models/Epic.ts b/src/models/Epic.ts index 7ea56b6..08bf4f0 100644 --- a/src/models/Epic.ts +++ b/src/models/Epic.ts @@ -32,3 +32,71 @@ export interface Epic { assignees?: string[]; labels?: string[]; } + +export interface CreateEpic { + name: string; + description_html?: string; + state_id?: string; + parent_id?: string; + assignee_ids?: string[]; + label_ids?: string[]; + priority?: PriorityEnum; + start_date?: string; + target_date?: string; + estimate_point?: string; + external_source?: string; + external_id?: string; +} + +export interface UpdateEpic { + name?: string; + description_html?: string; + state_id?: string; + parent_id?: string; + assignee_ids?: string[]; + label_ids?: string[]; + priority?: PriorityEnum; + start_date?: string; + target_date?: string; + estimate_point?: string; + external_source?: string; + external_id?: string; +} + +export interface AddEpicWorkItems { + work_item_ids: string[]; +} + +export interface EpicIssue { + id: string; + type_id?: string | null; + parent?: string | null; + created_at?: string; + updated_at?: string; + deleted_at?: string | null; + point?: number | null; + name: string; + description_html?: string; + description_stripped?: string; + description_binary?: string | null; + priority?: PriorityEnum; + start_date?: string | null; + target_date?: string | null; + sequence_id?: number; + sort_order?: number; + completed_at?: string | null; + archived_at?: string | null; + last_activity_at?: string; + is_draft?: boolean; + external_source?: string | null; + external_id?: string | null; + created_by?: string; + updated_by?: string | null; + project?: string; + workspace?: string; + state?: string; + estimate_point?: string | null; + type?: string | null; + assignees?: string[]; + labels?: string[]; +} diff --git a/tests/unit/epic.test.ts b/tests/unit/epic.test.ts index e3b709e..f3d118a 100644 --- a/tests/unit/epic.test.ts +++ b/tests/unit/epic.test.ts @@ -1,30 +1,101 @@ import { config } from "./constants"; import { PlaneClient } from "../../src/client/plane-client"; -import { createTestClient } from "../helpers/test-utils"; +import { Epic, UpdateEpic } from "../../src/models/Epic"; +import { createTestClient, randomizeName } from "../helpers/test-utils"; import { describeIf as describe } from "../helpers/conditional-tests"; describe(!!(config.workspaceSlug && config.projectId), "Epic API Tests", () => { let client: PlaneClient; let workspaceSlug: string; let projectId: string; + let epic: Epic; beforeAll(async () => { client = createTestClient(); workspaceSlug = config.workspaceSlug; projectId = config.projectId; + // update the project to enable epics + await client.projects.updateFeatures(workspaceSlug, projectId, { + epics: true, + }); + }); + + afterAll(async () => { + if (epic?.id) { + try { + await client.epics.delete(workspaceSlug, projectId, epic.id); + } catch (error) { + console.warn("Failed to delete epic:", error); + } + } + }); + + it("should create an epic", async () => { + const name = randomizeName("epic-"); + epic = await client.epics.create(workspaceSlug, projectId, { + name: name, + priority: "high", + }); + + expect(epic).toBeDefined(); + expect(epic.id).toBeDefined(); + expect(epic.name).toBe(name); + }); + + it("should retrieve an epic", async () => { + const retrieved = await client.epics.retrieve(workspaceSlug, projectId, epic.id!); + + expect(retrieved).toBeDefined(); + expect(retrieved.id).toBe(epic.id); + expect(retrieved.name).toBe(epic.name); + }); + + it("should update an epic", async () => { + const updateData: UpdateEpic = { + name: "Updated Epic Name", + }; + + const updated = await client.epics.update(workspaceSlug, projectId, epic.id!, updateData); + + expect(updated).toBeDefined(); + expect(updated.id).toBe(epic.id); + expect(updated.name).toBe("Updated Epic Name"); + epic = updated; }); it("should list epics", async () => { const epics = await client.epics.list(workspaceSlug, projectId); + expect(epics).toBeDefined(); expect(epics.results.length).toBeGreaterThan(0); + + const found = epics.results.find((e) => e.id === epic.id); + expect(found).toBeDefined(); }); - it("should retrieve an epic", async () => { - const epics = await client.epics.list(workspaceSlug, projectId); - const epic = await client.epics.retrieve(workspaceSlug, projectId, epics.results[0]!.id!); - expect(epic).toBeDefined(); - expect(epic.id).toBe(epics.results[0]!.id); - expect(epic.name).toBe(epics.results[0]!.name); + it("should list epic issues", async () => { + const issues = await client.epics.listIssues(workspaceSlug, projectId, epic.id!); + + expect(issues).toBeDefined(); + expect(Array.isArray(issues.results)).toBe(true); + }); + + it("should add work items to epic", async () => { + const workItem = await client.workItems.create(workspaceSlug, projectId, { + name: randomizeName("work-item-"), + }); + + try { + const addedIssues = await client.epics.addIssues(workspaceSlug, projectId, epic.id!, { + work_item_ids: [workItem.id], + }); + + expect(addedIssues).toBeDefined(); + expect(Array.isArray(addedIssues)).toBe(true); + expect(addedIssues.length).toBe(1); + expect(addedIssues[0]!.parent).toBe(epic.id); + } finally { + await client.workItems.delete(workspaceSlug, projectId, workItem.id); + } }); });