From 059bbe956a303b56e60705589954f894f9e4b5b4 Mon Sep 17 00:00:00 2001 From: Stephan Schellworth Date: Thu, 4 Jun 2026 09:24:04 +0200 Subject: [PATCH] fix(flow-editor): encode connection id in browse and ClickUp API paths URL-encode connection references with spaces/colons so ClickUp list browse resolves correctly. Surface browse error field in formatApiError. Co-authored-by: Cursor --- src/api/workflowApi.connectionPath.test.ts | 17 +++++++++++++++++ src/api/workflowApi.ts | 19 ++++++++++++------- src/hooks/useApi.ts | 1 + 3 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 src/api/workflowApi.connectionPath.test.ts diff --git a/src/api/workflowApi.connectionPath.test.ts b/src/api/workflowApi.connectionPath.test.ts new file mode 100644 index 0000000..90259cf --- /dev/null +++ b/src/api/workflowApi.connectionPath.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; + +/** Mirrors _encodedConnectionId in workflowApi.ts for browse/services URL paths. */ +function encodedConnectionId(connectionId: string): string { + return encodeURIComponent(connectionId); +} + +describe('connection path encoding for workflow browse', () => { + it('encodes spaces and colons in connection:clickup:username references', () => { + const ref = 'connection:clickup:Stephan Schellworth'; + const segment = encodedConnectionId(ref); + expect(segment).toBe('connection%3Aclickup%3AStephan%20Schellworth'); + const url = `/api/workflows/inst/connections/${segment}/browse`; + expect(url).not.toContain(' '); + expect(url).toContain('%20'); + }); +}); diff --git a/src/api/workflowApi.ts b/src/api/workflowApi.ts index c4144d4..8954c44 100644 --- a/src/api/workflowApi.ts +++ b/src/api/workflowApi.ts @@ -943,13 +943,18 @@ export interface ConnectionService { icon: string; } +/** Encode connection id/reference for URL path segments (may contain spaces/colons). */ +function _encodedConnectionId(connectionId: string): string { + return encodeURIComponent(connectionId); +} + export async function fetchConnectionServices( request: ApiRequestFunction, instanceId: string, connectionId: string ): Promise { const data = await request({ - url: `/api/workflows/${instanceId}/connections/${connectionId}/services`, + url: `/api/workflows/${instanceId}/connections/${_encodedConnectionId(connectionId)}/services`, method: 'get', }); return data?.services ?? []; @@ -972,7 +977,7 @@ export async function fetchBrowse( path = '/' ): Promise<{ items: BrowseEntry[]; path: string; service: string }> { const data = await request({ - url: `/api/workflows/${instanceId}/connections/${connectionId}/browse`, + url: `/api/workflows/${instanceId}/connections/${_encodedConnectionId(connectionId)}/browse`, method: 'get', params: { service, path }, }); @@ -986,7 +991,7 @@ export async function fetchClickupTask( taskId: string ): Promise> { const data = await request({ - url: `/api/clickup/${connectionId}/tasks/${encodeURIComponent(taskId)}`, + url: `/api/clickup/${_encodedConnectionId(connectionId)}/tasks/${encodeURIComponent(taskId)}`, method: 'get', }); return data && typeof data === 'object' ? (data as Record) : {}; @@ -999,7 +1004,7 @@ export async function fetchClickupList( listId: string ): Promise> { const data = await request({ - url: `/api/clickup/${connectionId}/lists/${listId}`, + url: `/api/clickup/${_encodedConnectionId(connectionId)}/lists/${encodeURIComponent(listId)}`, method: 'get', }); return data && typeof data === 'object' ? (data as Record) : {}; @@ -1012,7 +1017,7 @@ export async function fetchClickupTeam( teamId: string ): Promise> { const data = await request({ - url: `/api/clickup/${connectionId}/teams/${teamId}`, + url: `/api/clickup/${_encodedConnectionId(connectionId)}/teams/${encodeURIComponent(teamId)}`, method: 'get', }); return data && typeof data === 'object' ? (data as Record) : {}; @@ -1025,7 +1030,7 @@ export async function fetchClickupListFields( listId: string ): Promise<{ fields?: unknown[] } & Record> { const data = await request({ - url: `/api/clickup/${connectionId}/lists/${listId}/fields`, + url: `/api/clickup/${_encodedConnectionId(connectionId)}/lists/${encodeURIComponent(listId)}/fields`, method: 'get', }); return (data && typeof data === 'object' ? data : {}) as { fields?: unknown[] } & Record; @@ -1046,7 +1051,7 @@ export async function fetchClickupListTasks( { tasks?: ClickupListTaskItem[]; last_page?: boolean } & Record > { const data = await request({ - url: `/api/clickup/${connectionId}/lists/${listId}/tasks`, + url: `/api/clickup/${_encodedConnectionId(connectionId)}/lists/${encodeURIComponent(listId)}/tasks`, method: 'get', params: { page: options?.page ?? 0, diff --git a/src/hooks/useApi.ts b/src/hooks/useApi.ts index 724faac..0742005 100644 --- a/src/hooks/useApi.ts +++ b/src/hooks/useApi.ts @@ -36,6 +36,7 @@ export function formatApiError(error: any, defaultMessage: string): string { // Handle other error formats if (typeof data?.detail === 'string') return data.detail; if (typeof data?.message === 'string') return data.message; + if (typeof data?.error === 'string') return data.error; if (typeof data === 'string') return data; return defaultMessage;