From c21fce666ee89155beb7f410ce0893b4a8fcb0c0 Mon Sep 17 00:00:00 2001 From: Liam Egan Date: Tue, 30 Jun 2026 13:03:19 -0700 Subject: [PATCH 1/3] Allow task with subtask as an option for pinned task lists Problem: current implementation explicitly expects pinned task lists to be task lists (Teamwork API explicitly differentiates between task lists and tasks with subtasks, and the current implementation does not account for this). Solution: update the implementation to allow pinned task lists to be either task lists or tasks with subtasks. --- src/api/config/templates.ts | 2 +- src/api/teamwork/task-list-tasks.ts | 61 ++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/api/config/templates.ts b/src/api/config/templates.ts index a1496f2..24c751c 100644 --- a/src/api/config/templates.ts +++ b/src/api/config/templates.ts @@ -63,7 +63,7 @@ export function formatProjectConfig(config: ProjectConfig): string { (taskList) => ` - name: ${JSON.stringify(taskList.name)}\n id: ${taskList.id}\n`, ) .join("")}` - : ` pinnedTaskLists: []\n # - name: General Tasks\n # id: 1597639\n`; + : ` pinnedTaskLists: []\n # id can be a task list, or a task with subtasks\n # - name: General Tasks\n # id: 1597639\n`; return `# WTC project-level configuration. diff --git a/src/api/teamwork/task-list-tasks.ts b/src/api/teamwork/task-list-tasks.ts index 5a52af1..5aa5c3c 100644 --- a/src/api/teamwork/task-list-tasks.ts +++ b/src/api/teamwork/task-list-tasks.ts @@ -144,12 +144,12 @@ function parseTeamworkDueDate(dueDate: TeamworkDateValue | null | undefined): st return trimmed; } -/** Fetches tasks for a Teamwork task list and resolves workflow stage names, assignees, due dates, and priority. */ -export async function getTeamworkTaskListTasks(taskListId: number): Promise { - const parsed = TeamworkTaskListTasksResponseSchema.parse( - await fetchTeamworkApiJson(`/tasklists/${taskListId}/tasks.json?include=users,assigneeUsers`), - ); +type TeamworkTaskListTasksResponse = z.infer; +/** Normalizes a Teamwork tasks response, resolving workflow stage names, assignees, due dates, and priority. */ +async function normalizeTeamworkTasks( + parsed: TeamworkTaskListTasksResponse, +): Promise { const workflowIds = [ ...new Set( parsed.tasks.flatMap( @@ -205,6 +205,55 @@ export async function getTeamworkTaskListTasks(taskListId: number): Promise { + const parsed = TeamworkTaskListTasksResponseSchema.parse( + await fetchTeamworkApiJson(`/tasklists/${taskListId}/tasks.json?include=users,assigneeUsers`), + ); + return normalizeTeamworkTasks(parsed); +} + +/** Fetches the subtasks of a single Teamwork task, normalized the same way as task-list tasks. */ +export async function getTeamworkSubtasks(taskId: number): Promise { + const parsed = TeamworkTaskListTasksResponseSchema.parse( + await fetchTeamworkApiJson(`/tasks/${taskId}/subtasks.json?include=users,assigneeUsers`), + ); + return normalizeTeamworkTasks(parsed); +} + +/** + * Fetches tasks for a pinned ID that may be either a task list or a single task with subtasks. + * + * Tries the task-list endpoint first. When the ID is actually a task, that endpoint returns no + * tasks (or 404s), so we fall back to the task's subtasks. A genuinely empty task list still + * resolves to an empty array rather than surfacing the subtask lookup failure. + */ +export async function getTeamworkTaskListOrSubtasks(id: number): Promise { + let taskListTasks: TeamworkTask[]; + try { + taskListTasks = await getTeamworkTaskListTasks(id); + } catch (taskListError) { + try { + const subtasks = await getTeamworkSubtasks(id); + if (subtasks.length > 0) return subtasks; + } catch { + // Ignore the subtask failure and surface the original task-list error. + } + throw taskListError; + } + + if (taskListTasks.length > 0) return taskListTasks; + + try { + const subtasks = await getTeamworkSubtasks(id); + if (subtasks.length > 0) return subtasks; + } catch { + // The ID is a genuinely empty task list, not a task with subtasks. + } + + return taskListTasks; +} + /** A pinned task list with its fetched tasks (or an error message). */ export interface PinnedTaskListFetchResult { id: number; @@ -227,7 +276,7 @@ export async function getPinnedTaskListTasks( results.push({ id: taskList.id, name: taskList.name, - tasks: await getTeamworkTaskListTasks(taskList.id), + tasks: await getTeamworkTaskListOrSubtasks(taskList.id), error: null, }); } catch (error) { From ff9f0539c193370fea14d371405997febf0aa26e Mon Sep 17 00:00:00 2001 From: Liam Egan Date: Tue, 30 Jun 2026 13:03:19 -0700 Subject: [PATCH 2/3] Allow task with subtask as an option for pinned task lists Problem: current implementation explicitly expects pinned task lists to be task lists (Teamwork API explicitly differentiates between task lists and tasks with subtasks, and the current implementation does not account for this). Solution: update the implementation to allow pinned task lists to be either task lists or tasks with subtasks. - Resolving a direct conflict where both branches rewrote the getTeamworkTaskListTasks function. Main updated the function to try/catch and error on failure, while this update pulls the logic out into a shared normalizeTeamworkTasks function. --- src/api/config/templates.ts | 2 +- src/api/teamwork/task-list-tasks.ts | 85 ++++++++++++++++++++++++----- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/src/api/config/templates.ts b/src/api/config/templates.ts index 50562e9..ee2025a 100644 --- a/src/api/config/templates.ts +++ b/src/api/config/templates.ts @@ -57,7 +57,7 @@ export function formatProjectConfig(config: ProjectConfig): string { (taskList) => ` - name: ${JSON.stringify(taskList.name)}\n id: ${taskList.id}\n`, ) .join("")}` - : ` pinnedTaskLists: []\n # - name: General Tasks\n # id: 1597639\n`; + : ` pinnedTaskLists: []\n # id can be a task list, or a task with subtasks\n # - name: General Tasks\n # id: 1597639\n`; return `# WTC project-level configuration. diff --git a/src/api/teamwork/task-list-tasks.ts b/src/api/teamwork/task-list-tasks.ts index c39b63d..c956f0f 100644 --- a/src/api/teamwork/task-list-tasks.ts +++ b/src/api/teamwork/task-list-tasks.ts @@ -145,20 +145,12 @@ function parseTeamworkDueDate(dueDate: TeamworkDateValue | null | undefined): st return trimmed; } -/** Fetches tasks for a Teamwork task list and resolves workflow stage names, assignees, due dates, and priority. */ -export async function getTeamworkTaskListTasks(taskListId: number): Promise { - let parsed: z.infer; - try { - parsed = TeamworkTaskListTasksResponseSchema.parse( - await fetchTeamworkApiJson(`/tasklists/${taskListId}/tasks.json?include=users,assigneeUsers`), - ); - } catch (error) { - logWarn("teamwork", "taskList.get.error", `Failed to fetch task list ${taskListId}`, { - error: error instanceof Error ? error.message : String(error), - }); - throw error; - } +type TeamworkTaskListTasksResponse = z.infer; +/** Normalizes a Teamwork tasks response, resolving workflow stage names, assignees, due dates, and priority. */ +async function normalizeTeamworkTasks( + parsed: TeamworkTaskListTasksResponse, +): Promise { const workflowIds = [ ...new Set( parsed.tasks.flatMap( @@ -225,6 +217,71 @@ export async function getTeamworkTaskListTasks(taskListId: number): Promise { + let parsed: TeamworkTaskListTasksResponse; + try { + parsed = TeamworkTaskListTasksResponseSchema.parse( + await fetchTeamworkApiJson(`/tasklists/${taskListId}/tasks.json?include=users,assigneeUsers`), + ); + } catch (error) { + logWarn("teamwork", "taskList.get.error", `Failed to fetch task list ${taskListId}`, { + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + return normalizeTeamworkTasks(parsed); +} + +/** Fetches the subtasks of a single Teamwork task, normalized the same way as task-list tasks. */ +export async function getTeamworkSubtasks(taskId: number): Promise { + let parsed: TeamworkTaskListTasksResponse; + try { + parsed = TeamworkTaskListTasksResponseSchema.parse( + await fetchTeamworkApiJson(`/tasks/${taskId}/subtasks.json?include=users,assigneeUsers`), + ); + } catch (error) { + logWarn("teamwork", "task.subtasks.error", `Failed to fetch subtasks for task ${taskId}`, { + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + return normalizeTeamworkTasks(parsed); +} + +/** + * Fetches tasks for a pinned ID that may be either a task list or a single task with subtasks. + * + * Tries the task-list endpoint first. When the ID is actually a task, that endpoint returns no + * tasks (or 404s), so we fall back to the task's subtasks. A genuinely empty task list still + * resolves to an empty array rather than surfacing the subtask lookup failure. + */ +export async function getTeamworkTaskListOrSubtasks(id: number): Promise { + let taskListTasks: TeamworkTask[]; + try { + taskListTasks = await getTeamworkTaskListTasks(id); + } catch (taskListError) { + try { + const subtasks = await getTeamworkSubtasks(id); + if (subtasks.length > 0) return subtasks; + } catch { + // Ignore the subtask failure and surface the original task-list error. + } + throw taskListError; + } + + if (taskListTasks.length > 0) return taskListTasks; + + try { + const subtasks = await getTeamworkSubtasks(id); + if (subtasks.length > 0) return subtasks; + } catch { + // The ID is a genuinely empty task list, not a task with subtasks. + } + + return taskListTasks; +} + /** A pinned task list with its fetched tasks (or an error message). */ export interface PinnedTaskListFetchResult { id: number; @@ -247,7 +304,7 @@ export async function getPinnedTaskListTasks( results.push({ id: taskList.id, name: taskList.name, - tasks: await getTeamworkTaskListTasks(taskList.id), + tasks: await getTeamworkTaskListOrSubtasks(taskList.id), error: null, }); } catch (error) { From c45ecd1e3a2d32b8b108881aa5a2e19b81d4e258 Mon Sep 17 00:00:00 2001 From: Liam Egan Date: Tue, 30 Jun 2026 13:43:10 -0700 Subject: [PATCH 3/3] Removing testing wtc yaml --- .wtc.yaml | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 .wtc.yaml diff --git a/.wtc.yaml b/.wtc.yaml deleted file mode 100644 index 19e1243..0000000 --- a/.wtc.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# WTC project-level configuration. - -# Config file format version. Do not edit unless a migration guide tells you to. -version: 1 - -project: - # Important links for this project, such as Figma, staging, docs, or dashboards. - links: - - name: "Figma" - url: "https://www.figma.com/design/RlYt8gCvmm7Z7vTiSGRZHV" - -teamwork: - # Teamwork project ID linked to this repository. - # Leave empty until this repo is linked to Teamwork. - projectId: 393531 - - # Teamwork task ID for code review tracking. - # Set via the PR creation flow; leave empty to skip. - reviewTaskId: - - # Project-level task lists to surface in the Teamwork Project tab. - pinnedTaskLists: - - name: "Dev tasks" - id: 26759356 - - name: "General tasks" - id: 1692303