Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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": "@makeplane/plane-node-sdk",
"version": "0.2.9",
"version": "0.2.10",
"description": "Node SDK for Plane",
"author": "Plane <engineering@plane.so>",
"repository": {
Expand Down
53 changes: 52 additions & 1 deletion src/api/Epics.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -12,17 +12,68 @@ export class Epics extends BaseResource {
super(config);
}

/**
* Create a new epic in the specified project
*/
async create(workspaceSlug: string, projectId: string, data: CreateEpic): Promise<Epic> {
return this.post<Epic>(`/workspaces/${workspaceSlug}/projects/${projectId}/epics/`, data);
}

/**
* Retrieve an epic by ID
*/
async retrieve(workspaceSlug: string, projectId: string, epicId: string): Promise<Epic> {
return this.get<Epic>(`/workspaces/${workspaceSlug}/projects/${projectId}/epics/${epicId}/`);
}

/**
* Partially update an existing epic
*/
async update(workspaceSlug: string, projectId: string, epicId: string, data: UpdateEpic): Promise<Epic> {
return this.patch<Epic>(`/workspaces/${workspaceSlug}/projects/${projectId}/epics/${epicId}/`, data);
}

/**
* Delete an epic
*/
async delete(workspaceSlug: string, projectId: string, epicId: string): Promise<void> {
return this.httpDelete(`/workspaces/${workspaceSlug}/projects/${projectId}/epics/${epicId}/`);
}

/**
* List epics with optional filtering
*/
async list(workspaceSlug: string, projectId: string, params?: any): Promise<PaginatedResponse<Epic>> {
return this.get<PaginatedResponse<Epic>>(`/workspaces/${workspaceSlug}/projects/${projectId}/epics/`, params);
}

/**
* List work items under an epic
*/
async listIssues(
workspaceSlug: string,
projectId: string,
epicId: string,
params?: any
): Promise<PaginatedResponse<EpicIssue>> {
return this.get<PaginatedResponse<EpicIssue>>(
`/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<EpicIssue[]> {
return this.post<EpicIssue[]>(
`/workspaces/${workspaceSlug}/projects/${projectId}/epics/${epicId}/issues/`,
data
);
}
}
68 changes: 68 additions & 0 deletions src/models/Epic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
85 changes: 78 additions & 7 deletions tests/unit/epic.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
Loading