@agent-native/dispatch 0.29.1 → 0.29.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ import {
7
7
  IconArrowUpRight,
8
8
  IconClockHour4,
9
9
  } from "@tabler/icons-react";
10
- import { useEffect, useMemo } from "react";
10
+ import { useEffect, useMemo, useState } from "react";
11
11
  import {
12
12
  Link,
13
13
  Navigate,
@@ -24,6 +24,7 @@ import { Button } from "../../components/ui/button";
24
24
  import { Spinner } from "../../components/ui/spinner";
25
25
  import { resolveServerCatchAllTarget } from "../../lib/catch-all-target";
26
26
  import {
27
+ navigateToWorkspaceApp,
27
28
  workspaceAppHref,
28
29
  type WorkspaceAppSummary,
29
30
  } from "../../lib/workspace-apps";
@@ -111,12 +112,18 @@ export default function WorkspaceAppCatchAllRoute() {
111
112
  );
112
113
  const href = app ? workspaceAppHref(app) : null;
113
114
  const isSelfReference = appId === "dispatch";
115
+ const hasApp = app !== null;
116
+ const appIsPending = app?.status === "pending";
117
+ const [navigationFailed, setNavigationFailed] = useState(false);
114
118
 
115
119
  useEffect(() => {
116
120
  if (isSelfReference) return;
117
- if (!app || app.status === "pending" || !href) return;
118
- window.location.assign(href);
119
- }, [app, href, isSelfReference]);
121
+ if (!hasApp || appIsPending || !href) {
122
+ setNavigationFailed(false);
123
+ return;
124
+ }
125
+ setNavigationFailed(!navigateToWorkspaceApp(href));
126
+ }, [appIsPending, hasApp, href, isSelfReference]);
120
127
 
121
128
  if (isSelfReference) {
122
129
  return <Navigate to={appPath("/overview")} replace />;
@@ -136,7 +143,23 @@ export default function WorkspaceAppCatchAllRoute() {
136
143
  );
137
144
  }
138
145
 
139
- if ((isLoading && !app) || (app && app.status !== "pending" && href)) {
146
+ if (navigationFailed && href) {
147
+ return (
148
+ <DispatchShell
149
+ title={t("dispatch.pages.dataLoadFailed")}
150
+ description={t("dispatch.pages.pageNotFoundDescription")}
151
+ >
152
+ <ActionQueryError
153
+ onRetry={() => setNavigationFailed(!navigateToWorkspaceApp(href))}
154
+ />
155
+ </DispatchShell>
156
+ );
157
+ }
158
+
159
+ if (
160
+ (isLoading && !app) ||
161
+ (app && app.status !== "pending" && href && !navigationFailed)
162
+ ) {
140
163
  return (
141
164
  <div className="flex h-screen w-full items-center justify-center">
142
165
  <Spinner className="size-8" />
@@ -0,0 +1,66 @@
1
+ // @vitest-environment happy-dom
2
+ import React, { act } from "react";
3
+ import { createRoot, type Root } from "react-dom/client";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const routeState = vi.hoisted(() => ({
7
+ appId: "mail",
8
+ hostProps: null as {
9
+ appId?: string;
10
+ navigateToTopWindow?: (href: string) => boolean | void;
11
+ } | null,
12
+ navigateToWorkspaceApp: vi.fn(() => true),
13
+ }));
14
+
15
+ vi.mock("react-router", () => ({
16
+ useParams: () => ({ appId: routeState.appId }),
17
+ }));
18
+
19
+ vi.mock("../../components/workspace-app-host", () => ({
20
+ WorkspaceAppHost: (props: typeof routeState.hostProps) => {
21
+ routeState.hostProps = props;
22
+ return <div data-workspace-app-id={props?.appId} />;
23
+ },
24
+ }));
25
+
26
+ vi.mock("../../lib/workspace-apps", () => ({
27
+ navigateToWorkspaceApp: routeState.navigateToWorkspaceApp,
28
+ }));
29
+
30
+ import WorkspaceAppRoute from "./apps.$appId";
31
+
32
+ describe("WorkspaceAppRoute", () => {
33
+ let container: HTMLDivElement;
34
+ let root: Root;
35
+
36
+ beforeEach(() => {
37
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
38
+ routeState.appId = "mail";
39
+ routeState.hostProps = null;
40
+ routeState.navigateToWorkspaceApp.mockClear();
41
+ container = document.createElement("div");
42
+ document.body.appendChild(container);
43
+ root = createRoot(container);
44
+ });
45
+
46
+ afterEach(() => {
47
+ act(() => root.unmount());
48
+ container.remove();
49
+ vi.unstubAllGlobals();
50
+ });
51
+
52
+ it("passes the route app id and top-window helper to the workspace host", async () => {
53
+ await act(async () => {
54
+ root.render(<WorkspaceAppRoute />);
55
+ });
56
+
57
+ expect(routeState.hostProps?.appId).toBe("mail");
58
+ expect(routeState.hostProps?.navigateToTopWindow).toEqual(
59
+ expect.any(Function),
60
+ );
61
+
62
+ routeState.hostProps?.navigateToTopWindow?.("/mail");
63
+
64
+ expect(routeState.navigateToWorkspaceApp).toHaveBeenCalledWith("/mail");
65
+ });
66
+ });
@@ -1,6 +1,11 @@
1
1
  import { useParams } from "react-router";
2
2
 
3
3
  import { WorkspaceAppHost } from "../../components/workspace-app-host";
4
+ import { navigateToWorkspaceApp } from "../../lib/workspace-apps";
5
+
6
+ export function navigateWorkspaceAppFromRoute(href: string): boolean {
7
+ return navigateToWorkspaceApp(href);
8
+ }
4
9
 
5
10
  export function meta() {
6
11
  return [{ title: "Workspace app - Dispatch" }];
@@ -8,5 +13,10 @@ export function meta() {
8
13
 
9
14
  export default function WorkspaceAppRoute() {
10
15
  const { appId } = useParams();
11
- return <WorkspaceAppHost appId={appId} />;
16
+ return (
17
+ <WorkspaceAppHost
18
+ appId={appId}
19
+ navigateToTopWindow={navigateWorkspaceAppFromRoute}
20
+ />
21
+ );
12
22
  }