From b3d002d341841a5539688daabd2e8a1805acd06d Mon Sep 17 00:00:00 2001 From: Surya Prashanth Date: Fri, 28 Nov 2025 16:00:11 +0530 Subject: [PATCH 1/2] feat: add support to set options in create option property api --- src/api/WorkItemProperties/Values.ts | 60 +++- src/models/WorkItemProperty.ts | 25 +- .../properties-options.test.ts | 313 ++++++++++++------ .../work-item-types/properties-values.test.ts | 2 +- 4 files changed, 292 insertions(+), 108 deletions(-) diff --git a/src/api/WorkItemProperties/Values.ts b/src/api/WorkItemProperties/Values.ts index 36d0536..c70305a 100644 --- a/src/api/WorkItemProperties/Values.ts +++ b/src/api/WorkItemProperties/Values.ts @@ -1,10 +1,10 @@ -import { BaseResource } from '../BaseResource'; -import { Configuration } from '../../Configuration'; +import { BaseResource } from "../BaseResource"; +import { Configuration } from "../../Configuration"; import { UpdateWorkItemPropertyValue, ListWorkItemPropertyValuesParams, WorkItemPropertyValues, -} from '../../models/WorkItemProperty'; +} from "../../models/WorkItemProperty"; /** * WorkItemPropertyValues API resource @@ -22,7 +22,7 @@ export class Values extends BaseResource { workspaceSlug: string, projectId: string, workItemId: string, - propertyId: string, + propertyId: string ): Promise { const propertyValues = await this.get( `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${workItemId}/work-item-properties/${propertyId}/values/` @@ -45,9 +45,16 @@ export class Values extends BaseResource { ); } - /** * Create/update a property value + * + * For single-value properties: + * - Acts as an upsert operation (create or update) + * - Returns a single WorkItemPropertyValues + * + * For multi-value properties (is_multi=True): + * - Replaces all existing values with the new ones (sync operation) + * - Returns a list of values */ async create( workspaceSlug: string, @@ -61,4 +68,47 @@ export class Values extends BaseResource { updateData ); } + + /** + * Update an existing property value(s) (partial update) + * + * For single-value properties: + * - Updates the existing value + * - Returns a single WorkItemPropertyValues + * + * For multi-value properties (is_multi=True): + * - Replaces all existing values with the new ones (sync operation) + * - Returns a list of values + * + * @throws {HttpError} If the property value does not exist (404) + */ + async update( + workspaceSlug: string, + projectId: string, + workItemId: string, + propertyId: string, + updateData: UpdateWorkItemPropertyValue + ): Promise { + return this.patch( + `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${workItemId}/work-item-properties/${propertyId}/values/`, + updateData + ); + } + + /** + * Delete the property value(s) for a work item + * + * For single-value properties: + * - Deletes the single value + * + * For multi-value properties (is_multi=True): + * - Deletes all values for that property + * + * @throws {HttpError} If the property value does not exist (404) + */ + async delete(workspaceSlug: string, projectId: string, workItemId: string, propertyId: string): Promise { + return this.httpDelete( + `/workspaces/${workspaceSlug}/projects/${projectId}/work-items/${workItemId}/work-item-properties/${propertyId}/values/` + ); + } } diff --git a/src/models/WorkItemProperty.ts b/src/models/WorkItemProperty.ts index 8cc9980..de50f0a 100644 --- a/src/models/WorkItemProperty.ts +++ b/src/models/WorkItemProperty.ts @@ -102,6 +102,7 @@ export type TextSettings = { */ export interface WorkItemPropertyOption extends BaseModel { name: string; + description?: string; property: string; is_active?: boolean; sort_order?: number; @@ -137,8 +138,30 @@ export type WorkItemPropertyValues = { values: any[]; }[]; +/** + * UpdateWorkItemPropertyValue model interface + * Request model for creating/updating a work item property value. + * + * The value type depends on the property type: + * - TEXT/URL/EMAIL/FILE: string + * - DATETIME: string (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS) + * - DECIMAL: number (int or float) + * - BOOLEAN: boolean (true/false) + * - OPTION/RELATION (single): string (UUID) + * - OPTION/RELATION (multi, when is_multi=True): list of strings (UUIDs) or single string + * + * For multi-value properties (is_multi=True): + * - Accept either a single UUID string or a list of UUID strings + * - Multiple IssuePropertyValue records are created + * - Response will be a list of values + * + * For single-value properties: + * - Only one value is allowed per work item/property combination + */ export type UpdateWorkItemPropertyValue = { - values: [{ value: any }]; + value: string | boolean | number | string[]; + external_id?: string; + external_source?: string; }; export interface ListWorkItemPropertyValuesParams { diff --git a/tests/unit/work-item-types/properties-options.test.ts b/tests/unit/work-item-types/properties-options.test.ts index e3992e3..9fe524f 100644 --- a/tests/unit/work-item-types/properties-options.test.ts +++ b/tests/unit/work-item-types/properties-options.test.ts @@ -1,10 +1,10 @@ import { PlaneClient } from "../../../src/client/plane-client"; -import { WorkItemProperty, WorkItemPropertyOption } from "../../../src/models/WorkItemProperty"; +import { WorkItemProperty } from "../../../src/models/WorkItemProperty"; import { config } from "../constants"; import { createTestClient, randomizeName } from "../../helpers/test-utils"; -import { describeIf as describe } from "../../helpers/conditional-tests"; +import { describeIf } from "../../helpers/conditional-tests"; -describe( +describeIf( !!(config.workspaceSlug && config.projectId && config.workItemTypeId), "Work Item Type Properties and Options API Tests", () => { @@ -12,9 +12,6 @@ describe( let workspaceSlug: string; let projectId: string; let workItemTypeId: string; - let textProperty: WorkItemProperty; - let optionProperty: WorkItemProperty; - let propertyOption: WorkItemPropertyOption; beforeAll(async () => { client = createTestClient(); @@ -28,118 +25,232 @@ describe( }); }); - it("should test complete work item type properties and options workflow", async () => { - // ===== TEST TEXT PROPERTY ===== - // Create a TEXT property - const textPropertyName = randomizeName("Test WI Type Property"); - textProperty = await client.workItemProperties.create(workspaceSlug, projectId, workItemTypeId, { - name: textPropertyName, - display_name: textPropertyName, - property_type: "TEXT", - settings: { - display_format: "single-line", - }, - is_required: false, + describe("TEXT Property Tests", () => { + it("should create, retrieve, update, list, and delete a TEXT property", async () => { + // Create a TEXT property + const textPropertyName = randomizeName("Test WI Type Property"); + const textProperty = await client.workItemProperties.create(workspaceSlug, projectId, workItemTypeId, { + name: textPropertyName, + display_name: textPropertyName, + property_type: "TEXT", + settings: { + display_format: "single-line", + }, + is_required: false, + }); + + expect(textProperty).toBeDefined(); + expect(textProperty.id).toBeDefined(); + expect(textProperty.property_type).toBe("TEXT"); + + // Retrieve the property + const retrievedTextProperty = await client.workItemProperties.retrieve( + workspaceSlug, + projectId, + workItemTypeId, + textProperty.id! + ); + + expect(retrievedTextProperty).toBeDefined(); + expect(retrievedTextProperty.id).toBe(textProperty.id); + + // Update the property + const updatedTextPropertyName = randomizeName("Updated Test WI Type Property"); + const updatedTextProperty = await client.workItemProperties.update( + workspaceSlug, + projectId, + workItemTypeId, + textProperty.id!, + { + name: updatedTextPropertyName, + } + ); + + expect(updatedTextProperty).toBeDefined(); + expect(updatedTextProperty.id).toBe(textProperty.id); + + // List properties + const properties = await client.workItemProperties.list(workspaceSlug, projectId, workItemTypeId, { + limit: 10, + offset: 0, + }); + + expect(properties).toBeDefined(); + expect(Array.isArray(properties)).toBe(true); + const foundProperty = properties.find((p) => p.id === textProperty.id); + expect(foundProperty).toBeDefined(); + + // Delete the TEXT property + await client.workItemProperties.delete(workspaceSlug, projectId, workItemTypeId, textProperty.id!); }); + }); + + describe("OPTION Property Tests", () => { + let optionProperty: WorkItemProperty; - expect(textProperty).toBeDefined(); - expect(textProperty.id).toBeDefined(); - expect(textProperty.property_type).toBe("TEXT"); - - // Retrieve the property - const retrievedTextProperty = await client.workItemProperties.retrieve( - workspaceSlug, - projectId, - workItemTypeId, - textProperty.id! - ); - - expect(retrievedTextProperty).toBeDefined(); - expect(retrievedTextProperty.id).toBe(textProperty.id); - - // Update the property - const updatedTextPropertyName = randomizeName("Updated Test WI Type Property"); - const updatedTextProperty = await client.workItemProperties.update( - workspaceSlug, - projectId, - workItemTypeId, - textProperty.id!, + const defaultOptions = [ + { + name: "Backlog", + description: "Item is in the backlog", + is_default: true, + is_active: true, + }, { - name: updatedTextPropertyName, + name: "In Progress", + description: "Item is in progress", + is_active: true, + }, + { + name: "Done", + description: "Item is done", + is_active: true, + }, + ]; + + beforeEach(async () => { + // Create an OPTION property for testing + const optionPropertyName = randomizeName("Test Option Property"); + optionProperty = await client.workItemProperties.create(workspaceSlug, projectId, workItemTypeId, { + name: optionPropertyName, + display_name: optionPropertyName, + property_type: "OPTION", + is_required: false, + options: defaultOptions, + }); + }); + + afterEach(async () => { + // Clean up the OPTION property + if (optionProperty?.id) { + await client.workItemProperties.delete(workspaceSlug, projectId, workItemTypeId, optionProperty.id); } - ); + }); + + it("should create an OPTION property with options", async () => { + expect(optionProperty).toBeDefined(); + expect(optionProperty.id).toBeDefined(); + expect(optionProperty.property_type).toBe("OPTION"); + expect(optionProperty.options?.length).toBe(3); + expect(optionProperty.options?.[0].name).toBe("Backlog"); + expect(optionProperty.options?.[0].is_default).toBe(true); + expect(optionProperty.options?.[0].is_active).toBe(true); + expect(optionProperty.options?.[1].name).toBe("In Progress"); + expect(optionProperty.options?.[1].description).toBe("Item is in progress"); + expect(optionProperty.options?.[1].is_active).toBe(true); + }); - expect(updatedTextProperty).toBeDefined(); - expect(updatedTextProperty.id).toBe(textProperty.id); + it("should retrieve an OPTION property", async () => { + const retrievedOptionProperty = await client.workItemProperties.retrieve( + workspaceSlug, + projectId, + workItemTypeId, + optionProperty.id! + ); - // List properties - const properties = await client.workItemProperties.list(workspaceSlug, projectId, workItemTypeId, { - limit: 10, - offset: 0, + expect(retrievedOptionProperty).toBeDefined(); + expect(retrievedOptionProperty.id).toBe(optionProperty.id); + expect(retrievedOptionProperty.property_type).toBe("OPTION"); }); - expect(properties).toBeDefined(); - expect(Array.isArray(properties)).toBe(true); - const foundProperty = properties.find((p) => p.id === textProperty.id); - expect(foundProperty).toBeDefined(); - - // Delete the TEXT property - await client.workItemProperties.delete(workspaceSlug, projectId, workItemTypeId, textProperty.id!); - - // ===== TEST OPTION PROPERTY AND OPTIONS ===== - // Create an OPTION property - const optionPropertyName = randomizeName("Test Option Property"); - optionProperty = await client.workItemProperties.create(workspaceSlug, projectId, workItemTypeId, { - name: optionPropertyName, - display_name: optionPropertyName, - property_type: "OPTION", - is_required: false, + it("should update an OPTION property", async () => { + const updatedOptionPropertyName = randomizeName("Updated Option Property"); + const updatedOptionProperty = await client.workItemProperties.update( + workspaceSlug, + projectId, + workItemTypeId, + optionProperty.id!, + { + name: updatedOptionPropertyName, + } + ); + + expect(updatedOptionProperty).toBeDefined(); + expect(updatedOptionProperty.id).toBe(optionProperty.id); }); - expect(optionProperty).toBeDefined(); - expect(optionProperty.id).toBeDefined(); - expect(optionProperty.property_type).toBe("OPTION"); + it("should list OPTION properties", async () => { + const properties = await client.workItemProperties.list(workspaceSlug, projectId, workItemTypeId, { + limit: 10, + offset: 0, + }); - // Create a property option - const optionName = randomizeName("Test Property Option"); - propertyOption = await client.workItemProperties.options.create(workspaceSlug, projectId, optionProperty.id!, { - name: optionName, + expect(properties).toBeDefined(); + expect(Array.isArray(properties)).toBe(true); + const foundProperty = properties.find((p) => p.id === optionProperty.id); + expect(foundProperty).toBeDefined(); }); + }); - expect(propertyOption).toBeDefined(); - expect(propertyOption.id).toBeDefined(); - - // Retrieve the property option - const retrievedOption = await client.workItemProperties.options.retrieve( - workspaceSlug, - projectId, - optionProperty.id!, - propertyOption.id! - ); - - expect(retrievedOption).toBeDefined(); - expect(retrievedOption.id).toBe(propertyOption.id); - - // Update the property option - const updatedOptionName = randomizeName("Updated Property Option"); - const updatedOption = await client.workItemProperties.options.update( - workspaceSlug, - projectId, - optionProperty.id!, - propertyOption.id!, - { - name: updatedOptionName, + describe("Property Options Tests", () => { + let optionProperty: WorkItemProperty; + + beforeEach(async () => { + // Create an OPTION property for testing options + const optionPropertyName = randomizeName("Test Option Property for Options"); + optionProperty = await client.workItemProperties.create(workspaceSlug, projectId, workItemTypeId, { + name: optionPropertyName, + display_name: optionPropertyName, + property_type: "OPTION", + is_required: false, + }); + }); + + afterEach(async () => { + // Clean up the OPTION property + if (optionProperty?.id) { + await client.workItemProperties.delete(workspaceSlug, projectId, workItemTypeId, optionProperty.id); } - ); + }); + + it("should create, retrieve, update, and delete a property option", async () => { + // Create a property option + const optionName = randomizeName("Test Property Option"); + const propertyOption = await client.workItemProperties.options.create( + workspaceSlug, + projectId, + optionProperty.id!, + { + name: optionName, + } + ); - expect(updatedOption).toBeDefined(); - expect(updatedOption.id).toBe(propertyOption.id); + expect(propertyOption).toBeDefined(); + expect(propertyOption.id).toBeDefined(); - // Delete the property option - await client.workItemProperties.options.delete(workspaceSlug, projectId, optionProperty.id!, propertyOption.id!); + // Retrieve the property option + const retrievedOption = await client.workItemProperties.options.retrieve( + workspaceSlug, + projectId, + optionProperty.id!, + propertyOption.id! + ); - // Delete the OPTION property - await client.workItemProperties.delete(workspaceSlug, projectId, workItemTypeId, optionProperty.id!); + expect(retrievedOption).toBeDefined(); + expect(retrievedOption.id).toBe(propertyOption.id); + + // Update the property option + const updatedOptionName = randomizeName("Updated Property Option"); + const updatedOption = await client.workItemProperties.options.update( + workspaceSlug, + projectId, + optionProperty.id!, + propertyOption.id!, + { + name: updatedOptionName, + } + ); + + expect(updatedOption).toBeDefined(); + expect(updatedOption.id).toBe(propertyOption.id); + + // Delete the property option + await client.workItemProperties.options.delete( + workspaceSlug, + projectId, + optionProperty.id!, + propertyOption.id! + ); + }); }); } ); diff --git a/tests/unit/work-item-types/properties-values.test.ts b/tests/unit/work-item-types/properties-values.test.ts index b5f4e1a..2ff2175 100644 --- a/tests/unit/work-item-types/properties-values.test.ts +++ b/tests/unit/work-item-types/properties-values.test.ts @@ -35,7 +35,7 @@ describe( workItemId, propertyId, { - values: [{ value: testValue }], + value: testValue, } ); From 4d5e1dc22e6dfa72273e0adbeca298413a05f1f6 Mon Sep 17 00:00:00 2001 From: Surya Prashanth Date: Sat, 29 Nov 2025 15:23:01 +0530 Subject: [PATCH 2/2] fix: tests with new project features update api --- package.json | 2 +- tests/e2e/project.test.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 9c59f2c..d268699 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makeplane/plane-node-sdk", - "version": "0.2.2", + "version": "0.2.3", "description": "Node SDK for Plane", "author": "Plane ", "repository": { diff --git a/tests/e2e/project.test.ts b/tests/e2e/project.test.ts index b4c5119..e646acc 100644 --- a/tests/e2e/project.test.ts +++ b/tests/e2e/project.test.ts @@ -27,6 +27,11 @@ describe("End to End Project Test", () => { name: projectName, id: projectName.slice(0, 5).toUpperCase(), }); + + await client.projects.updateFeatures(e2eConfig.workspaceSlug, project.id, { + cycles: true, + modules: true, + }); }); it("should create and list cycles", async () => {