diff --git a/README.md b/README.md index 9e0484d..3fe460d 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,10 @@ const project = await client.projects.create("workspace-slug", { - **Workspace**: Workspace-level operations - **Epics**: Epic management and organization - **Intake**: Intake form and request management +- **Stickies**: Stickies management +- **Teamspaces**: Teamspace management +- **Initiatives**: Initiative management +- **Features**: Workspace and project features management ## Development @@ -129,6 +133,8 @@ pnpm test # Run specific test files pnpx ts-node tests/page.test.ts +# or +pnpm test page.test.ts ``` ## License diff --git a/env.example b/env.example index 088046c..3628d12 100644 --- a/env.example +++ b/env.example @@ -1,6 +1,10 @@ # Plane Node SDK Test Configuration # Copy this file to .env.test and update with your test environment values +# API configuration +PLANE_API_KEY=your-plane-api-key +PLANE_BASE_URL=your-plane-base-url + # Workspace configuration TEST_WORKSPACE_SLUG=your-workspace-slug diff --git a/package.json b/package.json index 4713d32..f90517d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makeplane/plane-node-sdk", - "version": "0.2.0", + "version": "0.2.1", "description": "Node SDK for Plane", "author": "Plane ", "repository": { diff --git a/src/api/BaseResource.ts b/src/api/BaseResource.ts index e7fb298..086f23b 100644 --- a/src/api/BaseResource.ts +++ b/src/api/BaseResource.ts @@ -111,10 +111,11 @@ export abstract class BaseResource { /** * DELETE request */ - protected async httpDelete(endpoint: string): Promise { + protected async httpDelete(endpoint: string, data?: any): Promise { try { await axios.delete(this.buildUrl(endpoint), { headers: this.getHeaders(), + data, }); } catch (error) { throw this.handleError(error); diff --git a/src/api/Initiatives/Epics.ts b/src/api/Initiatives/Epics.ts new file mode 100644 index 0000000..062463e --- /dev/null +++ b/src/api/Initiatives/Epics.ts @@ -0,0 +1,37 @@ +import { BaseResource } from "../BaseResource"; +import { Configuration } from "../../Configuration"; +import { Epic } from "../../models/Epic"; +import { PaginatedResponse } from "../../models/common"; +import { AddInitiativeEpicsRequest, RemoveInitiativeEpicsRequest } from "../../models/Initiative"; + +/** + * Initiative Epics API resource + * Handles initiative epic relationships + */ +export class Epics extends BaseResource { + constructor(config: Configuration) { + super(config); + } + + /** + * Get epics associated with an initiative + */ + async list(workspaceSlug: string, initiativeId: string, params?: { limit?: number; offset?: number }): Promise> { + return this.get>(`/workspaces/${workspaceSlug}/initiatives/${initiativeId}/epics/`, params); + } + + /** + * Add epics to an initiative + */ + async add(workspaceSlug: string, initiativeId: string, addEpics: AddInitiativeEpicsRequest): Promise { + return this.post(`/workspaces/${workspaceSlug}/initiatives/${initiativeId}/epics/`, addEpics); + } + + /** + * Remove epics from an initiative + */ + async remove(workspaceSlug: string, initiativeId: string, removeEpics: RemoveInitiativeEpicsRequest): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/initiatives/${initiativeId}/epics/`, removeEpics); + } +} + diff --git a/src/api/Initiatives/Labels.ts b/src/api/Initiatives/Labels.ts new file mode 100644 index 0000000..d306785 --- /dev/null +++ b/src/api/Initiatives/Labels.ts @@ -0,0 +1,73 @@ +import { BaseResource } from "../BaseResource"; +import { Configuration } from "../../Configuration"; +import { InitiativeLabel, CreateInitiativeLabel, UpdateInitiativeLabel, ListInitiativeLabelsParams } from "../../models/InitiativeLabel"; +import { PaginatedResponse } from "../../models/common"; +import { AddInitiativeLabelsRequest, RemoveInitiativeLabelsRequest } from "../../models/Initiative"; + +/** + * Initiative Labels API resource + * Handles initiative label relationships + */ +export class Labels extends BaseResource { + constructor(config: Configuration) { + super(config); + } + + /** + * Create a new initiative label + */ + async create(workspaceSlug: string, createInitiativeLabel: CreateInitiativeLabel): Promise { + return this.post(`/workspaces/${workspaceSlug}/initiatives/labels/`, createInitiativeLabel); + } + + /** + * Retrieve an initiative label by ID + */ + async retrieve(workspaceSlug: string, initiativeLabelId: string): Promise { + return this.get(`/workspaces/${workspaceSlug}/initiatives/labels/${initiativeLabelId}/`); + } + + /** + * Update an initiative label + */ + async update(workspaceSlug: string, initiativeLabelId: string, updateInitiativeLabel: UpdateInitiativeLabel): Promise { + return this.patch(`/workspaces/${workspaceSlug}/initiatives/labels/${initiativeLabelId}/`, updateInitiativeLabel); + } + + /** + * Delete an initiative label + */ + async delete(workspaceSlug: string, initiativeLabelId: string): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/initiatives/labels/${initiativeLabelId}/`); + } + + /** + * List initiative labels with optional filtering + */ + async list(workspaceSlug: string, params?: ListInitiativeLabelsParams): Promise> { + return this.get>(`/workspaces/${workspaceSlug}/initiatives/labels/`, params); + } + + /** + * Add labels to an initiative + */ + async addLabels(workspaceSlug: string, initiativeId: string, addLabels: AddInitiativeLabelsRequest): Promise { + return this.post(`/workspaces/${workspaceSlug}/initiatives/${initiativeId}/labels/`, addLabels); + } + + /** + * Remove labels from an initiative + */ + async removeLabels(workspaceSlug: string, initiativeId: string, removeLabels: RemoveInitiativeLabelsRequest): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/initiatives/${initiativeId}/labels/`, removeLabels); + } + + /** + * Get labels associated with an initiative + */ + async listLabels(workspaceSlug: string, initiativeId: string, params?: { limit?: number; offset?: number }): Promise> { + return this.get>(`/workspaces/${workspaceSlug}/initiatives/${initiativeId}/labels/`, params); + } + +} + diff --git a/src/api/Initiatives/Projects.ts b/src/api/Initiatives/Projects.ts new file mode 100644 index 0000000..416f7fd --- /dev/null +++ b/src/api/Initiatives/Projects.ts @@ -0,0 +1,37 @@ +import { BaseResource } from "../BaseResource"; +import { Configuration } from "../../Configuration"; +import { Project } from "../../models/Project"; +import { PaginatedResponse } from "../../models/common"; +import { AddInitiativeProjectsRequest, RemoveInitiativeProjectsRequest } from "../../models/Initiative"; + +/** + * Initiative Projects API resource + * Handles initiative project relationships + */ +export class Projects extends BaseResource { + constructor(config: Configuration) { + super(config); + } + + /** + * Get projects associated with an initiative + */ + async list(workspaceSlug: string, initiativeId: string, params?: { limit?: number; offset?: number }): Promise> { + return this.get>(`/workspaces/${workspaceSlug}/initiatives/${initiativeId}/projects/`, params); + } + + /** + * Add projects to an initiative + */ + async add(workspaceSlug: string, initiativeId: string, addProjects: AddInitiativeProjectsRequest): Promise { + return this.post(`/workspaces/${workspaceSlug}/initiatives/${initiativeId}/projects/`, addProjects); + } + + /** + * Remove projects from an initiative + */ + async remove(workspaceSlug: string, initiativeId: string, removeProjects: RemoveInitiativeProjectsRequest): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/initiatives/${initiativeId}/projects/`, removeProjects); + } +} + diff --git a/src/api/Initiatives/index.ts b/src/api/Initiatives/index.ts new file mode 100644 index 0000000..8563096 --- /dev/null +++ b/src/api/Initiatives/index.ts @@ -0,0 +1,60 @@ +import { BaseResource } from "../BaseResource"; +import { Configuration } from "../../Configuration"; +import { Initiative, CreateInitiative, UpdateInitiative, ListInitiativesParams } from "../../models/Initiative"; +import { PaginatedResponse } from "../../models/common"; +import { Labels } from "./Labels"; +import { Projects } from "./Projects"; +import { Epics } from "./Epics"; + +/** + * Initiatives API resource + * Handles all initiative-related operations + */ +export class Initiatives extends BaseResource { + public labels: Labels; + public projects: Projects; + public epics: Epics; + + constructor(config: Configuration) { + super(config); + this.labels = new Labels(config); + this.projects = new Projects(config); + this.epics = new Epics(config); + } + + /** + * Create a new initiative + */ + async create(workspaceSlug: string, createInitiative: CreateInitiative): Promise { + return this.post(`/workspaces/${workspaceSlug}/initiatives/`, createInitiative); + } + + /** + * Retrieve an initiative by ID + */ + async retrieve(workspaceSlug: string, initiativeId: string): Promise { + return this.get(`/workspaces/${workspaceSlug}/initiatives/${initiativeId}/`); + } + + /** + * Update an initiative + */ + async update(workspaceSlug: string, initiativeId: string, updateInitiative: UpdateInitiative): Promise { + return this.patch(`/workspaces/${workspaceSlug}/initiatives/${initiativeId}/`, updateInitiative); + } + + /** + * Delete an initiative + */ + async delete(workspaceSlug: string, initiativeId: string): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/initiatives/${initiativeId}/`); + } + + /** + * List initiatives with optional filtering + */ + async list(workspaceSlug: string, params?: ListInitiativesParams): Promise> { + return this.get>(`/workspaces/${workspaceSlug}/initiatives/`, params); + } +} + diff --git a/src/api/Projects.ts b/src/api/Projects.ts index 5b0159a..c551e49 100644 --- a/src/api/Projects.ts +++ b/src/api/Projects.ts @@ -3,6 +3,7 @@ import { Configuration } from "../Configuration"; import { Project, CreateProject, UpdateProject, ListProjectsParams } from "../models/Project"; import { PaginatedResponse } from "../models/common"; import { User } from "../models/User"; +import { ProjectFeatures, UpdateProjectFeatures } from "../models/ProjectFeatures"; /** * Project API resource @@ -65,4 +66,22 @@ export class Projects extends BaseResource { async getTotalWorkLogs(workspaceSlug: string, projectId: string): Promise { return this.get(`/workspaces/${workspaceSlug}/projects/${projectId}/work-logs/total/`); } + + /** + * Retrieve project features + */ + async retrieveFeatures(workspaceSlug: string, projectId: string): Promise { + return this.get(`/workspaces/${workspaceSlug}/projects/${projectId}/features/`); + } + + /** + * Update project features + */ + async updateFeatures( + workspaceSlug: string, + projectId: string, + updateFeatures: UpdateProjectFeatures + ): Promise { + return this.patch(`/workspaces/${workspaceSlug}/projects/${projectId}/features/`, updateFeatures); + } } diff --git a/src/api/Stickies.ts b/src/api/Stickies.ts new file mode 100644 index 0000000..7c6d7e9 --- /dev/null +++ b/src/api/Stickies.ts @@ -0,0 +1,49 @@ +import { BaseResource } from "./BaseResource"; +import { Configuration } from "../Configuration"; +import { PaginatedResponse } from "../models/common"; +import { Sticky, CreateSticky, UpdateSticky, ListStickiesParams } from "../models/Sticky"; + +/** + * Sticky API resource + * Handles all sticky related operations + */ +export class Stickies extends BaseResource { + constructor(config: Configuration) { + super(config); + } + + /** + * Create a new sticky + */ + async create(workspaceSlug: string, createSticky: CreateSticky): Promise { + return this.post(`/workspaces/${workspaceSlug}/stickies/`, createSticky); + } + + /** + * Retrieve a sticky by ID + */ + async retrieve(workspaceSlug: string, stickyId: string): Promise { + return this.get(`/workspaces/${workspaceSlug}/stickies/${stickyId}/`); + } + + /** + * Update a sticky + */ + async update(workspaceSlug: string, stickyId: string, updateSticky: UpdateSticky): Promise { + return this.patch(`/workspaces/${workspaceSlug}/stickies/${stickyId}/`, updateSticky); + } + + /** + * Delete a sticky + */ + async delete(workspaceSlug: string, stickyId: string): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/stickies/${stickyId}/`); + } + + /** + * List stickies with optional filtering + */ + async list(workspaceSlug: string, params?: ListStickiesParams): Promise> { + return this.get>(`/workspaces/${workspaceSlug}/stickies/`, params); + } +} diff --git a/src/api/Teamspaces/Members.ts b/src/api/Teamspaces/Members.ts new file mode 100644 index 0000000..6f34299 --- /dev/null +++ b/src/api/Teamspaces/Members.ts @@ -0,0 +1,37 @@ +import { BaseResource } from "../BaseResource"; +import { Configuration } from "../../Configuration"; +import { User } from "../../models/User"; +import { PaginatedResponse } from "../../models/common"; +import { AddTeamspaceMembersRequest, RemoveTeamspaceMembersRequest } from "../../models/Teamspace"; + +/** + * Teamspace Members API resource + * Handles teamspace member relationships + */ +export class Members extends BaseResource { + constructor(config: Configuration) { + super(config); + } + + /** + * Get members associated with a teamspace + */ + async list(workspaceSlug: string, teamspaceId: string, params?: { limit?: number; offset?: number }): Promise> { + return this.get>(`/workspaces/${workspaceSlug}/teamspaces/${teamspaceId}/members/`, params); + } + + /** + * Add members to a teamspace + */ + async add(workspaceSlug: string, teamspaceId: string, addMembers: AddTeamspaceMembersRequest): Promise { + return this.post(`/workspaces/${workspaceSlug}/teamspaces/${teamspaceId}/members/`, addMembers); + } + + /** + * Remove members from a teamspace + */ + async remove(workspaceSlug: string, teamspaceId: string, removeMembers: RemoveTeamspaceMembersRequest): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/teamspaces/${teamspaceId}/members/`, removeMembers); + } +} + diff --git a/src/api/Teamspaces/Projects.ts b/src/api/Teamspaces/Projects.ts new file mode 100644 index 0000000..9967cf8 --- /dev/null +++ b/src/api/Teamspaces/Projects.ts @@ -0,0 +1,37 @@ +import { BaseResource } from "../BaseResource"; +import { Configuration } from "../../Configuration"; +import { Project } from "../../models/Project"; +import { PaginatedResponse } from "../../models/common"; +import { AddTeamspaceProjectsRequest, RemoveTeamspaceProjectsRequest } from "../../models/Teamspace"; + +/** + * Teamspace Projects API resource + * Handles teamspace project relationships + */ +export class Projects extends BaseResource { + constructor(config: Configuration) { + super(config); + } + + /** + * Get projects associated with a teamspace + */ + async list(workspaceSlug: string, teamspaceId: string, params?: { limit?: number; offset?: number }): Promise> { + return this.get>(`/workspaces/${workspaceSlug}/teamspaces/${teamspaceId}/projects/`, params); + } + + /** + * Add projects to a teamspace + */ + async add(workspaceSlug: string, teamspaceId: string, addProjects: AddTeamspaceProjectsRequest): Promise { + return this.post(`/workspaces/${workspaceSlug}/teamspaces/${teamspaceId}/projects/`, addProjects); + } + + /** + * Remove projects from a teamspace + */ + async remove(workspaceSlug: string, teamspaceId: string, removeProjects: RemoveTeamspaceProjectsRequest): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/teamspaces/${teamspaceId}/projects/`, removeProjects); + } +} + diff --git a/src/api/Teamspaces/index.ts b/src/api/Teamspaces/index.ts new file mode 100644 index 0000000..fc45298 --- /dev/null +++ b/src/api/Teamspaces/index.ts @@ -0,0 +1,57 @@ +import { BaseResource } from "../BaseResource"; +import { Configuration } from "../../Configuration"; +import { Teamspace, CreateTeamspace, UpdateTeamspace, ListTeamspacesParams } from "../../models/Teamspace"; +import { PaginatedResponse } from "../../models/common"; +import { Projects } from "./Projects"; +import { Members } from "./Members"; + +/** + * Teamspaces API resource + * Handles all teamspace-related operations + */ +export class Teamspaces extends BaseResource { + public projects: Projects; + public members: Members; + + constructor(config: Configuration) { + super(config); + this.projects = new Projects(config); + this.members = new Members(config); + } + + /** + * Create a new teamspace + */ + async create(workspaceSlug: string, createTeamspace: CreateTeamspace): Promise { + return this.post(`/workspaces/${workspaceSlug}/teamspaces/`, createTeamspace); + } + + /** + * Retrieve a teamspace by ID + */ + async retrieve(workspaceSlug: string, teamspaceId: string): Promise { + return this.get(`/workspaces/${workspaceSlug}/teamspaces/${teamspaceId}/`); + } + + /** + * Update a teamspace + */ + async update(workspaceSlug: string, teamspaceId: string, updateTeamspace: UpdateTeamspace): Promise { + return this.patch(`/workspaces/${workspaceSlug}/teamspaces/${teamspaceId}/`, updateTeamspace); + } + + /** + * Delete a teamspace + */ + async delete(workspaceSlug: string, teamspaceId: string): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/teamspaces/${teamspaceId}/`); + } + + /** + * List teamspaces with optional filtering + */ + async list(workspaceSlug: string, params?: ListTeamspacesParams): Promise> { + return this.get>(`/workspaces/${workspaceSlug}/teamspaces/`, params); + } +} + diff --git a/src/api/Workspace.ts b/src/api/Workspace.ts index a2cdbc9..11968f0 100644 --- a/src/api/Workspace.ts +++ b/src/api/Workspace.ts @@ -1,6 +1,7 @@ import { BaseResource } from "./BaseResource"; import { Configuration } from "../Configuration"; import { User } from "../models/User"; +import { UpdateWorkspaceFeatures, WorkspaceFeatures } from "../models/WorkspaceFeatures"; /** * Workspace API resource @@ -17,4 +18,18 @@ export class Workspace extends BaseResource { async getMembers(workspaceSlug: string): Promise { return this.get(`/workspaces/${workspaceSlug}/members/`); } + + /** + * Retrieve workspace features + */ + async retrieveFeatures(workspaceSlug: string): Promise { + return this.get(`/workspaces/${workspaceSlug}/features/`); + } + + /** + * Update workspace features + */ + async updateFeatures(workspaceSlug: string, updateFeatures: UpdateWorkspaceFeatures): Promise { + return this.patch(`/workspaces/${workspaceSlug}/features/`, updateFeatures); + } } diff --git a/src/client/plane-client.ts b/src/client/plane-client.ts index 907a9d6..4891b43 100644 --- a/src/client/plane-client.ts +++ b/src/client/plane-client.ts @@ -15,6 +15,9 @@ import { Users } from "../api/Users"; import { Workspace } from "../api/Workspace"; import { Epics } from "../api/Epics"; import { Intake } from "../api/Intake"; +import { Stickies } from "../api/Stickies"; +import { Teamspaces } from "../api/Teamspaces"; +import { Initiatives } from "../api/Initiatives"; /** * Main Plane Client class @@ -38,6 +41,9 @@ export class PlaneClient { public workspace: Workspace; public epics: Epics; public intake: Intake; + public stickies: Stickies; + public teamspaces: Teamspaces; + public initiatives: Initiatives; constructor(config: { baseUrl?: string; apiKey?: string; accessToken?: string; enableLogging?: boolean }) { this.config = new Configuration({ @@ -67,5 +73,8 @@ export class PlaneClient { this.workspace = new Workspace(this.config); this.epics = new Epics(this.config); this.intake = new Intake(this.config); + this.stickies = new Stickies(this.config); + this.teamspaces = new Teamspaces(this.config); + this.initiatives = new Initiatives(this.config); } } diff --git a/src/index.ts b/src/index.ts index 781508b..27290ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,9 @@ export { Users } from "./api/Users"; export { Workspace } from "./api/Workspace"; export { Epics } from "./api/Epics"; export { Intake } from "./api/Intake"; +export { Stickies } from "./api/Stickies"; +export { Teamspaces } from "./api/Teamspaces"; +export { Initiatives } from "./api/Initiatives"; // Sub-resources export { Relations as WorkItemRelations } from "./api/WorkItems/Relations"; @@ -35,6 +38,11 @@ export { Options as WorkItemPropertyOptions } from "./api/WorkItemProperties/Opt export { Values as WorkItemPropertyValues } from "./api/WorkItemProperties/Values"; export { Properties as CustomerProperties } from "./api/Customers/Properties"; export { Requests as CustomerRequests } from "./api/Customers/Requests"; +export { Projects as TeamspaceProjects } from "./api/Teamspaces/Projects"; +export { Members as TeamspaceMembers } from "./api/Teamspaces/Members"; +export { Labels as InitiativeLabels } from "./api/Initiatives/Labels"; +export { Projects as InitiativeProjects } from "./api/Initiatives/Projects"; +export { Epics as InitiativeEpics } from "./api/Initiatives/Epics"; // Models export * from "./models"; diff --git a/src/models/Initiative.ts b/src/models/Initiative.ts new file mode 100644 index 0000000..ca23698 --- /dev/null +++ b/src/models/Initiative.ts @@ -0,0 +1,62 @@ +import { BaseModel, LogoProps } from "./common"; + +export enum InitiativeState { + DRAFT = "DRAFT", + PLANNED = "PLANNED", + ACTIVE = "ACTIVE", + COMPLETED = "COMPLETED", + CLOSED = "CLOSED", +} +/** + * Initiative model interfaces + */ +export interface Initiative extends BaseModel { + name: string; + description?: string; + description_html?: string; + description_stripped?: string; + description_binary?: string; + lead?: string; + start_date?: string; + end_date?: string; + logo_props?: LogoProps; + state?: InitiativeState; + workspace: string; +} + +export type CreateInitiative = Omit< + Initiative, + "id" | "created_at" | "updated_at" | "deleted_at" | "created_by" | "updated_by" | "workspace" +>; + +export type UpdateInitiative = Partial; + +export interface ListInitiativesParams { + limit?: number; + offset?: number; + [key: string]: any; +} + +export interface AddInitiativeLabelsRequest { + label_ids: string[]; +} + +export interface RemoveInitiativeLabelsRequest { + label_ids: string[]; +} + +export interface AddInitiativeProjectsRequest { + project_ids: string[]; +} + +export interface RemoveInitiativeProjectsRequest { + project_ids: string[]; +} + +export interface AddInitiativeEpicsRequest { + epic_ids: string[]; +} + +export interface RemoveInitiativeEpicsRequest { + epic_ids: string[]; +} diff --git a/src/models/InitiativeLabel.ts b/src/models/InitiativeLabel.ts new file mode 100644 index 0000000..eaf443b --- /dev/null +++ b/src/models/InitiativeLabel.ts @@ -0,0 +1,27 @@ +import { BaseModel } from "./common"; + +/** + * InitiativeLabel model interfaces + */ +export interface InitiativeLabel extends BaseModel { + name: string; + description?: string; + color?: string; + sort_order: number; + workspace: string; +} + +export type CreateInitiativeLabel = Omit< + InitiativeLabel, + "id" | "created_at" | "updated_at" | "deleted_at" | "created_by" | "updated_by" | "workspace" +> & { + sort_order?: number; +}; + +export type UpdateInitiativeLabel = Partial; + +export interface ListInitiativeLabelsParams { + limit?: number; + offset?: number; + [key: string]: any; +} diff --git a/src/models/ProjectFeatures.ts b/src/models/ProjectFeatures.ts new file mode 100644 index 0000000..dc7b031 --- /dev/null +++ b/src/models/ProjectFeatures.ts @@ -0,0 +1,14 @@ +/** + * Project Features model interfaces + */ +export interface ProjectFeatures { + epics: boolean; + modules: boolean; + cycles: boolean; + views: boolean; + pages: boolean; + intakes: boolean; + work_item_types: boolean; +} + +export type UpdateProjectFeatures = Partial; diff --git a/src/models/Sticky.ts b/src/models/Sticky.ts new file mode 100644 index 0000000..410ba64 --- /dev/null +++ b/src/models/Sticky.ts @@ -0,0 +1,42 @@ +import { BaseModel, LogoProps } from "./common"; + +/** + * Sticky model interfaces + */ +export interface Sticky extends BaseModel { + name?: string; + description?: Record; + description_html?: string; + description_stripped?: string; + description_binary?: string; + logo_props?: LogoProps; + color?: string; + background_color?: string; + workspace: string; + owner: string; + sort_order: number; +} + +export type CreateSticky = Omit< + Sticky, + | "id" + | "created_at" + | "updated_at" + | "deleted_at" + | "created_by" + | "updated_by" + | "workspace" + | "owner" + | "sort_order" + | "description_binary" + | "description_stripped" + | "description" +>; + +export type UpdateSticky = Partial; + +export interface ListStickiesParams { + limit?: number; + offset?: number; + [key: string]: any; +} diff --git a/src/models/Teamspace.ts b/src/models/Teamspace.ts new file mode 100644 index 0000000..b77e777 --- /dev/null +++ b/src/models/Teamspace.ts @@ -0,0 +1,44 @@ +import { BaseModel, LogoProps } from "./common"; + +/** + * Teamspace model interfaces + */ +export interface Teamspace extends BaseModel { + name: string; + description_json?: Record; + description_html?: string; + description_stripped?: string; + description_binary?: string; + logo_props?: LogoProps; + lead?: string; + workspace: string; +} + +export type CreateTeamspace = Omit< + Teamspace, + "id" | "created_at" | "updated_at" | "deleted_at" | "created_by" | "updated_by" | "workspace" +>; + +export type UpdateTeamspace = Partial; + +export interface ListTeamspacesParams { + limit?: number; + offset?: number; + [key: string]: any; +} + +export interface AddTeamspaceProjectsRequest { + project_ids: string[]; +} + +export interface RemoveTeamspaceProjectsRequest { + project_ids: string[]; +} + +export interface AddTeamspaceMembersRequest { + member_ids: string[]; +} + +export interface RemoveTeamspaceMembersRequest { + member_ids: string[]; +} diff --git a/src/models/WorkspaceFeatures.ts b/src/models/WorkspaceFeatures.ts new file mode 100644 index 0000000..98a801e --- /dev/null +++ b/src/models/WorkspaceFeatures.ts @@ -0,0 +1,13 @@ +/** + * Workspace Features model interfaces + */ +export interface WorkspaceFeatures { + project_grouping: boolean; + initiatives: boolean; + teams: boolean; + customers: boolean; + wiki: boolean; + pi: boolean; +} + +export type UpdateWorkspaceFeatures = Partial; diff --git a/src/models/index.ts b/src/models/index.ts index bbac8b4..55a8849 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,4 +1,6 @@ export * from "./Project"; +export * from "./ProjectFeatures"; +export * from "./WorkspaceFeatures"; export * from "./WorkItem"; export * from "./WorkItemProperty"; export * from "./Comment"; @@ -8,3 +10,7 @@ export * from "./Customer"; export * from "./Page"; export * from "./Cycle"; export * from "./Module"; +export * from "./Sticky"; +export * from "./Teamspace"; +export * from "./InitiativeLabel"; +export * from "./Initiative"; diff --git a/tests/unit/initiative.test.ts b/tests/unit/initiative.test.ts new file mode 100644 index 0000000..ffdbcbf --- /dev/null +++ b/tests/unit/initiative.test.ts @@ -0,0 +1,265 @@ +import { PlaneClient } from "../../src/client/plane-client"; +import { Initiative, InitiativeState, UpdateInitiative } from "../../src/models/Initiative"; +import { InitiativeLabel } from "../../src/models/InitiativeLabel"; +import { Project } from "../../src/models/Project"; +import { Epic } from "../../src/models/Epic"; +import { config } from "./constants"; +import { createTestClient, randomizeName } from "../helpers/test-utils"; +import { describeIf } from "../helpers/conditional-tests"; + +describeIf(!!(config.workspaceSlug && config.projectId), "Initiative API Tests", () => { + let client: PlaneClient; + let workspaceSlug: string; + let projectId: string; + let initiative: Initiative; + let initiativeLabel: InitiativeLabel; + let testProject: Project; + let testEpic: Epic; + + beforeAll(async () => { + client = createTestClient(); + workspaceSlug = config.workspaceSlug; + projectId = config.projectId; + + // Create a test project for initiative operations + testProject = await client.projects.create(workspaceSlug, { + name: randomizeName("Test Project for Initiative"), + identifier: randomizeName("TPI").slice(0, 5).toUpperCase(), + }); + + // Create an initiative label + initiativeLabel = await client.initiatives.labels.create(workspaceSlug, { + name: randomizeName("Test Initiative Label"), + color: "#FF5733", + sort_order: 100, + }); + + // Create a test epic if epics are available + try { + const epics = await client.epics.list(workspaceSlug, projectId); + if (epics.results.length > 0) { + testEpic = epics.results[0]; + } + } catch (error) { + console.warn("Epics not available for testing:", error); + } + }); + + afterAll(async () => { + // Clean up created resources + if (initiative?.id) { + try { + await client.initiatives.delete(workspaceSlug, initiative.id); + } catch (error) { + console.warn("Failed to delete initiative:", error); + } + } + if (initiativeLabel?.id) { + try { + await client.initiatives.labels.delete(workspaceSlug, initiativeLabel.id); + } catch (error) { + console.warn("Failed to delete initiative label:", error); + } + } + if (testProject?.id) { + try { + await client.projects.delete(workspaceSlug, testProject.id); + } catch (error) { + console.warn("Failed to delete test project:", error); + } + } + }); + + it("should create an initiative", async () => { + initiative = await client.initiatives.create(workspaceSlug, { + name: randomizeName("Test Initiative"), + description: "Test Initiative Description", + state: InitiativeState.ACTIVE, + }); + + expect(initiative).toBeDefined(); + expect(initiative.id).toBeDefined(); + expect(initiative.name).toContain("Test Initiative"); + expect(initiative.description).toBe("Test Initiative Description"); + expect(initiative.state).toBe(InitiativeState.ACTIVE); + }); + + it("should retrieve an initiative", async () => { + const retrievedInitiative = await client.initiatives.retrieve(workspaceSlug, initiative.id); + + expect(retrievedInitiative).toBeDefined(); + expect(retrievedInitiative.id).toBe(initiative.id); + expect(retrievedInitiative.name).toBe(initiative.name); + expect(retrievedInitiative.state).toBe(initiative.state); + }); + + it("should update an initiative", async () => { + const updateData: UpdateInitiative = { + name: randomizeName("Updated Test Initiative"), + description: "Updated Test Initiative Description", + state: InitiativeState.PLANNED, + }; + + const updatedInitiative = await client.initiatives.update(workspaceSlug, initiative.id, updateData); + + expect(updatedInitiative).toBeDefined(); + expect(updatedInitiative.id).toBe(initiative.id); + expect(updatedInitiative.name).toContain("Updated Test Initiative"); + expect(updatedInitiative.state).toBe(InitiativeState.PLANNED); + }); + + it("should list initiatives", async () => { + const initiatives = await client.initiatives.list(workspaceSlug); + + expect(initiatives).toBeDefined(); + expect(Array.isArray(initiatives.results)).toBe(true); + expect(initiatives.results.length).toBeGreaterThan(0); + + const foundInitiative = initiatives.results.find((i) => i.id === initiative.id); + expect(foundInitiative).toBeDefined(); + }); + + describe("Initiative Labels", () => { + it("should create an initiative label", async () => { + expect(initiativeLabel).toBeDefined(); + expect(initiativeLabel.id).toBeDefined(); + expect(initiativeLabel.name).toContain("Test Initiative Label"); + }); + + it("should retrieve an initiative label", async () => { + const retrievedLabel = await client.initiatives.labels.retrieve(workspaceSlug, initiativeLabel.id); + + expect(retrievedLabel).toBeDefined(); + expect(retrievedLabel.id).toBe(initiativeLabel.id); + expect(retrievedLabel.name).toBe(initiativeLabel.name); + }); + + it("should update an initiative label", async () => { + const updatedLabel = await client.initiatives.labels.update(workspaceSlug, initiativeLabel.id, { + name: randomizeName("Updated Test Initiative Label"), + color: "#33FF57", + }); + + expect(updatedLabel).toBeDefined(); + expect(updatedLabel.id).toBe(initiativeLabel.id); + expect(updatedLabel.name).toContain("Updated Test Initiative Label"); + expect(updatedLabel.color).toBe("#33FF57"); + }); + + it("should list initiative labels", async () => { + const labels = await client.initiatives.labels.list(workspaceSlug); + + expect(labels).toBeDefined(); + expect(Array.isArray(labels.results)).toBe(true); + expect(labels.results.length).toBeGreaterThan(0); + + const foundLabel = labels.results.find((l) => l.id === initiativeLabel.id); + expect(foundLabel).toBeDefined(); + }); + + it("should add labels to initiative", async () => { + const labels = await client.initiatives.labels.addLabels(workspaceSlug, initiative.id, { + label_ids: [initiativeLabel.id], + }); + + expect(labels).toBeDefined(); + expect(Array.isArray(labels)).toBe(true); + expect(labels.length).toBeGreaterThan(0); + }); + + it("should list labels in initiative", async () => { + const labels = await client.initiatives.labels.listLabels(workspaceSlug, initiative.id); + + expect(labels).toBeDefined(); + expect(Array.isArray(labels.results)).toBe(true); + expect(labels.results.length).toBeGreaterThan(0); + + const foundLabel = labels.results.find((l) => l.id === initiativeLabel.id); + expect(foundLabel).toBeDefined(); + }); + + it("should remove labels from initiative", async () => { + await client.initiatives.labels.removeLabels(workspaceSlug, initiative.id, { + label_ids: [initiativeLabel.id], + }); + + const labels = await client.initiatives.labels.listLabels(workspaceSlug, initiative.id); + const foundLabel = labels.results.find((l) => l.id === initiativeLabel.id); + expect(foundLabel).toBeUndefined(); + }); + }); + + describe("Initiative Projects", () => { + it("should add projects to initiative", async () => { + const projects = await client.initiatives.projects.add(workspaceSlug, initiative.id, { + project_ids: [testProject.id], + }); + + expect(projects).toBeDefined(); + expect(Array.isArray(projects)).toBe(true); + expect(projects.length).toBeGreaterThan(0); + }); + + it("should list projects in initiative", async () => { + const projects = await client.initiatives.projects.list(workspaceSlug, initiative.id); + + expect(projects).toBeDefined(); + expect(Array.isArray(projects.results)).toBe(true); + expect(projects.results.length).toBeGreaterThan(0); + + const foundProject = projects.results.find((p) => p.id === testProject.id); + expect(foundProject).toBeDefined(); + }); + + it("should remove projects from initiative", async () => { + await client.initiatives.projects.remove(workspaceSlug, initiative.id, { + project_ids: [testProject.id], + }); + + const projects = await client.initiatives.projects.list(workspaceSlug, initiative.id); + const foundProject = projects.results.find((p) => p.id === testProject.id); + expect(foundProject).toBeUndefined(); + }); + }); + + describe("Initiative Epics", () => { + it("should add epics to initiative", async () => { + if (!testEpic?.id) { + return; + } + + const epics = await client.initiatives.epics.add(workspaceSlug, initiative.id, { + epic_ids: [testEpic.id], + }); + + expect(epics).toBeDefined(); + expect(Array.isArray(epics)).toBe(true); + }); + + it("should list epics in initiative", async () => { + if (!testEpic?.id) { + return; + } + + const epics = await client.initiatives.epics.list(workspaceSlug, initiative.id); + + expect(epics).toBeDefined(); + expect(Array.isArray(epics.results)).toBe(true); + }); + + it("should remove epics from initiative", async () => { + if (!testEpic?.id) { + return; + } + + await client.initiatives.epics.remove(workspaceSlug, initiative.id, { + epic_ids: [testEpic.id], + }); + + const epics = await client.initiatives.epics.list(workspaceSlug, initiative.id); + const foundEpic = epics.results.find((e) => e.id === testEpic.id); + expect(foundEpic).toBeUndefined(); + }); + }); +}); + diff --git a/tests/unit/project.test.ts b/tests/unit/project.test.ts index e029e35..da4303a 100644 --- a/tests/unit/project.test.ts +++ b/tests/unit/project.test.ts @@ -80,4 +80,38 @@ describe(!!config.workspaceSlug, "Project API Tests", () => { expect(members).toBeDefined(); expect(Array.isArray(members)).toBe(true); }); + + describe(!!config.workspaceSlug, "Project Features", () => { + it("should retrieve project features", async () => { + const features = await client.projects.retrieveFeatures(workspaceSlug, project.id); + + expect(features).toBeDefined(); + expect(typeof features.epics).toBe("boolean"); + expect(typeof features.modules).toBe("boolean"); + expect(typeof features.cycles).toBe("boolean"); + expect(typeof features.views).toBe("boolean"); + expect(typeof features.pages).toBe("boolean"); + expect(typeof features.intakes).toBe("boolean"); + expect(typeof features.work_item_types).toBe("boolean"); + }); + + it("should update project features", async () => { + const originalFeatures = await client.projects.retrieveFeatures(workspaceSlug, project.id); + + const updatedFeatures = await client.projects.updateFeatures(workspaceSlug, project.id, { + epics: !originalFeatures.epics, + modules: !originalFeatures.modules, + }); + + expect(updatedFeatures).toBeDefined(); + expect(updatedFeatures.epics).toBe(!originalFeatures.epics); + expect(updatedFeatures.modules).toBe(!originalFeatures.modules); + + // Restore original values + await client.projects.updateFeatures(workspaceSlug, project.id, { + epics: originalFeatures.epics, + modules: originalFeatures.modules, + }); + }); + }); }); diff --git a/tests/unit/sticky.test.ts b/tests/unit/sticky.test.ts new file mode 100644 index 0000000..779b30a --- /dev/null +++ b/tests/unit/sticky.test.ts @@ -0,0 +1,79 @@ +import { PlaneClient } from "../../src/client/plane-client"; +import { Sticky, UpdateSticky } from "../../src/models/Sticky"; +import { config } from "./constants"; +import { createTestClient, randomizeName } from "../helpers/test-utils"; +import { describeIf } from "../helpers/conditional-tests"; + +describeIf(!!config.workspaceSlug, "Sticky API Tests", () => { + let client: PlaneClient; + let workspaceSlug: string; + let sticky: Sticky; + + beforeAll(async () => { + client = createTestClient(); + workspaceSlug = config.workspaceSlug; + }); + + afterAll(async () => { + // Clean up created sticky + if (sticky?.id) { + try { + await client.stickies.delete(workspaceSlug, sticky.id); + } catch (error) { + console.warn("Failed to delete sticky:", error); + } + } + }); + + it("should create a sticky", async () => { + sticky = await client.stickies.create(workspaceSlug, { + name: randomizeName("Test Sticky"), + description_html: "

Test Sticky Description

", + color: "#FF5733", + }); + + expect(sticky).toBeDefined(); + expect(sticky.id).toBeDefined(); + expect(sticky.name).toContain("Test Sticky"); + expect(sticky.description_html).toBe("

Test Sticky Description

"); + expect(sticky.color).toBe("#FF5733"); + }); + + it("should retrieve a sticky", async () => { + const retrievedSticky = await client.stickies.retrieve(workspaceSlug, sticky.id); + + expect(retrievedSticky).toBeDefined(); + expect(retrievedSticky.id).toBe(sticky.id); + expect(retrievedSticky.name).toBe(sticky.name); + expect(retrievedSticky.description_html).toBe(sticky.description_html); + }); + + it("should update a sticky", async () => { + const updateData: UpdateSticky = { + name: randomizeName("Updated Test Sticky"), + description_html: "

Updated Test Sticky Description

", + color: "#33FF57", + }; + + const updatedSticky = await client.stickies.update(workspaceSlug, sticky.id, updateData); + + expect(updatedSticky).toBeDefined(); + expect(updatedSticky.id).toBe(sticky.id); + expect(updatedSticky.name).toContain("Updated Test Sticky"); + expect(updatedSticky.description_html).toBe("

Updated Test Sticky Description

"); + expect(updatedSticky.color).toBe("#33FF57"); + }); + + it("should list stickies", async () => { + const stickies = await client.stickies.list(workspaceSlug); + + expect(stickies).toBeDefined(); + expect(Array.isArray(stickies.results)).toBe(true); + expect(stickies.results.length).toBeGreaterThan(0); + + const foundSticky = stickies.results.find((s) => s.id === sticky.id); + expect(foundSticky).toBeDefined(); + expect(foundSticky?.name).toContain("Updated Test Sticky"); + }); +}); + diff --git a/tests/unit/teamspace.test.ts b/tests/unit/teamspace.test.ts new file mode 100644 index 0000000..4eaca1e --- /dev/null +++ b/tests/unit/teamspace.test.ts @@ -0,0 +1,161 @@ +import { PlaneClient } from "../../src/client/plane-client"; +import { Teamspace, UpdateTeamspace } from "../../src/models/Teamspace"; +import { Project } from "../../src/models/Project"; +import { User } from "../../src/models/User"; +import { config } from "./constants"; +import { createTestClient, randomizeName } from "../helpers/test-utils"; +import { describeIf } from "../helpers/conditional-tests"; + +describeIf(!!(config.workspaceSlug), "Teamspace API Tests", () => { + let client: PlaneClient; + let workspaceSlug: string; + let teamspace: Teamspace; + let testProject: Project; + + beforeAll(async () => { + client = createTestClient(); + workspaceSlug = config.workspaceSlug; + + // Create a test project for teamspace operations + testProject = await client.projects.create(workspaceSlug, { + name: randomizeName("Test Project for Teamspace"), + identifier: randomizeName("TPT").slice(0, 5).toUpperCase(), + }); + }); + + afterAll(async () => { + // Clean up created resources + if (teamspace?.id) { + try { + await client.teamspaces.delete(workspaceSlug, teamspace.id); + } catch (error) { + console.warn("Failed to delete teamspace:", error); + } + } + if (testProject?.id) { + try { + await client.projects.delete(workspaceSlug, testProject.id); + } catch (error) { + console.warn("Failed to delete test project:", error); + } + } + }); + + it("should create a teamspace", async () => { + teamspace = await client.teamspaces.create(workspaceSlug, { + name: randomizeName("Test Teamspace"), + description_html: "

Test Teamspace Description

", + }); + + expect(teamspace).toBeDefined(); + expect(teamspace.id).toBeDefined(); + expect(teamspace.name).toContain("Test Teamspace"); + expect(teamspace.description_html).toBe("

Test Teamspace Description

"); + }); + + it("should retrieve a teamspace", async () => { + const retrievedTeamspace = await client.teamspaces.retrieve(workspaceSlug, teamspace.id); + + expect(retrievedTeamspace).toBeDefined(); + expect(retrievedTeamspace.id).toBe(teamspace.id); + expect(retrievedTeamspace.name).toBe(teamspace.name); + }); + + it("should update a teamspace", async () => { + const updateData: UpdateTeamspace = { + name: randomizeName("Updated Test Teamspace"), + description_html: "

Updated Test Teamspace Description

", + }; + + const updatedTeamspace = await client.teamspaces.update(workspaceSlug, teamspace.id, updateData); + + expect(updatedTeamspace).toBeDefined(); + expect(updatedTeamspace.id).toBe(teamspace.id); + expect(updatedTeamspace.name).toContain("Updated Test Teamspace"); + }); + + it("should list teamspaces", async () => { + const teamspaces = await client.teamspaces.list(workspaceSlug); + + expect(teamspaces).toBeDefined(); + expect(Array.isArray(teamspaces.results)).toBe(true); + expect(teamspaces.results.length).toBeGreaterThan(0); + + const foundTeamspace = teamspaces.results.find((t) => t.id === teamspace.id); + expect(foundTeamspace).toBeDefined(); + }); + + describe("Teamspace Projects", () => { + it("should add projects to teamspace", async () => { + const projects = await client.teamspaces.projects.add(workspaceSlug, teamspace.id, { + project_ids: [testProject.id], + }); + + expect(projects).toBeDefined(); + expect(Array.isArray(projects)).toBe(true); + expect(projects.length).toBeGreaterThan(0); + }); + + it("should list projects in teamspace", async () => { + const projects = await client.teamspaces.projects.list(workspaceSlug, teamspace.id); + + expect(projects).toBeDefined(); + expect(Array.isArray(projects.results)).toBe(true); + expect(projects.results.length).toBeGreaterThan(0); + + const foundProject = projects.results.find((p) => p.id === testProject.id); + expect(foundProject).toBeDefined(); + }); + + it("should remove projects from teamspace", async () => { + await client.teamspaces.projects.remove(workspaceSlug, teamspace.id, { + project_ids: [testProject.id], + }); + + const projects = await client.teamspaces.projects.list(workspaceSlug, teamspace.id); + const foundProject = projects.results.find((p) => p.id === testProject.id); + expect(foundProject).toBeUndefined(); + }); + }); + + describe("Teamspace Members", () => { + let testUser: User; + + beforeAll(async () => { + // Get current user for member operations + testUser = await client.users.me(); + }); + + it("should add members to teamspace", async () => { + const members = await client.teamspaces.members.add(workspaceSlug, teamspace.id, { + member_ids: [testUser.id!], + }); + + expect(members).toBeDefined(); + expect(Array.isArray(members)).toBe(true); + expect(members.length).toBeGreaterThan(0); + }); + + it("should list members in teamspace", async () => { + const members = await client.teamspaces.members.list(workspaceSlug, teamspace.id); + + expect(members).toBeDefined(); + expect(Array.isArray(members.results)).toBe(true); + expect(members.results.length).toBeGreaterThan(0); + + const foundMember = members.results.find((m) => m.id === testUser.id); + expect(foundMember).toBeDefined(); + }); + + it("should remove members from teamspace", async () => { + await client.teamspaces.members.remove(workspaceSlug, teamspace.id, { + member_ids: [testUser.id!], + }); + + const members = await client.teamspaces.members.list(workspaceSlug, teamspace.id); + const foundMember = members.results.find((m) => m.id === testUser.id); + expect(foundMember).toBeUndefined(); + }); + }); +}); + diff --git a/tests/unit/workspace-features.test.ts b/tests/unit/workspace-features.test.ts new file mode 100644 index 0000000..416b87d --- /dev/null +++ b/tests/unit/workspace-features.test.ts @@ -0,0 +1,46 @@ +import { PlaneClient } from "../../src/client/plane-client"; +import { config } from "./constants"; +import { createTestClient } from "../helpers/test-utils"; +import { describeIf } from "../helpers/conditional-tests"; + +describeIf(!!config.workspaceSlug, "Workspace Features API Tests", () => { + let client: PlaneClient; + let workspaceSlug: string; + + beforeAll(async () => { + client = createTestClient(); + workspaceSlug = config.workspaceSlug; + }); + + it("should retrieve workspace features", async () => { + const features = await client.workspace.retrieveFeatures(workspaceSlug); + + expect(features).toBeDefined(); + expect(typeof features.project_grouping).toBe("boolean"); + expect(typeof features.initiatives).toBe("boolean"); + expect(typeof features.teams).toBe("boolean"); + expect(typeof features.customers).toBe("boolean"); + expect(typeof features.wiki).toBe("boolean"); + expect(typeof features.pi).toBe("boolean"); + }); + + it("should update workspace features", async () => { + const originalFeatures = await client.workspace.retrieveFeatures(workspaceSlug); + + const updatedFeatures = await client.workspace.updateFeatures(workspaceSlug, { + project_grouping: !originalFeatures.project_grouping, + initiatives: !originalFeatures.initiatives, + }); + + expect(updatedFeatures).toBeDefined(); + expect(updatedFeatures.project_grouping).toBe(!originalFeatures.project_grouping); + expect(updatedFeatures.initiatives).toBe(!originalFeatures.initiatives); + + // Restore original values + await client.workspace.updateFeatures(workspaceSlug, { + project_grouping: originalFeatures.project_grouping, + initiatives: originalFeatures.initiatives, + }); + }); +}); +