@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.
- package/.storybook/main.ts +1 -0
- package/.storybook/preview.css +7 -0
- package/.storybook/preview.tsx +56 -0
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +207 -0
- package/LICENSE +21 -0
- package/dist/chrome.d.ts +3 -0
- package/dist/chrome.d.ts.map +1 -0
- package/dist/chrome.js +42 -0
- package/dist/chrome.js.map +1 -0
- package/dist/components/model-provider-reminder-banner.d.ts +4 -0
- package/dist/components/model-provider-reminder-banner.d.ts.map +1 -0
- package/dist/components/model-provider-reminder-banner.js +73 -0
- package/dist/components/model-provider-reminder-banner.js.map +1 -0
- package/dist/components/project-crumb.d.ts +7 -0
- package/dist/components/project-crumb.d.ts.map +1 -0
- package/dist/components/project-crumb.js +93 -0
- package/dist/components/project-crumb.js.map +1 -0
- package/dist/components/project-switcher.d.ts +7 -0
- package/dist/components/project-switcher.d.ts.map +1 -0
- package/dist/components/project-switcher.js +138 -0
- package/dist/components/project-switcher.js.map +1 -0
- package/dist/components/source-strip.d.ts +14 -0
- package/dist/components/source-strip.d.ts.map +1 -0
- package/dist/components/source-strip.js +98 -0
- package/dist/components/source-strip.js.map +1 -0
- package/dist/components/workspace-setup-guard.d.ts +9 -0
- package/dist/components/workspace-setup-guard.d.ts.map +1 -0
- package/dist/components/workspace-setup-guard.js +160 -0
- package/dist/components/workspace-setup-guard.js.map +1 -0
- package/dist/feature.d.ts +35 -0
- package/dist/feature.d.ts.map +1 -0
- package/dist/feature.js +45 -0
- package/dist/feature.js.map +1 -0
- package/dist/hooks/api/definitions.d.ts +97 -0
- package/dist/hooks/api/definitions.d.ts.map +1 -0
- package/dist/hooks/api/definitions.js +55 -0
- package/dist/hooks/api/definitions.js.map +1 -0
- package/dist/hooks/api/projects.d.ts +93 -0
- package/dist/hooks/api/projects.d.ts.map +1 -0
- package/dist/hooks/api/projects.js +79 -0
- package/dist/hooks/api/projects.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/pages/create-project-page.d.ts +2 -0
- package/dist/pages/create-project-page.d.ts.map +1 -0
- package/dist/pages/create-project-page.js +406 -0
- package/dist/pages/create-project-page.js.map +1 -0
- package/dist/pages/create-project-page.stories.d.ts +24 -0
- package/dist/pages/create-project-page.stories.d.ts.map +1 -0
- package/dist/pages/create-project-page.stories.js +345 -0
- package/dist/pages/create-project-page.stories.js.map +1 -0
- package/dist/pages/home-router.d.ts +2 -0
- package/dist/pages/home-router.d.ts.map +1 -0
- package/dist/pages/home-router.js +7 -0
- package/dist/pages/home-router.js.map +1 -0
- package/dist/pages/project-workflows-page.d.ts +4 -0
- package/dist/pages/project-workflows-page.d.ts.map +1 -0
- package/dist/pages/project-workflows-page.js +481 -0
- package/dist/pages/project-workflows-page.js.map +1 -0
- package/dist/pages/projects-hub-page.d.ts +2 -0
- package/dist/pages/projects-hub-page.d.ts.map +1 -0
- package/dist/pages/projects-hub-page.js +256 -0
- package/dist/pages/projects-hub-page.js.map +1 -0
- package/dist/project-error.d.ts +7 -0
- package/dist/project-error.d.ts.map +1 -0
- package/dist/project-error.js +93 -0
- package/dist/project-error.js.map +1 -0
- package/dist/routes/create-project.d.ts +6 -0
- package/dist/routes/create-project.d.ts.map +1 -0
- package/dist/routes/create-project.js +7 -0
- package/dist/routes/create-project.js.map +1 -0
- package/dist/routes/home.d.ts +6 -0
- package/dist/routes/home.d.ts.map +1 -0
- package/dist/routes/home.js +7 -0
- package/dist/routes/home.js.map +1 -0
- package/dist/routes/project-index.d.ts +10 -0
- package/dist/routes/project-index.d.ts.map +1 -0
- package/dist/routes/project-index.js +15 -0
- package/dist/routes/project-index.js.map +1 -0
- package/dist/routes/workflows.d.ts +5 -0
- package/dist/routes/workflows.d.ts.map +1 -0
- package/dist/routes/workflows.js +16 -0
- package/dist/routes/workflows.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +120 -0
- package/src/chrome.tsx +26 -0
- package/src/components/model-provider-reminder-banner.tsx +78 -0
- package/src/components/project-crumb.tsx +70 -0
- package/src/components/project-switcher.test.tsx +91 -0
- package/src/components/project-switcher.tsx +127 -0
- package/src/components/source-strip.tsx +91 -0
- package/src/components/workspace-setup-guard.test.tsx +534 -0
- package/src/components/workspace-setup-guard.tsx +185 -0
- package/src/feature.ts +44 -0
- package/src/hooks/api/definitions.ts +50 -0
- package/src/hooks/api/projects.test.ts +78 -0
- package/src/hooks/api/projects.ts +75 -0
- package/src/index.ts +15 -0
- package/src/pages/create-project-page.stories.tsx +344 -0
- package/src/pages/create-project-page.test.tsx +317 -0
- package/src/pages/create-project-page.tsx +386 -0
- package/src/pages/home-router.test.tsx +27 -0
- package/src/pages/home-router.tsx +5 -0
- package/src/pages/project-workflows-page.test.tsx +258 -0
- package/src/pages/project-workflows-page.tsx +445 -0
- package/src/pages/projects-hub-page.test.tsx +313 -0
- package/src/pages/projects-hub-page.tsx +246 -0
- package/src/project-error.test.ts +59 -0
- package/src/project-error.ts +106 -0
- package/src/routes/create-project.tsx +4 -0
- package/src/routes/home.tsx +4 -0
- package/src/routes/project-index.tsx +11 -0
- package/src/routes/workflows.tsx +10 -0
- package/test/pages.tsx +151 -0
- package/test/setup.ts +3 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- 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';
|