diff --git a/package.json b/package.json index 9547378..36a360c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makeplane/plane-node-sdk", - "version": "0.2.12", + "version": "0.2.13", "description": "Node SDK for Plane", "author": "Plane ", "repository": { diff --git a/src/api/Pages.ts b/src/api/Pages.ts index 3fa1de0..37b8ef5 100644 --- a/src/api/Pages.ts +++ b/src/api/Pages.ts @@ -1,6 +1,6 @@ import { BaseResource } from "./BaseResource"; import { Configuration } from "../Configuration"; -import { Page, CreatePage } from "../models/Page"; +import { Page, CreatePage, UpdatePage } from "../models/Page"; import { PaginatedResponse } from "../models/common"; /** @@ -35,6 +35,41 @@ export class Pages extends BaseResource { return this.get>(`/workspaces/${workspaceSlug}/pages/`, params); } + /** + * Update a workspace page's name, content, or both. A page that is locked or + * archived is refused. + * + * Content is written through Plane's live collaboration service, which owns the + * document. If it is unreachable the API answers 502 and nothing is written, + * rather than leaving the editor showing the old text. + */ + async updateWorkspacePage(workspaceSlug: string, pageId: string, updatePage: UpdatePage): Promise { + return this.put(`/workspaces/${workspaceSlug}/pages/${pageId}/`, updatePage); + } + + /** + * Archive a workspace page. Archiving is the reversible step the API requires + * before a page can be deleted. + */ + async archiveWorkspacePage(workspaceSlug: string, pageId: string): Promise { + return this.post(`/workspaces/${workspaceSlug}/pages/${pageId}/archive/`); + } + + /** + * Restore an archived workspace page + */ + async unarchiveWorkspacePage(workspaceSlug: string, pageId: string): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/pages/${pageId}/archive/`); + } + + /** + * Delete a workspace page. The page must be archived first; the API answers 400 + * "The page should be archived before deleting" otherwise. + */ + async deleteWorkspacePage(workspaceSlug: string, pageId: string): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/pages/${pageId}/`); + } + // ===== PROJECT PAGES API METHODS ===== /** @@ -58,6 +93,46 @@ export class Pages extends BaseResource { return this.get>(`/workspaces/${workspaceSlug}/projects/${projectId}/pages/`, params); } + /** + * Update a project page's name, content, or both. A page that is locked or + * archived is refused. + * + * Content is written through Plane's live collaboration service, which owns the + * document. If it is unreachable the API answers 502 and nothing is written, + * rather than leaving the editor showing the old text. + */ + async updateProjectPage( + workspaceSlug: string, + projectId: string, + pageId: string, + updatePage: UpdatePage + ): Promise { + return this.put(`/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`, updatePage); + } + + /** + * Archive a project page. Archiving is the reversible step the API requires + * before a page can be deleted. + */ + async archiveProjectPage(workspaceSlug: string, projectId: string, pageId: string): Promise { + return this.post(`/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/archive/`); + } + + /** + * Restore an archived project page + */ + async unarchiveProjectPage(workspaceSlug: string, projectId: string, pageId: string): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/archive/`); + } + + /** + * Delete a project page. The page must be archived first; the API answers 400 + * "The page should be archived before deleting" otherwise. + */ + async deleteProjectPage(workspaceSlug: string, projectId: string, pageId: string): Promise { + return this.httpDelete(`/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`); + } + /** * Retrieve workspace page */ diff --git a/src/models/Page.ts b/src/models/Page.ts index 7b4e5c8..88f5e66 100644 --- a/src/models/Page.ts +++ b/src/models/Page.ts @@ -13,6 +13,19 @@ export interface Page extends BaseModel { description_stripped?: string; created_by: string; updated_by?: string; + /** 0 = public, 1 = private */ + access?: number; + is_locked?: boolean; + /** Set when the page is archived; deleting a page requires it to be archived first */ + archived_at?: string; + parent_id?: string; + /** + * Where the page is filed, and the id of the row that files it there -- the + * second is what addresses the membership when moving the page between + * collections. Both are null for a page in no collection. + */ + collection_id?: string; + page_collection_id?: string; // Additional fields will be added after API verification [key: string]: any; } @@ -23,3 +36,12 @@ export type CreatePage = Partial & { /** Create the page directly inside this collection */ collection_id?: string; }; + +/** + * Fields a page update may change. At least one is required; sending neither is + * refused with 400 "Either name or description_html is required." + */ +export interface UpdatePage { + name?: string; + description_html?: string; +} diff --git a/src/models/Project.ts b/src/models/Project.ts index a96dcd6..e8f94cd 100644 --- a/src/models/Project.ts +++ b/src/models/Project.ts @@ -14,6 +14,7 @@ export interface Project extends BaseModel { cover_image_url: null; name: string; description: string; + description_html?: string; network?: number; identifier?: string; emoji?: null; diff --git a/tests/unit/page.test.ts b/tests/unit/page.test.ts index 15aad15..bf779c3 100644 --- a/tests/unit/page.test.ts +++ b/tests/unit/page.test.ts @@ -72,4 +72,48 @@ describe(!!(config.workspaceSlug && config.projectId), "Page API Tests", () => { expect(Array.isArray(pages.results)).toBe(true); expect(pages.results.find((p) => p.id === projectPage.id)).toBeDefined(); }); + + it("should update a workspace page", async () => { + const name = randomizeName("Updated Workspace Page"); + const updated = await client.pages.updateWorkspacePage(workspaceSlug, workspacePage.id!, { name }); + + expect(updated.id).toBe(workspacePage.id); + expect(updated.name).toBe(name); + }); + + it("should update a project page's name and content", async () => { + const name = randomizeName("Updated Project Page"); + const updated = await client.pages.updateProjectPage(workspaceSlug, projectId, projectPage.id!, { + name, + description_html: "

Revised Content

", + }); + + expect(updated.id).toBe(projectPage.id); + expect(updated.name).toBe(name); + }); + + it("should refuse an update that carries no field to change", async () => { + // Refused rather than reported as a successful no-op. + await expect(client.pages.updateProjectPage(workspaceSlug, projectId, projectPage.id!, {})).rejects.toThrow(); + }); + + it("should archive and unarchive a workspace page", async () => { + await client.pages.archiveWorkspacePage(workspaceSlug, workspacePage.id!); + expect((await client.pages.retrieveWorkspacePage(workspaceSlug, workspacePage.id!)).archived_at).toBeTruthy(); + + await client.pages.unarchiveWorkspacePage(workspaceSlug, workspacePage.id!); + expect((await client.pages.retrieveWorkspacePage(workspaceSlug, workspacePage.id!)).archived_at).toBeFalsy(); + }); + + it("should refuse to delete a page that is not archived", async () => { + const page = await client.pages.createProjectPage(workspaceSlug, projectId, { + name: randomizeName("Test Delete Page"), + description_html: "

Draft

", + }); + + await expect(client.pages.deleteProjectPage(workspaceSlug, projectId, page.id!)).rejects.toThrow(); + + await client.pages.archiveProjectPage(workspaceSlug, projectId, page.id!); + await client.pages.deleteProjectPage(workspaceSlug, projectId, page.id!); + }); });