From 26b5f2ce254c91c1e97085664d35973b075a4413 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 7 Aug 2025 09:26:33 +0000 Subject: [PATCH] fix(site): fix render crash when no embedded apps are defined for task --- site/src/pages/TaskPage/TaskApps.stories.tsx | 134 ++++++++++++++++ site/src/pages/TaskPage/TaskApps.tsx | 158 +++++++++++-------- 2 files changed, 230 insertions(+), 62 deletions(-) create mode 100644 site/src/pages/TaskPage/TaskApps.stories.tsx diff --git a/site/src/pages/TaskPage/TaskApps.stories.tsx b/site/src/pages/TaskPage/TaskApps.stories.tsx new file mode 100644 index 0000000000000..c3006e2db0a14 --- /dev/null +++ b/site/src/pages/TaskPage/TaskApps.stories.tsx @@ -0,0 +1,134 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import type { WorkspaceApp } from "api/typesGenerated"; +import { + MockTasks, + MockWorkspace, + MockWorkspaceAgent, + MockWorkspaceApp, +} from "testHelpers/entities"; +import { withProxyProvider } from "testHelpers/storybook"; +import { TaskApps } from "./TaskApps"; + +const meta: Meta = { + title: "pages/TaskPage/TaskApps", + component: TaskApps, + decorators: [withProxyProvider()], + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +const mockAgentNoApps = { + ...MockWorkspaceAgent, + apps: [], +}; + +const mockExternalApp: WorkspaceApp = { + ...MockWorkspaceApp, + external: true, +}; + +const mockEmbeddedApp: WorkspaceApp = { + ...MockWorkspaceApp, + external: false, +}; + +const taskWithNoApps = { + ...MockTasks[0], + workspace: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { + ...MockWorkspace.latest_build.resources[0], + agents: [mockAgentNoApps], + }, + ], + }, + }, +}; + +export const NoEmbeddedApps: Story = { + args: { + task: taskWithNoApps, + }, +}; + +export const WithExternalAppsOnly: Story = { + args: { + task: { + ...MockTasks[0], + workspace: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { + ...MockWorkspace.latest_build.resources[0], + agents: [ + { + ...MockWorkspaceAgent, + apps: [mockExternalApp], + }, + ], + }, + ], + }, + }, + }, + }, +}; + +export const WithEmbeddedApps: Story = { + args: { + task: { + ...MockTasks[0], + workspace: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { + ...MockWorkspace.latest_build.resources[0], + agents: [ + { + ...MockWorkspaceAgent, + apps: [mockEmbeddedApp], + }, + ], + }, + ], + }, + }, + }, + }, +}; + +export const WithMixedApps: Story = { + args: { + task: { + ...MockTasks[0], + workspace: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { + ...MockWorkspace.latest_build.resources[0], + agents: [ + { + ...MockWorkspaceAgent, + apps: [mockEmbeddedApp, mockExternalApp], + }, + ], + }, + ], + }, + }, + }, + }, +}; diff --git a/site/src/pages/TaskPage/TaskApps.tsx b/site/src/pages/TaskPage/TaskApps.tsx index 0cccc8c7a01df..1fd31bd3b1481 100644 --- a/site/src/pages/TaskPage/TaskApps.tsx +++ b/site/src/pages/TaskPage/TaskApps.tsx @@ -1,4 +1,4 @@ -import type { WorkspaceApp } from "api/typesGenerated"; +import type { WorkspaceAgent, WorkspaceApp } from "api/typesGenerated"; import { Button } from "components/Button/Button"; import { DropdownMenu, @@ -8,6 +8,7 @@ import { } from "components/DropdownMenu/DropdownMenu"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { InfoTooltip } from "components/InfoTooltip/InfoTooltip"; +import { Link } from "components/Link/Link"; import { ChevronDownIcon, LayoutGridIcon } from "lucide-react"; import { useAppLink } from "modules/apps/useAppLink"; import type { Task } from "modules/tasks/tasks"; @@ -15,6 +16,7 @@ import type React from "react"; import { type FC, useState } from "react"; import { Link as RouterLink } from "react-router-dom"; import { cn } from "utils/cn"; +import { docs } from "utils/docs"; import { TaskAppIFrame } from "./TaskAppIframe"; type TaskAppsProps = { @@ -37,25 +39,9 @@ export const TaskApps: FC = ({ task }) => { const embeddedApps = apps.filter((app) => !app.external); const externalApps = apps.filter((app) => app.external); - const [activeAppId, setActiveAppId] = useState(() => { - const appId = embeddedApps[0]?.id; - if (!appId) { - throw new Error("No apps found in task"); - } - return appId; - }); - - const activeApp = apps.find((app) => app.id === activeAppId); - if (!activeApp) { - throw new Error(`Active app with ID ${activeAppId} not found in task`); - } - - const agent = agents.find((a) => - a.apps.some((app) => app.id === activeAppId), + const [activeAppId, setActiveAppId] = useState( + embeddedApps[0]?.id, ); - if (!agent) { - throw new Error(`Agent for app ${activeAppId} not found in task workspace`); - } return (
@@ -76,56 +62,104 @@ export const TaskApps: FC = ({ task }) => { {externalApps.length > 0 && ( -
- - - - - - {externalApps.map((app) => { - const link = useAppLink(app, { - agent, - workspace: task.workspace, - }); - - return ( - - - {app.icon ? ( - - ) : ( - - )} - {link.label} - - - ); - })} - - -
+ )} -
- {embeddedApps.map((app) => { - return ( - - ); - })} -
+ {embeddedApps.length > 0 ? ( +
+ {embeddedApps.map((app) => { + return ( + + ); + })} +
+ ) : ( +
+

+ No embedded apps found. +

+ + + + Learn how to configure apps + {" "} + for your tasks. + +
+ )}
); }; +type TaskExternalAppsDropdownProps = { + task: Task; + agents: WorkspaceAgent[]; + externalApps: WorkspaceApp[]; +}; + +const TaskExternalAppsDropdown: FC = ({ + task, + agents, + externalApps, +}) => { + return ( +
+ + + + + + {externalApps.map((app) => { + const agent = agents.find((agent) => + agent.apps.some((a) => a.id === app.id), + ); + if (!agent) { + throw new Error( + `Agent for app ${app.id} not found in task workspace`, + ); + } + + const link = useAppLink(app, { + agent, + workspace: task.workspace, + }); + + return ( + + + {app.icon ? ( + + ) : ( + + )} + {link.label} + + + ); + })} + + +
+ ); +}; + type TaskAppTabProps = { task: Task; app: WorkspaceApp; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy