@shipfox/client-onboarding 10.0.0 → 11.0.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-setup-route.d.ts","sourceRoot":"","sources":["../src/workspace-setup-route.ts"],"names":[],"mappings":"AASA,OAAO,EAA0B,KAAK,mBAAmB,EAAC,MAAM,+BAA+B,CAAC;AAChG,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,uBAAuB,CAAC;AAKvD,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,uBAAuB,CAAC,EAC5C,WAAW,EACX,WAAW,EACX,QAAQ,GACT,EAAE,0BAA0B,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAyD3D"}
1
+ {"version":3,"file":"workspace-setup-route.d.ts","sourceRoot":"","sources":["../src/workspace-setup-route.ts"],"names":[],"mappings":"AASA,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,uBAAuB,CAAC;AAKvD,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,uBAAuB,CAAC,EAC5C,WAAW,EACX,WAAW,EACX,QAAQ,GACT,EAAE,0BAA0B,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAsE3D"}
@@ -1,10 +1,25 @@
1
1
  import { isModelProviderOnboardingDismissed, modelProviderConfigsQueryOptions } from '@shipfox/client-agent';
2
2
  import { sourceConnectionsQueryOptions } from '@shipfox/client-integrations';
3
3
  import { projectExistenceQueryOptions } from '@shipfox/client-projects';
4
- import { WorkspaceSetupLoadError } from '@shipfox/client-shell/runtime';
4
+ import { userWorkspacesQueryOptions, WorkspaceSetupLoadError } from '@shipfox/client-shell/runtime';
5
5
  import { redirect } from '@tanstack/react-router';
6
6
  const TRAILING_SLASHES_RE = /\/+$/u;
7
7
  export async function loadWorkspaceSetupRoute({ queryClient, workspaceId, pathname }) {
8
+ const workspace = await fetchWorkspaceSummary(queryClient, workspaceId);
9
+ const workspaceStatus = workspace?.status;
10
+ switch(workspaceStatus){
11
+ case undefined:
12
+ case 'active':
13
+ break;
14
+ case 'suspended':
15
+ case 'deleted':
16
+ return {
17
+ hideProjectNavigation: true,
18
+ unavailable: true
19
+ };
20
+ default:
21
+ return assertNever(workspaceStatus);
22
+ }
8
23
  const projects = await fetchWorkspaceProjectExistence(queryClient, workspaceId);
9
24
  const normalizedPathname = normalizePath(pathname);
10
25
  if (projects.projects.length > 0) {
@@ -70,6 +85,17 @@ export async function loadWorkspaceSetupRoute({ queryClient, workspaceId, pathna
70
85
  replace: true
71
86
  });
72
87
  }
88
+ function assertNever(value) {
89
+ throw new Error(`Unhandled workspace status: ${JSON.stringify(value)}`);
90
+ }
91
+ async function fetchWorkspaceSummary(queryClient, workspaceId) {
92
+ try {
93
+ const result = await queryClient.fetchQuery(userWorkspacesQueryOptions());
94
+ return result.memberships.find((workspace)=>workspace.id === workspaceId);
95
+ } catch (error) {
96
+ throw new WorkspaceSetupLoadError(error);
97
+ }
98
+ }
73
99
  async function fetchWorkspaceProjectExistence(queryClient, workspaceId) {
74
100
  const options = projectExistenceQueryOptions(workspaceId);
75
101
  try {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/workspace-setup-route.ts"],"sourcesContent":["import {\n isModelProviderOnboardingDismissed,\n modelProviderConfigsQueryOptions,\n} from '@shipfox/client-agent';\nimport {\n type IntegrationConnection,\n sourceConnectionsQueryOptions,\n} from '@shipfox/client-integrations';\nimport {projectExistenceQueryOptions} from '@shipfox/client-projects';\nimport {WorkspaceSetupLoadError, type WorkspaceSetupState} from '@shipfox/client-shell/runtime';\nimport type {QueryClient} from '@tanstack/react-query';\nimport {redirect} from '@tanstack/react-router';\n\nconst TRAILING_SLASHES_RE = /\\/+$/u;\n\nexport interface WorkspaceSetupRouteOptions {\n queryClient: QueryClient;\n workspaceId: string;\n pathname: string;\n}\n\nexport async function loadWorkspaceSetupRoute({\n queryClient,\n workspaceId,\n pathname,\n}: WorkspaceSetupRouteOptions): Promise<WorkspaceSetupState> {\n const projects = await fetchWorkspaceProjectExistence(queryClient, workspaceId);\n const normalizedPathname = normalizePath(pathname);\n\n if (projects.projects.length > 0) {\n if (isIntegrationsIndexPath(normalizedPathname, workspaceId)) {\n throw redirect({\n to: '/workspaces/$wid/settings/integrations',\n params: {wid: workspaceId},\n replace: true,\n });\n }\n\n return {hideProjectNavigation: false};\n }\n\n const sourceConnections = await fetchWorkspaceSourceConnections(queryClient, workspaceId);\n const hasSourceConnection = sourceConnections.length > 0;\n\n if (!hasSourceConnection) {\n if (isIntegrationSetupPath(normalizedPathname, workspaceId)) {\n return {hideProjectNavigation: true};\n }\n\n throw redirect({\n to: '/workspaces/$wid/integrations',\n params: {wid: workspaceId},\n replace: true,\n });\n }\n\n if (isAgentSettingsPath(normalizedPathname, workspaceId)) {\n return {hideProjectNavigation: true};\n }\n\n const providerHandled = await hasHandledModelProviderOnboarding(queryClient, workspaceId);\n if (!providerHandled) {\n if (isModelProviderOnboardingPath(normalizedPathname, workspaceId)) {\n return {hideProjectNavigation: true};\n }\n\n throw redirect({\n to: '/workspaces/$wid/model-provider',\n params: {wid: workspaceId},\n replace: true,\n });\n }\n\n if (isProjectCreationPath(normalizedPathname, workspaceId)) {\n return {hideProjectNavigation: true};\n }\n\n throw redirect({\n to: '/workspaces/$wid/projects/new',\n params: {wid: workspaceId},\n replace: true,\n });\n}\n\nasync function fetchWorkspaceProjectExistence(queryClient: QueryClient, workspaceId: string) {\n const options = projectExistenceQueryOptions(workspaceId);\n\n try {\n return await queryClient.fetchQuery(options);\n } catch (error) {\n const cached = queryClient.getQueryData<{projects: unknown[]}>(options.queryKey);\n if (cached !== undefined) return cached;\n throw new WorkspaceSetupLoadError(error);\n }\n}\n\nasync function fetchWorkspaceSourceConnections(queryClient: QueryClient, workspaceId: string) {\n const options = sourceConnectionsQueryOptions(workspaceId);\n\n try {\n return await queryClient.fetchQuery(options);\n } catch (error) {\n const cached = queryClient.getQueryData<IntegrationConnection[]>(options.queryKey);\n if (cached !== undefined) return cached;\n throw new WorkspaceSetupLoadError(error);\n }\n}\n\nasync function hasHandledModelProviderOnboarding(\n queryClient: QueryClient,\n workspaceId: string,\n): Promise<boolean> {\n if (isModelProviderOnboardingDismissed(workspaceId)) return true;\n\n const options = modelProviderConfigsQueryOptions(workspaceId);\n try {\n const configs = await queryClient.fetchQuery(options);\n return configs.configs.length > 0;\n } catch {\n const cached = queryClient.getQueryData<{configs: unknown[]}>(options.queryKey);\n return cached?.configs.length !== 0;\n }\n}\n\nfunction normalizePath(pathname: string) {\n if (pathname === '/') return pathname;\n return pathname.replace(TRAILING_SLASHES_RE, '');\n}\n\nfunction workspacePath(workspaceId: string, suffix: string) {\n return `/workspaces/${workspaceId}${suffix}`;\n}\n\nfunction isIntegrationsIndexPath(pathname: string, workspaceId: string) {\n return pathname === workspacePath(workspaceId, '/integrations');\n}\n\nfunction isIntegrationSetupPath(pathname: string, workspaceId: string) {\n const basePath = workspacePath(workspaceId, '/integrations');\n return (\n pathname === basePath ||\n pathname.startsWith(`${basePath}/`) ||\n pathname === workspacePath(workspaceId, '/settings/integrations')\n );\n}\n\nfunction isProjectCreationPath(pathname: string, workspaceId: string) {\n return pathname === workspacePath(workspaceId, '/projects/new');\n}\n\nfunction isModelProviderOnboardingPath(pathname: string, workspaceId: string) {\n return pathname === workspacePath(workspaceId, '/model-provider');\n}\n\nfunction isAgentSettingsPath(pathname: string, workspaceId: string) {\n return pathname === workspacePath(workspaceId, '/settings/agents');\n}\n"],"names":["isModelProviderOnboardingDismissed","modelProviderConfigsQueryOptions","sourceConnectionsQueryOptions","projectExistenceQueryOptions","WorkspaceSetupLoadError","redirect","TRAILING_SLASHES_RE","loadWorkspaceSetupRoute","queryClient","workspaceId","pathname","projects","fetchWorkspaceProjectExistence","normalizedPathname","normalizePath","length","isIntegrationsIndexPath","to","params","wid","replace","hideProjectNavigation","sourceConnections","fetchWorkspaceSourceConnections","hasSourceConnection","isIntegrationSetupPath","isAgentSettingsPath","providerHandled","hasHandledModelProviderOnboarding","isModelProviderOnboardingPath","isProjectCreationPath","options","fetchQuery","error","cached","getQueryData","queryKey","undefined","configs","workspacePath","suffix","basePath","startsWith"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/workspace-setup-route.ts"],"sourcesContent":["import {\n isModelProviderOnboardingDismissed,\n modelProviderConfigsQueryOptions,\n} from '@shipfox/client-agent';\nimport {\n type IntegrationConnection,\n sourceConnectionsQueryOptions,\n} from '@shipfox/client-integrations';\nimport {projectExistenceQueryOptions} from '@shipfox/client-projects';\nimport {\n userWorkspacesQueryOptions,\n WorkspaceSetupLoadError,\n type WorkspaceSetupState,\n} from '@shipfox/client-shell/runtime';\nimport type {QueryClient} from '@tanstack/react-query';\nimport {redirect} from '@tanstack/react-router';\n\nconst TRAILING_SLASHES_RE = /\\/+$/u;\n\nexport interface WorkspaceSetupRouteOptions {\n queryClient: QueryClient;\n workspaceId: string;\n pathname: string;\n}\n\nexport async function loadWorkspaceSetupRoute({\n queryClient,\n workspaceId,\n pathname,\n}: WorkspaceSetupRouteOptions): Promise<WorkspaceSetupState> {\n const workspace = await fetchWorkspaceSummary(queryClient, workspaceId);\n const workspaceStatus = workspace?.status;\n switch (workspaceStatus) {\n case undefined:\n case 'active':\n break;\n case 'suspended':\n case 'deleted':\n return {hideProjectNavigation: true, unavailable: true};\n default:\n return assertNever(workspaceStatus);\n }\n\n const projects = await fetchWorkspaceProjectExistence(queryClient, workspaceId);\n const normalizedPathname = normalizePath(pathname);\n\n if (projects.projects.length > 0) {\n if (isIntegrationsIndexPath(normalizedPathname, workspaceId)) {\n throw redirect({\n to: '/workspaces/$wid/settings/integrations',\n params: {wid: workspaceId},\n replace: true,\n });\n }\n\n return {hideProjectNavigation: false};\n }\n\n const sourceConnections = await fetchWorkspaceSourceConnections(queryClient, workspaceId);\n const hasSourceConnection = sourceConnections.length > 0;\n\n if (!hasSourceConnection) {\n if (isIntegrationSetupPath(normalizedPathname, workspaceId)) {\n return {hideProjectNavigation: true};\n }\n\n throw redirect({\n to: '/workspaces/$wid/integrations',\n params: {wid: workspaceId},\n replace: true,\n });\n }\n\n if (isAgentSettingsPath(normalizedPathname, workspaceId)) {\n return {hideProjectNavigation: true};\n }\n\n const providerHandled = await hasHandledModelProviderOnboarding(queryClient, workspaceId);\n if (!providerHandled) {\n if (isModelProviderOnboardingPath(normalizedPathname, workspaceId)) {\n return {hideProjectNavigation: true};\n }\n\n throw redirect({\n to: '/workspaces/$wid/model-provider',\n params: {wid: workspaceId},\n replace: true,\n });\n }\n\n if (isProjectCreationPath(normalizedPathname, workspaceId)) {\n return {hideProjectNavigation: true};\n }\n\n throw redirect({\n to: '/workspaces/$wid/projects/new',\n params: {wid: workspaceId},\n replace: true,\n });\n}\n\nfunction assertNever(value: never): never {\n throw new Error(`Unhandled workspace status: ${JSON.stringify(value)}`);\n}\n\nasync function fetchWorkspaceSummary(queryClient: QueryClient, workspaceId: string) {\n try {\n const result = await queryClient.fetchQuery(userWorkspacesQueryOptions());\n return result.memberships.find((workspace) => workspace.id === workspaceId);\n } catch (error) {\n throw new WorkspaceSetupLoadError(error);\n }\n}\n\nasync function fetchWorkspaceProjectExistence(queryClient: QueryClient, workspaceId: string) {\n const options = projectExistenceQueryOptions(workspaceId);\n\n try {\n return await queryClient.fetchQuery(options);\n } catch (error) {\n const cached = queryClient.getQueryData<{projects: unknown[]}>(options.queryKey);\n if (cached !== undefined) return cached;\n throw new WorkspaceSetupLoadError(error);\n }\n}\n\nasync function fetchWorkspaceSourceConnections(queryClient: QueryClient, workspaceId: string) {\n const options = sourceConnectionsQueryOptions(workspaceId);\n\n try {\n return await queryClient.fetchQuery(options);\n } catch (error) {\n const cached = queryClient.getQueryData<IntegrationConnection[]>(options.queryKey);\n if (cached !== undefined) return cached;\n throw new WorkspaceSetupLoadError(error);\n }\n}\n\nasync function hasHandledModelProviderOnboarding(\n queryClient: QueryClient,\n workspaceId: string,\n): Promise<boolean> {\n if (isModelProviderOnboardingDismissed(workspaceId)) return true;\n\n const options = modelProviderConfigsQueryOptions(workspaceId);\n try {\n const configs = await queryClient.fetchQuery(options);\n return configs.configs.length > 0;\n } catch {\n const cached = queryClient.getQueryData<{configs: unknown[]}>(options.queryKey);\n return cached?.configs.length !== 0;\n }\n}\n\nfunction normalizePath(pathname: string) {\n if (pathname === '/') return pathname;\n return pathname.replace(TRAILING_SLASHES_RE, '');\n}\n\nfunction workspacePath(workspaceId: string, suffix: string) {\n return `/workspaces/${workspaceId}${suffix}`;\n}\n\nfunction isIntegrationsIndexPath(pathname: string, workspaceId: string) {\n return pathname === workspacePath(workspaceId, '/integrations');\n}\n\nfunction isIntegrationSetupPath(pathname: string, workspaceId: string) {\n const basePath = workspacePath(workspaceId, '/integrations');\n return (\n pathname === basePath ||\n pathname.startsWith(`${basePath}/`) ||\n pathname === workspacePath(workspaceId, '/settings/integrations')\n );\n}\n\nfunction isProjectCreationPath(pathname: string, workspaceId: string) {\n return pathname === workspacePath(workspaceId, '/projects/new');\n}\n\nfunction isModelProviderOnboardingPath(pathname: string, workspaceId: string) {\n return pathname === workspacePath(workspaceId, '/model-provider');\n}\n\nfunction isAgentSettingsPath(pathname: string, workspaceId: string) {\n return pathname === workspacePath(workspaceId, '/settings/agents');\n}\n"],"names":["isModelProviderOnboardingDismissed","modelProviderConfigsQueryOptions","sourceConnectionsQueryOptions","projectExistenceQueryOptions","userWorkspacesQueryOptions","WorkspaceSetupLoadError","redirect","TRAILING_SLASHES_RE","loadWorkspaceSetupRoute","queryClient","workspaceId","pathname","workspace","fetchWorkspaceSummary","workspaceStatus","status","undefined","hideProjectNavigation","unavailable","assertNever","projects","fetchWorkspaceProjectExistence","normalizedPathname","normalizePath","length","isIntegrationsIndexPath","to","params","wid","replace","sourceConnections","fetchWorkspaceSourceConnections","hasSourceConnection","isIntegrationSetupPath","isAgentSettingsPath","providerHandled","hasHandledModelProviderOnboarding","isModelProviderOnboardingPath","isProjectCreationPath","value","Error","JSON","stringify","result","fetchQuery","memberships","find","id","error","options","cached","getQueryData","queryKey","configs","workspacePath","suffix","basePath","startsWith"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-onboarding",
3
3
  "license": "MIT",
4
- "version": "10.0.0",
4
+ "version": "11.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -18,10 +18,10 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@swc/helpers": "^0.5.17",
21
- "@shipfox/client-agent": "10.0.0",
22
- "@shipfox/client-projects": "10.0.0",
23
- "@shipfox/client-shell": "10.0.0",
24
- "@shipfox/client-integrations": "10.0.0"
21
+ "@shipfox/client-agent": "11.0.0",
22
+ "@shipfox/client-projects": "11.0.0",
23
+ "@shipfox/client-integrations": "11.0.0",
24
+ "@shipfox/client-shell": "11.0.0"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "@tanstack/react-query": "^5.101.0",
@@ -28,6 +28,8 @@ import {act, cleanup, fireEvent, render, screen, waitFor} from '@testing-library
28
28
  import {loadWorkspaceSetupRoute} from './workspace-setup-route.js';
29
29
 
30
30
  const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
31
+ const MEMBERSHIP_ID = '44444444-4444-4444-8444-444444444444';
32
+ const USER_ID = '55555555-5555-4555-8555-555555555555';
31
33
 
32
34
  function jsonResponse(body: unknown, init: ResponseInit = {}) {
33
35
  return new Response(JSON.stringify(body), {
@@ -80,6 +82,7 @@ interface SetupFetchOptions {
80
82
  connectionsFail?: boolean;
81
83
  providerConfigsFail?: boolean;
82
84
  projectsPending?: boolean;
85
+ workspaceStatus?: 'active' | 'suspended' | 'deleted';
83
86
  }
84
87
 
85
88
  function setupFetch(options: SetupFetchOptions = {}) {
@@ -92,10 +95,28 @@ function setupFetch(options: SetupFetchOptions = {}) {
92
95
  connectionsFail = false,
93
96
  providerConfigsFail = false,
94
97
  projectsPending = false,
98
+ workspaceStatus = 'active',
95
99
  } = options;
96
100
 
97
101
  return vi.fn((input: RequestInfo | URL) => {
98
102
  const url = input instanceof Request ? input.url : String(input);
103
+ if (url.endsWith('/workspaces')) {
104
+ return Promise.resolve(
105
+ jsonResponse({
106
+ memberships: [
107
+ {
108
+ id: MEMBERSHIP_ID,
109
+ user_id: USER_ID,
110
+ workspace_id: WORKSPACE_ID,
111
+ workspace_name: 'Workspace',
112
+ workspace_status: workspaceStatus,
113
+ created_at: new Date().toISOString(),
114
+ updated_at: new Date().toISOString(),
115
+ },
116
+ ],
117
+ }),
118
+ );
119
+ }
99
120
  if (url.includes('/projects?')) {
100
121
  if (projectsPending) return new Promise<Response>(() => undefined);
101
122
  if (projectsFail) return Promise.resolve(jsonResponse({code: 'server-error'}, {status: 500}));
@@ -179,7 +200,7 @@ function GuardedRoute({label}: {label: string}) {
179
200
  <div data-testid="project-navigation">
180
201
  {setupState.hideProjectNavigation ? 'hidden' : 'visible'}
181
202
  </div>
182
- <main>{label}</main>
203
+ <main>{setupState.unavailable ? 'Workspace unavailable' : label}</main>
183
204
  </>
184
205
  );
185
206
  }
@@ -236,6 +257,19 @@ describe('workspace setup route hook', () => {
236
257
  expect(screen.queryByText('Workspace home')).not.toBeInTheDocument();
237
258
  });
238
259
 
260
+ test.each([
261
+ 'suspended',
262
+ 'deleted',
263
+ ] as const)('stops workspace setup for a %s workspace before loading workspace data', async (workspaceStatus) => {
264
+ const fetchImpl = setupFetch({workspaceStatus, projectsPending: true});
265
+
266
+ renderSetupRoute(`/workspaces/${WORKSPACE_ID}`, fetchImpl);
267
+
268
+ expect(await screen.findByText('Workspace unavailable')).toBeInTheDocument();
269
+ expect(screen.getByTestId('project-navigation')).toHaveTextContent('hidden');
270
+ expect(calledUrls(fetchImpl).some((url) => url.includes('/projects?'))).toBe(false);
271
+ });
272
+
239
273
  test('allows normal workspace content and skips source connections when a project exists', async () => {
240
274
  const fetchImpl = setupFetch({projects: [projectStub()]});
241
275
 
@@ -449,6 +483,23 @@ describe('workspace setup route hook', () => {
449
483
  let projectAttempts = 0;
450
484
  const fetchImpl = vi.fn((input: RequestInfo | URL) => {
451
485
  const url = input instanceof Request ? input.url : String(input);
486
+ if (url.endsWith('/workspaces')) {
487
+ return Promise.resolve(
488
+ jsonResponse({
489
+ memberships: [
490
+ {
491
+ id: MEMBERSHIP_ID,
492
+ user_id: USER_ID,
493
+ workspace_id: WORKSPACE_ID,
494
+ workspace_name: 'Workspace',
495
+ workspace_status: 'active',
496
+ created_at: new Date().toISOString(),
497
+ updated_at: new Date().toISOString(),
498
+ },
499
+ ],
500
+ }),
501
+ );
502
+ }
452
503
  if (url.includes('/projects?')) {
453
504
  projectAttempts += 1;
454
505
  if (projectAttempts === 1)
@@ -488,6 +539,23 @@ describe('workspace setup route hook', () => {
488
539
  const fetchImpl = setupFetch({connections: [sourceConnection()]});
489
540
  fetchImpl.mockImplementation((input: RequestInfo | URL) => {
490
541
  const url = input instanceof Request ? input.url : String(input);
542
+ if (url.endsWith('/workspaces')) {
543
+ return Promise.resolve(
544
+ jsonResponse({
545
+ memberships: [
546
+ {
547
+ id: MEMBERSHIP_ID,
548
+ user_id: USER_ID,
549
+ workspace_id: WORKSPACE_ID,
550
+ workspace_name: 'Workspace',
551
+ workspace_status: 'active',
552
+ created_at: new Date().toISOString(),
553
+ updated_at: new Date().toISOString(),
554
+ },
555
+ ],
556
+ }),
557
+ );
558
+ }
491
559
  if (url.includes('/projects?')) {
492
560
  return Promise.resolve(jsonResponse({projects, next_cursor: null}));
493
561
  }
@@ -7,7 +7,11 @@ import {
7
7
  sourceConnectionsQueryOptions,
8
8
  } from '@shipfox/client-integrations';
9
9
  import {projectExistenceQueryOptions} from '@shipfox/client-projects';
10
- import {WorkspaceSetupLoadError, type WorkspaceSetupState} from '@shipfox/client-shell/runtime';
10
+ import {
11
+ userWorkspacesQueryOptions,
12
+ WorkspaceSetupLoadError,
13
+ type WorkspaceSetupState,
14
+ } from '@shipfox/client-shell/runtime';
11
15
  import type {QueryClient} from '@tanstack/react-query';
12
16
  import {redirect} from '@tanstack/react-router';
13
17
 
@@ -24,6 +28,19 @@ export async function loadWorkspaceSetupRoute({
24
28
  workspaceId,
25
29
  pathname,
26
30
  }: WorkspaceSetupRouteOptions): Promise<WorkspaceSetupState> {
31
+ const workspace = await fetchWorkspaceSummary(queryClient, workspaceId);
32
+ const workspaceStatus = workspace?.status;
33
+ switch (workspaceStatus) {
34
+ case undefined:
35
+ case 'active':
36
+ break;
37
+ case 'suspended':
38
+ case 'deleted':
39
+ return {hideProjectNavigation: true, unavailable: true};
40
+ default:
41
+ return assertNever(workspaceStatus);
42
+ }
43
+
27
44
  const projects = await fetchWorkspaceProjectExistence(queryClient, workspaceId);
28
45
  const normalizedPathname = normalizePath(pathname);
29
46
 
@@ -82,6 +99,19 @@ export async function loadWorkspaceSetupRoute({
82
99
  });
83
100
  }
84
101
 
102
+ function assertNever(value: never): never {
103
+ throw new Error(`Unhandled workspace status: ${JSON.stringify(value)}`);
104
+ }
105
+
106
+ async function fetchWorkspaceSummary(queryClient: QueryClient, workspaceId: string) {
107
+ try {
108
+ const result = await queryClient.fetchQuery(userWorkspacesQueryOptions());
109
+ return result.memberships.find((workspace) => workspace.id === workspaceId);
110
+ } catch (error) {
111
+ throw new WorkspaceSetupLoadError(error);
112
+ }
113
+ }
114
+
85
115
  async function fetchWorkspaceProjectExistence(queryClient: QueryClient, workspaceId: string) {
86
116
  const options = projectExistenceQueryOptions(workspaceId);
87
117