@shipfox/client-projects 0.2.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.
Files changed (125) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/.storybook/preview.css +7 -0
  3. package/.storybook/preview.tsx +56 -0
  4. package/.turbo/turbo-build.log +2 -0
  5. package/.turbo/turbo-type$colon$emit.log +1 -0
  6. package/.turbo/turbo-type.log +1 -0
  7. package/CHANGELOG.md +207 -0
  8. package/LICENSE +21 -0
  9. package/dist/chrome.d.ts +3 -0
  10. package/dist/chrome.d.ts.map +1 -0
  11. package/dist/chrome.js +42 -0
  12. package/dist/chrome.js.map +1 -0
  13. package/dist/components/model-provider-reminder-banner.d.ts +4 -0
  14. package/dist/components/model-provider-reminder-banner.d.ts.map +1 -0
  15. package/dist/components/model-provider-reminder-banner.js +73 -0
  16. package/dist/components/model-provider-reminder-banner.js.map +1 -0
  17. package/dist/components/project-crumb.d.ts +7 -0
  18. package/dist/components/project-crumb.d.ts.map +1 -0
  19. package/dist/components/project-crumb.js +93 -0
  20. package/dist/components/project-crumb.js.map +1 -0
  21. package/dist/components/project-switcher.d.ts +7 -0
  22. package/dist/components/project-switcher.d.ts.map +1 -0
  23. package/dist/components/project-switcher.js +138 -0
  24. package/dist/components/project-switcher.js.map +1 -0
  25. package/dist/components/source-strip.d.ts +14 -0
  26. package/dist/components/source-strip.d.ts.map +1 -0
  27. package/dist/components/source-strip.js +98 -0
  28. package/dist/components/source-strip.js.map +1 -0
  29. package/dist/components/workspace-setup-guard.d.ts +9 -0
  30. package/dist/components/workspace-setup-guard.d.ts.map +1 -0
  31. package/dist/components/workspace-setup-guard.js +160 -0
  32. package/dist/components/workspace-setup-guard.js.map +1 -0
  33. package/dist/feature.d.ts +35 -0
  34. package/dist/feature.d.ts.map +1 -0
  35. package/dist/feature.js +45 -0
  36. package/dist/feature.js.map +1 -0
  37. package/dist/hooks/api/definitions.d.ts +97 -0
  38. package/dist/hooks/api/definitions.d.ts.map +1 -0
  39. package/dist/hooks/api/definitions.js +55 -0
  40. package/dist/hooks/api/definitions.js.map +1 -0
  41. package/dist/hooks/api/projects.d.ts +93 -0
  42. package/dist/hooks/api/projects.d.ts.map +1 -0
  43. package/dist/hooks/api/projects.js +79 -0
  44. package/dist/hooks/api/projects.js.map +1 -0
  45. package/dist/index.d.ts +13 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +13 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/pages/create-project-page.d.ts +2 -0
  50. package/dist/pages/create-project-page.d.ts.map +1 -0
  51. package/dist/pages/create-project-page.js +406 -0
  52. package/dist/pages/create-project-page.js.map +1 -0
  53. package/dist/pages/create-project-page.stories.d.ts +24 -0
  54. package/dist/pages/create-project-page.stories.d.ts.map +1 -0
  55. package/dist/pages/create-project-page.stories.js +345 -0
  56. package/dist/pages/create-project-page.stories.js.map +1 -0
  57. package/dist/pages/home-router.d.ts +2 -0
  58. package/dist/pages/home-router.d.ts.map +1 -0
  59. package/dist/pages/home-router.js +7 -0
  60. package/dist/pages/home-router.js.map +1 -0
  61. package/dist/pages/project-workflows-page.d.ts +4 -0
  62. package/dist/pages/project-workflows-page.d.ts.map +1 -0
  63. package/dist/pages/project-workflows-page.js +481 -0
  64. package/dist/pages/project-workflows-page.js.map +1 -0
  65. package/dist/pages/projects-hub-page.d.ts +2 -0
  66. package/dist/pages/projects-hub-page.d.ts.map +1 -0
  67. package/dist/pages/projects-hub-page.js +256 -0
  68. package/dist/pages/projects-hub-page.js.map +1 -0
  69. package/dist/project-error.d.ts +7 -0
  70. package/dist/project-error.d.ts.map +1 -0
  71. package/dist/project-error.js +93 -0
  72. package/dist/project-error.js.map +1 -0
  73. package/dist/routes/create-project.d.ts +6 -0
  74. package/dist/routes/create-project.d.ts.map +1 -0
  75. package/dist/routes/create-project.js +7 -0
  76. package/dist/routes/create-project.js.map +1 -0
  77. package/dist/routes/home.d.ts +6 -0
  78. package/dist/routes/home.d.ts.map +1 -0
  79. package/dist/routes/home.js +7 -0
  80. package/dist/routes/home.js.map +1 -0
  81. package/dist/routes/project-index.d.ts +10 -0
  82. package/dist/routes/project-index.d.ts.map +1 -0
  83. package/dist/routes/project-index.js +15 -0
  84. package/dist/routes/project-index.js.map +1 -0
  85. package/dist/routes/workflows.d.ts +5 -0
  86. package/dist/routes/workflows.d.ts.map +1 -0
  87. package/dist/routes/workflows.js +16 -0
  88. package/dist/routes/workflows.js.map +1 -0
  89. package/dist/tsconfig.test.tsbuildinfo +1 -0
  90. package/package.json +120 -0
  91. package/src/chrome.tsx +26 -0
  92. package/src/components/model-provider-reminder-banner.tsx +78 -0
  93. package/src/components/project-crumb.tsx +70 -0
  94. package/src/components/project-switcher.test.tsx +91 -0
  95. package/src/components/project-switcher.tsx +127 -0
  96. package/src/components/source-strip.tsx +91 -0
  97. package/src/components/workspace-setup-guard.test.tsx +534 -0
  98. package/src/components/workspace-setup-guard.tsx +185 -0
  99. package/src/feature.ts +44 -0
  100. package/src/hooks/api/definitions.ts +50 -0
  101. package/src/hooks/api/projects.test.ts +78 -0
  102. package/src/hooks/api/projects.ts +75 -0
  103. package/src/index.ts +15 -0
  104. package/src/pages/create-project-page.stories.tsx +344 -0
  105. package/src/pages/create-project-page.test.tsx +317 -0
  106. package/src/pages/create-project-page.tsx +386 -0
  107. package/src/pages/home-router.test.tsx +27 -0
  108. package/src/pages/home-router.tsx +5 -0
  109. package/src/pages/project-workflows-page.test.tsx +258 -0
  110. package/src/pages/project-workflows-page.tsx +445 -0
  111. package/src/pages/projects-hub-page.test.tsx +313 -0
  112. package/src/pages/projects-hub-page.tsx +246 -0
  113. package/src/project-error.test.ts +59 -0
  114. package/src/project-error.ts +106 -0
  115. package/src/routes/create-project.tsx +4 -0
  116. package/src/routes/home.tsx +4 -0
  117. package/src/routes/project-index.tsx +11 -0
  118. package/src/routes/workflows.tsx +10 -0
  119. package/test/pages.tsx +151 -0
  120. package/test/setup.ts +3 -0
  121. package/tsconfig.build.json +9 -0
  122. package/tsconfig.build.tsbuildinfo +1 -0
  123. package/tsconfig.json +3 -0
  124. package/tsconfig.test.json +8 -0
  125. package/vitest.config.ts +70 -0
@@ -0,0 +1,185 @@
1
+ import type {ListIntegrationConnectionsResponseDto} from '@shipfox/api-integration-core-dto';
2
+ import type {ListProjectsResponseDto} from '@shipfox/api-projects-dto';
3
+ import {
4
+ isModelProviderOnboardingDismissed,
5
+ listModelProviderConfigs,
6
+ modelProviderQueryKeys,
7
+ } from '@shipfox/client-agent';
8
+ import {integrationsQueryKeys, listSourceConnections} from '@shipfox/client-integrations';
9
+ import {WorkspaceSetupLoadError, type WorkspaceSetupState} from '@shipfox/client-shell/runtime';
10
+ import type {QueryClient} from '@tanstack/react-query';
11
+ import {redirect} from '@tanstack/react-router';
12
+ import {listProjects, projectsQueryKeys} from '#hooks/api/projects.js';
13
+
14
+ const TRAILING_SLASHES_RE = /\/+$/u;
15
+ const PROJECT_EXISTENCE_STALE_TIME_MS = 30_000;
16
+ type ListModelProviderConfigsResponseDto = Awaited<ReturnType<typeof listModelProviderConfigs>>;
17
+
18
+ export interface WorkspaceSetupRouteOptions {
19
+ queryClient: QueryClient;
20
+ workspaceId: string;
21
+ pathname: string;
22
+ }
23
+
24
+ export async function loadWorkspaceSetupRoute({
25
+ queryClient,
26
+ workspaceId,
27
+ pathname,
28
+ }: WorkspaceSetupRouteOptions): Promise<WorkspaceSetupState> {
29
+ const projects = await fetchWorkspaceProjectExistence(queryClient, workspaceId);
30
+ const normalizedPathname = normalizePath(pathname);
31
+ const hasProject = projects.projects.length > 0;
32
+
33
+ if (hasProject) {
34
+ if (isIntegrationsIndexPath(normalizedPathname, workspaceId)) {
35
+ throw redirect({
36
+ to: '/workspaces/$wid/settings/integrations',
37
+ params: {wid: workspaceId},
38
+ replace: true,
39
+ });
40
+ }
41
+
42
+ return {hideProjectNavigation: false};
43
+ }
44
+
45
+ const sourceConnections = await fetchWorkspaceSourceConnections(queryClient, workspaceId);
46
+ const hasSourceConnection = sourceConnections.connections.length > 0;
47
+
48
+ if (!hasSourceConnection) {
49
+ if (isIntegrationSetupPath(normalizedPathname, workspaceId)) {
50
+ return {hideProjectNavigation: true};
51
+ }
52
+
53
+ throw redirect({
54
+ to: '/workspaces/$wid/integrations',
55
+ params: {wid: workspaceId},
56
+ replace: true,
57
+ });
58
+ }
59
+
60
+ if (isAgentSettingsPath(normalizedPathname, workspaceId)) {
61
+ return {hideProjectNavigation: true};
62
+ }
63
+
64
+ const providerHandled = await hasHandledModelProviderOnboarding(queryClient, workspaceId);
65
+ if (!providerHandled) {
66
+ if (isModelProviderOnboardingPath(normalizedPathname, workspaceId)) {
67
+ return {hideProjectNavigation: true};
68
+ }
69
+
70
+ throw redirect({
71
+ to: '/workspaces/$wid/model-provider',
72
+ params: {wid: workspaceId},
73
+ replace: true,
74
+ });
75
+ }
76
+
77
+ if (isProjectCreationPath(normalizedPathname, workspaceId)) {
78
+ return {hideProjectNavigation: true};
79
+ }
80
+
81
+ throw redirect({
82
+ to: '/workspaces/$wid/projects/new',
83
+ params: {wid: workspaceId},
84
+ replace: true,
85
+ });
86
+ }
87
+
88
+ async function fetchWorkspaceProjectExistence(queryClient: QueryClient, workspaceId: string) {
89
+ const queryKey = projectsQueryKeys.exists(workspaceId);
90
+
91
+ try {
92
+ return await queryClient.fetchQuery({
93
+ queryKey,
94
+ queryFn: ({signal}) => listProjects({workspaceId, limit: 1, signal}),
95
+ // beforeLoad runs for every in-workspace navigation. Use a short
96
+ // freshness window to avoid hot-path refetches while still detecting
97
+ // project creation from another tab or actor.
98
+ staleTime: PROJECT_EXISTENCE_STALE_TIME_MS,
99
+ });
100
+ } catch (error) {
101
+ const cached = queryClient.getQueryData<ListProjectsResponseDto>(queryKey);
102
+ if (cached !== undefined) return cached;
103
+ throw new WorkspaceSetupLoadError(error);
104
+ }
105
+ }
106
+
107
+ async function fetchWorkspaceSourceConnections(queryClient: QueryClient, workspaceId: string) {
108
+ const queryKey = integrationsQueryKeys.sourceConnections(workspaceId);
109
+
110
+ try {
111
+ // Onboarding-only path: kept fresh (no staleTime) because some install
112
+ // flows connect a source without invalidating this key, and a stale
113
+ // "no connection" read would trap the user in the onboarding redirect.
114
+ return await queryClient.fetchQuery({
115
+ queryKey,
116
+ queryFn: ({signal}) => listSourceConnections({workspaceId, signal}),
117
+ });
118
+ } catch (error) {
119
+ const cached = queryClient.getQueryData<ListIntegrationConnectionsResponseDto>(queryKey);
120
+ if (cached !== undefined) return cached;
121
+ throw new WorkspaceSetupLoadError(error);
122
+ }
123
+ }
124
+
125
+ async function hasHandledModelProviderOnboarding(
126
+ queryClient: QueryClient,
127
+ workspaceId: string,
128
+ ): Promise<boolean> {
129
+ if (isModelProviderOnboardingDismissed(workspaceId)) return true;
130
+
131
+ const configs = await fetchWorkspaceModelProviderConfigs(queryClient, workspaceId);
132
+ return configs === null || configs.configs.length > 0;
133
+ }
134
+
135
+ async function fetchWorkspaceModelProviderConfigs(
136
+ queryClient: QueryClient,
137
+ workspaceId: string,
138
+ ): Promise<ListModelProviderConfigsResponseDto | null> {
139
+ const queryKey = modelProviderQueryKeys.configs(workspaceId);
140
+
141
+ try {
142
+ return await queryClient.fetchQuery({
143
+ queryKey,
144
+ queryFn: ({signal}) => listModelProviderConfigs({workspaceId, signal}),
145
+ });
146
+ } catch {
147
+ const cached = queryClient.getQueryData<ListModelProviderConfigsResponseDto>(queryKey);
148
+ if (cached !== undefined) return cached;
149
+ return null;
150
+ }
151
+ }
152
+
153
+ function normalizePath(pathname: string) {
154
+ if (pathname === '/') return pathname;
155
+ return pathname.replace(TRAILING_SLASHES_RE, '');
156
+ }
157
+
158
+ function workspacePath(workspaceId: string, suffix: string) {
159
+ return `/workspaces/${workspaceId}${suffix}`;
160
+ }
161
+
162
+ function isIntegrationsIndexPath(pathname: string, workspaceId: string) {
163
+ return pathname === workspacePath(workspaceId, '/integrations');
164
+ }
165
+
166
+ function isIntegrationSetupPath(pathname: string, workspaceId: string) {
167
+ const basePath = workspacePath(workspaceId, '/integrations');
168
+ return (
169
+ pathname === basePath ||
170
+ pathname.startsWith(`${basePath}/`) ||
171
+ pathname === workspacePath(workspaceId, '/settings/integrations')
172
+ );
173
+ }
174
+
175
+ function isProjectCreationPath(pathname: string, workspaceId: string) {
176
+ return pathname === workspacePath(workspaceId, '/projects/new');
177
+ }
178
+
179
+ function isModelProviderOnboardingPath(pathname: string, workspaceId: string) {
180
+ return pathname === workspacePath(workspaceId, '/model-provider');
181
+ }
182
+
183
+ function isAgentSettingsPath(pathname: string, workspaceId: string) {
184
+ return pathname === workspacePath(workspaceId, '/settings/agents');
185
+ }
package/src/feature.ts ADDED
@@ -0,0 +1,44 @@
1
+ import {defineClientFeature} from '@shipfox/client-shell';
2
+
3
+ export const projectsFeature = defineClientFeature({
4
+ id: 'shipfox.projects',
5
+ routes: [
6
+ {
7
+ path: '/workspaces/$wid',
8
+ parent: 'workspaceLayout',
9
+ impl: '@shipfox/client-projects/routes/home',
10
+ },
11
+ {
12
+ path: '/workspaces/$wid/projects/new',
13
+ parent: 'workspaceLayout',
14
+ impl: '@shipfox/client-projects/routes/create-project',
15
+ },
16
+ {
17
+ path: '/workspaces/$wid/projects/$pid',
18
+ parent: 'projectLayout',
19
+ impl: '@shipfox/client-projects/routes/project-index',
20
+ },
21
+ {
22
+ path: '/workspaces/$wid/projects/$pid/workflows',
23
+ parent: 'projectLayout',
24
+ impl: '@shipfox/client-projects/routes/workflows',
25
+ },
26
+ ],
27
+ navigation: [
28
+ {
29
+ id: 'nav.projects',
30
+ scope: 'workspace',
31
+ label: 'Projects',
32
+ to: '/workspaces/$wid',
33
+ exact: true,
34
+ order: 100,
35
+ },
36
+ {
37
+ id: 'nav.workflows',
38
+ scope: 'project',
39
+ label: 'Workflows',
40
+ to: '/workspaces/$wid/projects/$pid/workflows',
41
+ order: 200,
42
+ },
43
+ ],
44
+ });
@@ -0,0 +1,50 @@
1
+ import type {DefinitionListResponseDto} from '@shipfox/api-definitions-dto';
2
+ import {apiRequest} from '@shipfox/client-api';
3
+ import {keepPreviousData, useInfiniteQuery, useQuery} from '@tanstack/react-query';
4
+
5
+ export const definitionsQueryKeys = {
6
+ all: ['definitions'] as const,
7
+ list: (projectId: string) => [...definitionsQueryKeys.all, 'list', projectId] as const,
8
+ };
9
+
10
+ export async function listDefinitions({
11
+ projectId,
12
+ limit = 50,
13
+ cursor,
14
+ signal,
15
+ }: {
16
+ projectId: string;
17
+ limit?: number;
18
+ cursor?: string | undefined;
19
+ signal?: AbortSignal;
20
+ }) {
21
+ const params = new URLSearchParams({project_id: projectId, limit: String(limit)});
22
+ if (cursor) params.set('cursor', cursor);
23
+ return await apiRequest<DefinitionListResponseDto>(`/definitions?${params.toString()}`, {
24
+ signal,
25
+ });
26
+ }
27
+
28
+ export function useDefinitionsInfiniteQuery(projectId: string | undefined, limit = 50) {
29
+ return useInfiniteQuery({
30
+ queryKey: projectId
31
+ ? definitionsQueryKeys.list(projectId)
32
+ : [...definitionsQueryKeys.all, 'list'],
33
+ enabled: Boolean(projectId),
34
+ initialPageParam: undefined as string | undefined,
35
+ queryFn: ({pageParam, signal}) =>
36
+ listDefinitions({projectId: projectId ?? '', limit, cursor: pageParam, signal}),
37
+ getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined,
38
+ placeholderData: keepPreviousData,
39
+ });
40
+ }
41
+
42
+ export function useDefinitionsQuery(projectId: string | undefined) {
43
+ return useQuery({
44
+ queryKey: projectId
45
+ ? definitionsQueryKeys.list(projectId)
46
+ : [...definitionsQueryKeys.all, 'list'],
47
+ enabled: Boolean(projectId),
48
+ queryFn: ({signal}) => listDefinitions({projectId: projectId ?? '', signal}),
49
+ });
50
+ }
@@ -0,0 +1,78 @@
1
+ import {configureApiClient} from '@shipfox/client-api';
2
+ import {createProject, listProjects} from './projects.js';
3
+
4
+ function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
5
+ return new Response(JSON.stringify(body), {
6
+ headers: {'content-type': 'application/json'},
7
+ status: 200,
8
+ ...init,
9
+ });
10
+ }
11
+
12
+ describe('listProjects', () => {
13
+ beforeEach(() => {
14
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl: undefined});
15
+ });
16
+
17
+ test('includes workspace, limit, cursor, and search params', async () => {
18
+ const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({projects: [], next_cursor: null}));
19
+ configureApiClient({fetchImpl});
20
+
21
+ const result = await listProjects({
22
+ workspaceId: '11111111-1111-4111-8111-111111111111',
23
+ limit: 25,
24
+ cursor: 'cursor-1',
25
+ search: 'platform api',
26
+ });
27
+
28
+ const request = fetchImpl.mock.calls[0]?.[0] as Request;
29
+ const url = new URL(request.url);
30
+ expect(result.projects).toEqual([]);
31
+ expect(url.pathname).toBe('/projects');
32
+ expect(url.searchParams.get('workspace_id')).toBe('11111111-1111-4111-8111-111111111111');
33
+ expect(url.searchParams.get('limit')).toBe('25');
34
+ expect(url.searchParams.get('cursor')).toBe('cursor-1');
35
+ expect(url.searchParams.get('search')).toBe('platform api');
36
+ });
37
+ });
38
+
39
+ describe('createProject', () => {
40
+ beforeEach(() => {
41
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl: undefined});
42
+ });
43
+
44
+ test('posts the project body', async () => {
45
+ let requestBody: unknown;
46
+ const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
47
+ requestBody = await (input as Request).clone().json();
48
+ return jsonResponse({
49
+ id: '44444444-4444-4444-8444-444444444444',
50
+ workspace_id: '11111111-1111-4111-8111-111111111111',
51
+ name: 'Platform',
52
+ source: {
53
+ connection_id: '33333333-3333-4333-8333-333333333333',
54
+ external_repository_id: 'platform',
55
+ },
56
+ created_at: '2026-05-07T01:00:00.000Z',
57
+ updated_at: '2026-05-07T01:00:00.000Z',
58
+ });
59
+ });
60
+ configureApiClient({fetchImpl});
61
+ const body = {
62
+ workspace_id: '11111111-1111-4111-8111-111111111111',
63
+ name: 'Platform',
64
+ source: {
65
+ connection_id: '33333333-3333-4333-8333-333333333333',
66
+ external_repository_id: 'platform',
67
+ },
68
+ };
69
+
70
+ const result = await createProject(body);
71
+
72
+ const request = fetchImpl.mock.calls[0]?.[0] as Request;
73
+ expect(result.name).toBe('Platform');
74
+ expect(request.url).toBe('https://api.example.test/projects');
75
+ expect(request.method).toBe('POST');
76
+ expect(requestBody).toEqual(body);
77
+ });
78
+ });
@@ -0,0 +1,75 @@
1
+ import type {
2
+ CreateProjectBodyDto,
3
+ ListProjectsResponseDto,
4
+ ProjectResponseDto,
5
+ } from '@shipfox/api-projects-dto';
6
+ import {apiRequest} from '@shipfox/client-api';
7
+ import {keepPreviousData, useInfiniteQuery, useMutation, useQuery} from '@tanstack/react-query';
8
+
9
+ export const projectsQueryKeys = {
10
+ all: ['projects'] as const,
11
+ list: (workspaceId: string, search?: string) =>
12
+ [...projectsQueryKeys.all, 'list', workspaceId, search ?? ''] as const,
13
+ exists: (workspaceId: string) => [...projectsQueryKeys.all, 'exists', workspaceId] as const,
14
+ detail: (projectId: string) => [...projectsQueryKeys.all, 'detail', projectId] as const,
15
+ };
16
+
17
+ export async function listProjects({
18
+ workspaceId,
19
+ limit = 50,
20
+ cursor,
21
+ search,
22
+ signal,
23
+ }: {
24
+ workspaceId: string;
25
+ limit?: number;
26
+ cursor?: string | undefined;
27
+ search?: string | undefined;
28
+ signal?: AbortSignal;
29
+ }) {
30
+ const params = new URLSearchParams({workspace_id: workspaceId, limit: String(limit)});
31
+ if (cursor) params.set('cursor', cursor);
32
+ if (search) params.set('search', search);
33
+
34
+ return await apiRequest<ListProjectsResponseDto>(`/projects?${params.toString()}`, {signal});
35
+ }
36
+
37
+ export async function getProject(projectId: string) {
38
+ return await apiRequest<ProjectResponseDto>(`/projects/${projectId}`);
39
+ }
40
+
41
+ export async function createProject(body: CreateProjectBodyDto) {
42
+ return await apiRequest<ProjectResponseDto>('/projects', {method: 'POST', body});
43
+ }
44
+
45
+ export function useProjectsInfiniteQuery(
46
+ workspaceId: string | undefined,
47
+ search?: string,
48
+ limit = 50,
49
+ ) {
50
+ return useInfiniteQuery({
51
+ queryKey: workspaceId
52
+ ? projectsQueryKeys.list(workspaceId, search)
53
+ : [...projectsQueryKeys.all, 'list'],
54
+ enabled: Boolean(workspaceId),
55
+ initialPageParam: undefined as string | undefined,
56
+ queryFn: ({pageParam, signal}) =>
57
+ listProjects({workspaceId: workspaceId ?? '', limit, cursor: pageParam, search, signal}),
58
+ getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined,
59
+ placeholderData: keepPreviousData,
60
+ });
61
+ }
62
+
63
+ export function useProjectQuery(projectId: string | undefined) {
64
+ return useQuery({
65
+ queryKey: projectId
66
+ ? projectsQueryKeys.detail(projectId)
67
+ : [...projectsQueryKeys.all, 'detail'],
68
+ enabled: Boolean(projectId),
69
+ queryFn: () => getProject(projectId ?? ''),
70
+ });
71
+ }
72
+
73
+ export function useCreateProjectMutation() {
74
+ return useMutation({mutationFn: createProject});
75
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export type {WorkspaceSetupState} from '@shipfox/client-shell/runtime';
2
+ export {ProjectBreadcrumb, ProjectLayoutGuard} from './chrome.js';
3
+ export {ProjectCrumb, type ProjectCrumbProps} from './components/project-crumb.js';
4
+ export {ProjectSwitcher, type ProjectSwitcherProps} from './components/project-switcher.js';
5
+ export {
6
+ loadWorkspaceSetupRoute,
7
+ type WorkspaceSetupRouteOptions,
8
+ } from './components/workspace-setup-guard.js';
9
+ export * from './hooks/api/definitions.js';
10
+ export * from './hooks/api/projects.js';
11
+ export * from './pages/create-project-page.js';
12
+ export * from './pages/home-router.js';
13
+ export * from './pages/project-workflows-page.js';
14
+ export * from './pages/projects-hub-page.js';
15
+ export * from './project-error.js';