fix(flow-editor): encode connection id in browse and ClickUp API paths
All checks were successful
Deploy Nyla Frontend to Integration / deploy (push) Successful in 1m17s

URL-encode connection references with spaces/colons so ClickUp list browse resolves correctly. Surface browse error field in formatApiError.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Stephan Schellworth 2026-06-04 09:24:04 +02:00
parent b36b303def
commit 059bbe956a
3 changed files with 30 additions and 7 deletions

View file

@ -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');
});
});

View file

@ -943,13 +943,18 @@ export interface ConnectionService {
icon: string; 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( export async function fetchConnectionServices(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string, instanceId: string,
connectionId: string connectionId: string
): Promise<ConnectionService[]> { ): Promise<ConnectionService[]> {
const data = await request({ const data = await request({
url: `/api/workflows/${instanceId}/connections/${connectionId}/services`, url: `/api/workflows/${instanceId}/connections/${_encodedConnectionId(connectionId)}/services`,
method: 'get', method: 'get',
}); });
return data?.services ?? []; return data?.services ?? [];
@ -972,7 +977,7 @@ export async function fetchBrowse(
path = '/' path = '/'
): Promise<{ items: BrowseEntry[]; path: string; service: string }> { ): Promise<{ items: BrowseEntry[]; path: string; service: string }> {
const data = await request({ const data = await request({
url: `/api/workflows/${instanceId}/connections/${connectionId}/browse`, url: `/api/workflows/${instanceId}/connections/${_encodedConnectionId(connectionId)}/browse`,
method: 'get', method: 'get',
params: { service, path }, params: { service, path },
}); });
@ -986,7 +991,7 @@ export async function fetchClickupTask(
taskId: string taskId: string
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
const data = await request({ const data = await request({
url: `/api/clickup/${connectionId}/tasks/${encodeURIComponent(taskId)}`, url: `/api/clickup/${_encodedConnectionId(connectionId)}/tasks/${encodeURIComponent(taskId)}`,
method: 'get', method: 'get',
}); });
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {}; return data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
@ -999,7 +1004,7 @@ export async function fetchClickupList(
listId: string listId: string
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
const data = await request({ const data = await request({
url: `/api/clickup/${connectionId}/lists/${listId}`, url: `/api/clickup/${_encodedConnectionId(connectionId)}/lists/${encodeURIComponent(listId)}`,
method: 'get', method: 'get',
}); });
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {}; return data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
@ -1012,7 +1017,7 @@ export async function fetchClickupTeam(
teamId: string teamId: string
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
const data = await request({ const data = await request({
url: `/api/clickup/${connectionId}/teams/${teamId}`, url: `/api/clickup/${_encodedConnectionId(connectionId)}/teams/${encodeURIComponent(teamId)}`,
method: 'get', method: 'get',
}); });
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {}; return data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
@ -1025,7 +1030,7 @@ export async function fetchClickupListFields(
listId: string listId: string
): Promise<{ fields?: unknown[] } & Record<string, unknown>> { ): Promise<{ fields?: unknown[] } & Record<string, unknown>> {
const data = await request({ const data = await request({
url: `/api/clickup/${connectionId}/lists/${listId}/fields`, url: `/api/clickup/${_encodedConnectionId(connectionId)}/lists/${encodeURIComponent(listId)}/fields`,
method: 'get', method: 'get',
}); });
return (data && typeof data === 'object' ? data : {}) as { fields?: unknown[] } & Record<string, unknown>; return (data && typeof data === 'object' ? data : {}) as { fields?: unknown[] } & Record<string, unknown>;
@ -1046,7 +1051,7 @@ export async function fetchClickupListTasks(
{ tasks?: ClickupListTaskItem[]; last_page?: boolean } & Record<string, unknown> { tasks?: ClickupListTaskItem[]; last_page?: boolean } & Record<string, unknown>
> { > {
const data = await request({ const data = await request({
url: `/api/clickup/${connectionId}/lists/${listId}/tasks`, url: `/api/clickup/${_encodedConnectionId(connectionId)}/lists/${encodeURIComponent(listId)}/tasks`,
method: 'get', method: 'get',
params: { params: {
page: options?.page ?? 0, page: options?.page ?? 0,

View file

@ -36,6 +36,7 @@ export function formatApiError(error: any, defaultMessage: string): string {
// Handle other error formats // Handle other error formats
if (typeof data?.detail === 'string') return data.detail; if (typeof data?.detail === 'string') return data.detail;
if (typeof data?.message === 'string') return data.message; if (typeof data?.message === 'string') return data.message;
if (typeof data?.error === 'string') return data.error;
if (typeof data === 'string') return data; if (typeof data === 'string') return data;
return defaultMessage; return defaultMessage;