@shipfox/client-projects 5.0.0 → 6.0.1
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +57 -0
- package/dist/chrome.d.ts.map +1 -1
- package/dist/chrome.js +7 -10
- package/dist/chrome.js.map +1 -1
- package/dist/components/model-provider-reminder-banner.d.ts.map +1 -1
- package/dist/components/model-provider-reminder-banner.js +1 -1
- package/dist/components/model-provider-reminder-banner.js.map +1 -1
- package/dist/components/source-strip.d.ts +2 -2
- package/dist/components/source-strip.d.ts.map +1 -1
- package/dist/components/source-strip.js +2 -2
- package/dist/components/source-strip.js.map +1 -1
- package/dist/core/definition.d.ts +32 -0
- package/dist/core/definition.d.ts.map +1 -0
- package/dist/core/definition.js +3 -0
- package/dist/core/definition.js.map +1 -0
- package/dist/core/project.d.ts +26 -0
- package/dist/core/project.d.ts.map +1 -0
- package/dist/core/project.js +12 -0
- package/dist/core/project.js.map +1 -0
- package/dist/feature.d.ts +8 -0
- package/dist/feature.d.ts.map +1 -1
- package/dist/feature.js +11 -10
- package/dist/feature.js.map +1 -1
- package/dist/hooks/api/definitions.d.ts +12 -88
- package/dist/hooks/api/definitions.d.ts.map +1 -1
- package/dist/hooks/api/definitions.js +18 -10
- package/dist/hooks/api/definitions.js.map +1 -1
- package/dist/hooks/api/mappers.d.ts +17 -0
- package/dist/hooks/api/mappers.d.ts.map +1 -0
- package/dist/hooks/api/mappers.js +56 -0
- package/dist/hooks/api/mappers.js.map +1 -0
- package/dist/hooks/api/projects.d.ts +18 -84
- package/dist/hooks/api/projects.d.ts.map +1 -1
- package/dist/hooks/api/projects.js +72 -29
- package/dist/hooks/api/projects.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pages/create-project-page.d.ts.map +1 -1
- package/dist/pages/create-project-page.js +12 -37
- package/dist/pages/create-project-page.js.map +1 -1
- package/dist/pages/create-project-page.stories.js +3 -1
- package/dist/pages/create-project-page.stories.js.map +1 -1
- package/dist/pages/home-router.d.ts +4 -1
- package/dist/pages/home-router.d.ts.map +1 -1
- package/dist/pages/home-router.js +4 -2
- package/dist/pages/home-router.js.map +1 -1
- package/dist/pages/projects-hub-page.d.ts +3 -1
- package/dist/pages/projects-hub-page.d.ts.map +1 -1
- package/dist/pages/projects-hub-page.js +26 -30
- package/dist/pages/projects-hub-page.js.map +1 -1
- package/dist/routes/home.d.ts +3 -2
- package/dist/routes/home.d.ts.map +1 -1
- package/dist/routes/home.js +7 -2
- package/dist/routes/home.js.map +1 -1
- package/dist/routes/inputs.d.ts +6 -0
- package/dist/routes/inputs.d.ts.map +1 -0
- package/dist/routes/inputs.js +14 -0
- package/dist/routes/inputs.js.map +1 -0
- package/dist/routes/search.d.ts +5 -0
- package/dist/routes/search.d.ts.map +1 -0
- package/dist/routes/search.js +8 -0
- package/dist/routes/search.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +14 -15
- package/src/chrome.tsx +11 -6
- package/src/components/model-provider-reminder-banner.tsx +1 -3
- package/src/components/project-switcher.test.tsx +2 -1
- package/src/components/source-strip.tsx +5 -5
- package/src/core/definition.ts +31 -0
- package/src/core/project.ts +47 -0
- package/src/feature.ts +13 -11
- package/src/hooks/api/definitions.ts +54 -19
- package/src/hooks/api/mappers.ts +67 -0
- package/src/hooks/api/projects.test.ts +11 -4
- package/src/hooks/api/projects.ts +114 -39
- package/src/index.ts +9 -0
- package/src/pages/create-project-page.stories.tsx +6 -1
- package/src/pages/create-project-page.test.tsx +3 -1
- package/src/pages/create-project-page.tsx +16 -51
- package/src/pages/home-router.tsx +3 -2
- package/src/pages/projects-hub-page.test.tsx +13 -8
- package/src/pages/projects-hub-page.tsx +24 -34
- package/src/routes/home.tsx +6 -2
- package/src/routes/inputs.test.ts +14 -0
- package/src/routes/inputs.ts +15 -0
- package/src/routes/search.ts +8 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,32 +1,55 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
createProjectBodySchema,
|
|
3
|
+
listProjectsResponseSchema,
|
|
4
|
+
projectResponseSchema,
|
|
5
5
|
} from '@shipfox/api-projects-dto';
|
|
6
|
-
import {
|
|
6
|
+
import {checkedApiRequest} from '@shipfox/client-api';
|
|
7
7
|
import {
|
|
8
|
-
type
|
|
8
|
+
type InfiniteData,
|
|
9
9
|
keepPreviousData,
|
|
10
10
|
queryOptions,
|
|
11
|
+
type UseInfiniteQueryOptions,
|
|
12
|
+
type UseQueryOptions,
|
|
11
13
|
useInfiniteQuery,
|
|
12
14
|
useMutation,
|
|
13
15
|
useQuery,
|
|
16
|
+
useQueryClient,
|
|
14
17
|
} from '@tanstack/react-query';
|
|
18
|
+
import type {CreateProjectCommand, Project, ProjectList} from '#core/project.js';
|
|
19
|
+
import {toProject, toProjectList} from './mappers.js';
|
|
15
20
|
|
|
16
21
|
export const projectsQueryKeys = {
|
|
17
22
|
all: ['projects'] as const,
|
|
18
|
-
list: (workspaceId: string, search
|
|
19
|
-
[...projectsQueryKeys.all, 'list', workspaceId, search
|
|
23
|
+
list: (workspaceId: string, search = '') =>
|
|
24
|
+
[...projectsQueryKeys.all, 'list', workspaceId, search] as const,
|
|
20
25
|
exists: (workspaceId: string) => [...projectsQueryKeys.all, 'exists', workspaceId] as const,
|
|
21
26
|
detail: (projectId: string) => [...projectsQueryKeys.all, 'detail', projectId] as const,
|
|
22
27
|
};
|
|
23
28
|
|
|
24
|
-
type
|
|
25
|
-
|
|
29
|
+
type ProjectListQueryKey =
|
|
30
|
+
| ReturnType<typeof projectsQueryKeys.list>
|
|
31
|
+
| readonly ['projects', 'list'];
|
|
32
|
+
type ProjectExistenceQueryKey =
|
|
33
|
+
| ReturnType<typeof projectsQueryKeys.exists>
|
|
34
|
+
| readonly ['projects', 'exists'];
|
|
35
|
+
type ProjectDetailQueryKey =
|
|
36
|
+
| ReturnType<typeof projectsQueryKeys.detail>
|
|
37
|
+
| readonly ['projects', 'detail'];
|
|
38
|
+
|
|
39
|
+
type ProjectListInfiniteQueryOptions = UseInfiniteQueryOptions<
|
|
40
|
+
ProjectList,
|
|
41
|
+
Error,
|
|
42
|
+
InfiniteData<ProjectList, string | undefined>,
|
|
43
|
+
ProjectListQueryKey,
|
|
44
|
+
string | undefined
|
|
45
|
+
>;
|
|
46
|
+
type ProjectExistenceQueryOptions = UseQueryOptions<
|
|
47
|
+
ProjectList,
|
|
26
48
|
Error,
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
ProjectList,
|
|
50
|
+
ProjectExistenceQueryKey
|
|
29
51
|
>;
|
|
52
|
+
type ProjectDetailQueryOptions = UseQueryOptions<Project, Error, Project, ProjectDetailQueryKey>;
|
|
30
53
|
|
|
31
54
|
export async function listProjects({
|
|
32
55
|
workspaceId,
|
|
@@ -39,59 +62,111 @@ export async function listProjects({
|
|
|
39
62
|
limit?: number;
|
|
40
63
|
cursor?: string | undefined;
|
|
41
64
|
search?: string | undefined;
|
|
42
|
-
signal?: AbortSignal;
|
|
43
|
-
}) {
|
|
65
|
+
signal?: AbortSignal | undefined;
|
|
66
|
+
}): Promise<ProjectList> {
|
|
44
67
|
const params = new URLSearchParams({workspace_id: workspaceId, limit: String(limit)});
|
|
45
68
|
if (cursor) params.set('cursor', cursor);
|
|
46
69
|
if (search) params.set('search', search);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
export async function getProject(projectId: string) {
|
|
52
|
-
return await apiRequest<ProjectResponseDto>(`/projects/${projectId}`);
|
|
70
|
+
return toProjectList(
|
|
71
|
+
await checkedApiRequest(listProjectsResponseSchema, `/projects?${params.toString()}`, {signal}),
|
|
72
|
+
);
|
|
53
73
|
}
|
|
54
74
|
|
|
55
|
-
export async function
|
|
56
|
-
return await
|
|
75
|
+
export async function getProject(projectId: string): Promise<Project> {
|
|
76
|
+
return toProject(await checkedApiRequest(projectResponseSchema, `/projects/${projectId}`));
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
export function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
79
|
+
export async function createProject(command: CreateProjectCommand): Promise<Project> {
|
|
80
|
+
const body = createProjectBodySchema.parse({
|
|
81
|
+
workspace_id: command.workspaceId,
|
|
82
|
+
name: command.name,
|
|
83
|
+
source: {
|
|
84
|
+
connection_id: command.source.connectionId,
|
|
85
|
+
external_repository_id: command.source.externalRepositoryId,
|
|
86
|
+
},
|
|
64
87
|
});
|
|
88
|
+
return toProject(
|
|
89
|
+
await checkedApiRequest(projectResponseSchema, '/projects', {method: 'POST', body}),
|
|
90
|
+
);
|
|
65
91
|
}
|
|
66
92
|
|
|
67
|
-
export function
|
|
93
|
+
export function projectsInfiniteQueryOptions(
|
|
68
94
|
workspaceId: string | undefined,
|
|
69
95
|
search?: string,
|
|
70
96
|
limit = 50,
|
|
71
|
-
) {
|
|
72
|
-
|
|
97
|
+
): ProjectListInfiniteQueryOptions {
|
|
98
|
+
const normalizedSearch = search?.trim() ?? '';
|
|
99
|
+
return {
|
|
73
100
|
queryKey: workspaceId
|
|
74
|
-
? projectsQueryKeys.list(workspaceId,
|
|
75
|
-
: [...projectsQueryKeys.all, 'list'],
|
|
101
|
+
? projectsQueryKeys.list(workspaceId, normalizedSearch)
|
|
102
|
+
: ([...projectsQueryKeys.all, 'list'] as const),
|
|
76
103
|
enabled: Boolean(workspaceId),
|
|
77
104
|
initialPageParam: undefined as string | undefined,
|
|
78
|
-
queryFn: ({pageParam, signal}) =>
|
|
79
|
-
listProjects({
|
|
80
|
-
|
|
105
|
+
queryFn: ({pageParam, signal}: {pageParam: string | undefined; signal: AbortSignal}) =>
|
|
106
|
+
listProjects({
|
|
107
|
+
workspaceId: workspaceId ?? '',
|
|
108
|
+
limit,
|
|
109
|
+
cursor: pageParam,
|
|
110
|
+
search: normalizedSearch || undefined,
|
|
111
|
+
signal,
|
|
112
|
+
}),
|
|
113
|
+
getNextPageParam: (lastPage: ProjectList) => lastPage.nextCursor ?? undefined,
|
|
81
114
|
placeholderData: keepPreviousData,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function projectExistenceQueryOptions(
|
|
119
|
+
workspaceId: string | undefined,
|
|
120
|
+
): ProjectExistenceQueryOptions {
|
|
121
|
+
return queryOptions({
|
|
122
|
+
queryKey: workspaceId
|
|
123
|
+
? projectsQueryKeys.exists(workspaceId)
|
|
124
|
+
: ([...projectsQueryKeys.all, 'exists'] as const),
|
|
125
|
+
enabled: Boolean(workspaceId),
|
|
126
|
+
queryFn: ({signal}) => listProjects({workspaceId: workspaceId ?? '', limit: 1, signal}),
|
|
127
|
+
staleTime: 30_000,
|
|
82
128
|
});
|
|
83
129
|
}
|
|
84
130
|
|
|
85
|
-
export function
|
|
86
|
-
return
|
|
131
|
+
export function projectQueryOptions(projectId: string | undefined): ProjectDetailQueryOptions {
|
|
132
|
+
return queryOptions({
|
|
87
133
|
queryKey: projectId
|
|
88
134
|
? projectsQueryKeys.detail(projectId)
|
|
89
|
-
: [...projectsQueryKeys.all, 'detail'],
|
|
135
|
+
: ([...projectsQueryKeys.all, 'detail'] as const),
|
|
90
136
|
enabled: Boolean(projectId),
|
|
91
137
|
queryFn: () => getProject(projectId ?? ''),
|
|
92
138
|
});
|
|
93
139
|
}
|
|
94
140
|
|
|
141
|
+
export function useProjectsInfiniteQuery(
|
|
142
|
+
workspaceId: string | undefined,
|
|
143
|
+
search?: string,
|
|
144
|
+
limit = 50,
|
|
145
|
+
) {
|
|
146
|
+
return useInfiniteQuery(projectsInfiniteQueryOptions(workspaceId, search, limit));
|
|
147
|
+
}
|
|
148
|
+
export function useProjectQuery(projectId: string | undefined) {
|
|
149
|
+
return useQuery(projectQueryOptions(projectId));
|
|
150
|
+
}
|
|
151
|
+
|
|
95
152
|
export function useCreateProjectMutation() {
|
|
96
|
-
|
|
153
|
+
const queryClient = useQueryClient();
|
|
154
|
+
return useMutation({
|
|
155
|
+
mutationFn: createProject,
|
|
156
|
+
onSuccess: async (project) => {
|
|
157
|
+
queryClient.setQueryData(projectsQueryKeys.detail(project.id), project);
|
|
158
|
+
queryClient.setQueryData<ProjectList>(projectsQueryKeys.exists(project.workspaceId), {
|
|
159
|
+
projects: [project],
|
|
160
|
+
nextCursor: null,
|
|
161
|
+
});
|
|
162
|
+
await queryClient.invalidateQueries({queryKey: projectsQueryKeys.list(project.workspaceId)});
|
|
163
|
+
},
|
|
164
|
+
onError: async (_error, command) => {
|
|
165
|
+
await queryClient.invalidateQueries({
|
|
166
|
+
queryKey: projectsQueryKeys.exists(command.workspaceId),
|
|
167
|
+
refetchType: 'active',
|
|
168
|
+
});
|
|
169
|
+
await queryClient.invalidateQueries({queryKey: projectsQueryKeys.list(command.workspaceId)});
|
|
170
|
+
},
|
|
171
|
+
});
|
|
97
172
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export type {Definition, DefinitionList, DefinitionSyncSummary} from '#core/definition.js';
|
|
2
|
+
export {
|
|
3
|
+
type CreateProjectCommand,
|
|
4
|
+
type Project,
|
|
5
|
+
type ProjectList,
|
|
6
|
+
type ProjectSource,
|
|
7
|
+
projectNameFromRepository,
|
|
8
|
+
selectProjectSource,
|
|
9
|
+
} from '#core/project.js';
|
|
1
10
|
export {ProjectBreadcrumb, ProjectLayoutGuard} from './chrome.js';
|
|
2
11
|
export {ProjectCrumb, type ProjectCrumbProps} from './components/project-crumb.js';
|
|
3
12
|
export {ProjectSwitcher, type ProjectSwitcherProps} from './components/project-switcher.js';
|
|
@@ -150,7 +150,11 @@ function fetchForScenario(scenario: Scenario): typeof fetch {
|
|
|
150
150
|
const method = input instanceof Request ? input.method : (init?.method ?? 'GET');
|
|
151
151
|
if (url.pathname === `/workspaces/${WORKSPACE_ID}/agent/model-providers`) {
|
|
152
152
|
return Promise.resolve(
|
|
153
|
-
jsonResponse({
|
|
153
|
+
jsonResponse({
|
|
154
|
+
configs: [modelProviderConfig()],
|
|
155
|
+
default_provider_id: 'openai',
|
|
156
|
+
default_harness_id: null,
|
|
157
|
+
}),
|
|
154
158
|
);
|
|
155
159
|
}
|
|
156
160
|
if (url.pathname === '/integration-connections') {
|
|
@@ -312,6 +316,7 @@ function repository(overrides: Partial<RepositoryDto>): RepositoryDto {
|
|
|
312
316
|
|
|
313
317
|
function modelProviderConfig() {
|
|
314
318
|
return {
|
|
319
|
+
kind: 'builtin',
|
|
315
320
|
workspace_id: WORKSPACE_ID,
|
|
316
321
|
provider_id: 'openai',
|
|
317
322
|
default_model: 'gpt-5.5-pro',
|
|
@@ -163,7 +163,9 @@ describe('CreateProjectPage', () => {
|
|
|
163
163
|
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
164
164
|
const request = input as Request;
|
|
165
165
|
if (request.url.endsWith('/agent/model-providers')) {
|
|
166
|
-
return Promise.resolve(
|
|
166
|
+
return Promise.resolve(
|
|
167
|
+
jsonResponse({configs: [], default_provider_id: null, default_harness_id: null}),
|
|
168
|
+
);
|
|
167
169
|
}
|
|
168
170
|
if (request.url.includes('/integration-connections?')) {
|
|
169
171
|
return Promise.resolve(jsonResponse({connections: [connectionDto()]}));
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
createProjectBodySchema,
|
|
4
|
-
type ListProjectsResponseDto,
|
|
5
|
-
type ProjectResponseDto,
|
|
6
|
-
} from '@shipfox/api-projects-dto';
|
|
1
|
+
import {createProjectBodySchema} from '@shipfox/api-projects-dto';
|
|
7
2
|
import {useMaybeActiveWorkspace} from '@shipfox/client-auth';
|
|
8
3
|
import {
|
|
9
4
|
ConnectionPicker,
|
|
5
|
+
type IntegrationConnection,
|
|
10
6
|
IntegrationIcon,
|
|
11
7
|
RepositoryPicker,
|
|
12
8
|
useRepositoriesInfiniteQuery,
|
|
@@ -20,24 +16,21 @@ import {FullPageLoader} from '@shipfox/react-ui/loader';
|
|
|
20
16
|
import {toast} from '@shipfox/react-ui/toast';
|
|
21
17
|
import {Header, Text} from '@shipfox/react-ui/typography';
|
|
22
18
|
import {useForm} from '@tanstack/react-form';
|
|
23
|
-
import {type QueryClient, useQueryClient} from '@tanstack/react-query';
|
|
24
19
|
import {Link, Navigate, useNavigate} from '@tanstack/react-router';
|
|
25
20
|
import {useEffect, useRef, useState} from 'react';
|
|
26
21
|
import {ModelProviderReminderBanner} from '#components/model-provider-reminder-banner.js';
|
|
27
|
-
import {
|
|
22
|
+
import {type CreateProjectCommand, projectNameFromRepository} from '#core/project.js';
|
|
23
|
+
import {useCreateProjectMutation} from '#hooks/api/projects.js';
|
|
28
24
|
import {projectErrorCopy} from '#project-error.js';
|
|
29
25
|
|
|
30
|
-
const REPOSITORY_NAME_SPLIT_RE = /[/-]/;
|
|
31
|
-
|
|
32
26
|
export function CreateProjectPage() {
|
|
33
27
|
const workspace = useMaybeActiveWorkspace();
|
|
34
|
-
const queryClient = useQueryClient();
|
|
35
28
|
const navigate = useNavigate();
|
|
36
29
|
const createProject = useCreateProjectMutation();
|
|
37
30
|
const errorRef = useRef<HTMLDivElement>(null);
|
|
38
31
|
|
|
39
32
|
const connectionsQuery = useSourceConnectionsQuery(workspace?.id);
|
|
40
|
-
const connections = connectionsQuery.data
|
|
33
|
+
const connections = connectionsQuery.data ?? [];
|
|
41
34
|
|
|
42
35
|
const [selectedConnectionId, setSelectedConnectionId] = useState<string | undefined>();
|
|
43
36
|
const singleConnectionId = connections.length === 1 ? connections[0]?.id : undefined;
|
|
@@ -48,7 +41,7 @@ export function CreateProjectPage() {
|
|
|
48
41
|
}
|
|
49
42
|
}, [singleConnectionId, selectedConnectionId]);
|
|
50
43
|
|
|
51
|
-
const selectedConnection:
|
|
44
|
+
const selectedConnection: IntegrationConnection | undefined = connections.find(
|
|
52
45
|
(connection) => connection.id === effectiveSelectedConnectionId,
|
|
53
46
|
);
|
|
54
47
|
|
|
@@ -65,11 +58,11 @@ export function CreateProjectPage() {
|
|
|
65
58
|
const [selectedRepositoryId, setSelectedRepositoryId] = useState<string | undefined>();
|
|
66
59
|
useEffect(() => {
|
|
67
60
|
if (!selectedRepositoryId && repositories[0]) {
|
|
68
|
-
setSelectedRepositoryId(repositories[0].
|
|
61
|
+
setSelectedRepositoryId(repositories[0].externalRepositoryId);
|
|
69
62
|
}
|
|
70
63
|
}, [repositories, selectedRepositoryId]);
|
|
71
64
|
const selectedRepository = repositories.find(
|
|
72
|
-
(repository) => repository.
|
|
65
|
+
(repository) => repository.externalRepositoryId === selectedRepositoryId,
|
|
73
66
|
);
|
|
74
67
|
|
|
75
68
|
const [nameTouched, setNameTouched] = useState(false);
|
|
@@ -128,18 +121,15 @@ export function CreateProjectPage() {
|
|
|
128
121
|
}
|
|
129
122
|
|
|
130
123
|
try {
|
|
131
|
-
const
|
|
132
|
-
|
|
124
|
+
const command: CreateProjectCommand = {
|
|
125
|
+
workspaceId: workspace.id,
|
|
133
126
|
name: projectName,
|
|
134
127
|
source: {
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
connectionId: selectedConnection.id,
|
|
129
|
+
externalRepositoryId: selectedRepository.externalRepositoryId,
|
|
137
130
|
},
|
|
138
|
-
}
|
|
139
|
-
const project = await createProject.mutateAsync(
|
|
140
|
-
setWorkspaceProjectExists(queryClient, workspace.id, project);
|
|
141
|
-
queryClient.setQueryData(projectsQueryKeys.detail(project.id), project);
|
|
142
|
-
await queryClient.invalidateQueries({queryKey: projectsQueryKeys.list(workspace.id)});
|
|
131
|
+
};
|
|
132
|
+
const project = await createProject.mutateAsync(command);
|
|
143
133
|
toast.success('Project created.');
|
|
144
134
|
await navigate({
|
|
145
135
|
to: '/workspaces/$wid/projects/$pid',
|
|
@@ -148,11 +138,6 @@ export function CreateProjectPage() {
|
|
|
148
138
|
} catch (error) {
|
|
149
139
|
const copy = projectErrorCopy(error);
|
|
150
140
|
if (copy.existingProjectId) {
|
|
151
|
-
await queryClient.invalidateQueries({
|
|
152
|
-
queryKey: projectsQueryKeys.exists(workspace.id),
|
|
153
|
-
refetchType: 'active',
|
|
154
|
-
});
|
|
155
|
-
await queryClient.invalidateQueries({queryKey: projectsQueryKeys.list(workspace.id)});
|
|
156
141
|
toast.info('Project already exists.');
|
|
157
142
|
await navigate({
|
|
158
143
|
to: '/workspaces/$wid/projects/$pid',
|
|
@@ -284,7 +269,7 @@ export function CreateProjectPage() {
|
|
|
284
269
|
|
|
285
270
|
<ProjectSummary
|
|
286
271
|
connection={selectedConnection}
|
|
287
|
-
repositoryName={selectedRepository?.
|
|
272
|
+
repositoryName={selectedRepository?.fullName}
|
|
288
273
|
/>
|
|
289
274
|
|
|
290
275
|
<form.Field
|
|
@@ -333,7 +318,7 @@ function ProjectSummary({
|
|
|
333
318
|
connection,
|
|
334
319
|
repositoryName,
|
|
335
320
|
}: {
|
|
336
|
-
connection:
|
|
321
|
+
connection: IntegrationConnection | undefined;
|
|
337
322
|
repositoryName: string | undefined;
|
|
338
323
|
}) {
|
|
339
324
|
return (
|
|
@@ -356,15 +341,6 @@ function ProjectSummary({
|
|
|
356
341
|
);
|
|
357
342
|
}
|
|
358
343
|
|
|
359
|
-
function projectNameFromRepository(repositoryId: string): string {
|
|
360
|
-
return repositoryId
|
|
361
|
-
.trim()
|
|
362
|
-
.split(REPOSITORY_NAME_SPLIT_RE)
|
|
363
|
-
.filter(Boolean)
|
|
364
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
365
|
-
.join(' ');
|
|
366
|
-
}
|
|
367
|
-
|
|
368
344
|
function useDebouncedValue<T>(value: T, delayMs: number): T {
|
|
369
345
|
const [debounced, setDebounced] = useState(value);
|
|
370
346
|
useEffect(() => {
|
|
@@ -373,14 +349,3 @@ function useDebouncedValue<T>(value: T, delayMs: number): T {
|
|
|
373
349
|
}, [value, delayMs]);
|
|
374
350
|
return debounced;
|
|
375
351
|
}
|
|
376
|
-
|
|
377
|
-
function setWorkspaceProjectExists(
|
|
378
|
-
queryClient: QueryClient,
|
|
379
|
-
workspaceId: string,
|
|
380
|
-
project: ProjectResponseDto,
|
|
381
|
-
) {
|
|
382
|
-
queryClient.setQueryData<ListProjectsResponseDto>(projectsQueryKeys.exists(workspaceId), {
|
|
383
|
-
projects: [project],
|
|
384
|
-
next_cursor: null,
|
|
385
|
-
});
|
|
386
|
-
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {ProjectsHubPage} from '#pages/projects-hub-page.js';
|
|
2
|
+
import type {ProjectsSearch} from '#routes/search.js';
|
|
2
3
|
|
|
3
|
-
export function HomeRouter() {
|
|
4
|
-
return <ProjectsHubPage />;
|
|
4
|
+
export function HomeRouter({search = {}}: {search?: ProjectsSearch}) {
|
|
5
|
+
return <ProjectsHubPage search={search.search ?? ''} />;
|
|
5
6
|
}
|
|
@@ -40,7 +40,11 @@ describe('ProjectsHubPage', () => {
|
|
|
40
40
|
test('shows and dismisses the model provider reminder when no provider is configured', async () => {
|
|
41
41
|
const fetchImpl = createHubFetch({
|
|
42
42
|
projects: jsonResponse({projects: [], next_cursor: null}),
|
|
43
|
-
modelProviders: jsonResponse({
|
|
43
|
+
modelProviders: jsonResponse({
|
|
44
|
+
configs: [],
|
|
45
|
+
default_provider_id: null,
|
|
46
|
+
default_harness_id: null,
|
|
47
|
+
}),
|
|
44
48
|
});
|
|
45
49
|
configureApiClient({fetchImpl});
|
|
46
50
|
|
|
@@ -84,14 +88,14 @@ describe('ProjectsHubPage', () => {
|
|
|
84
88
|
if (url.searchParams.get('cursor') === 'cursor-1') {
|
|
85
89
|
return Promise.resolve(
|
|
86
90
|
jsonResponse({
|
|
87
|
-
projects: [projectDto({id: '
|
|
91
|
+
projects: [projectDto({id: '11111111-1111-4111-8111-111111111113', name: 'API'})],
|
|
88
92
|
next_cursor: null,
|
|
89
93
|
}),
|
|
90
94
|
);
|
|
91
95
|
}
|
|
92
96
|
return Promise.resolve(
|
|
93
97
|
jsonResponse({
|
|
94
|
-
projects: [projectDto({id: '
|
|
98
|
+
projects: [projectDto({id: '11111111-1111-4111-8111-111111111112', name: 'Platform'})],
|
|
95
99
|
next_cursor: 'cursor-1',
|
|
96
100
|
}),
|
|
97
101
|
);
|
|
@@ -132,7 +136,7 @@ describe('ProjectsHubPage', () => {
|
|
|
132
136
|
projects: jsonResponse({
|
|
133
137
|
projects: [
|
|
134
138
|
projectDto({
|
|
135
|
-
id: '
|
|
139
|
+
id: '11111111-1111-4111-8111-111111111112',
|
|
136
140
|
name: 'Platform',
|
|
137
141
|
externalRepositoryId: 'github:octo/platform',
|
|
138
142
|
}),
|
|
@@ -171,7 +175,7 @@ describe('ProjectsHubPage', () => {
|
|
|
171
175
|
configureApiClient({
|
|
172
176
|
fetchImpl: createHubFetch({
|
|
173
177
|
projects: jsonResponse({
|
|
174
|
-
projects: [projectDto({id: '
|
|
178
|
+
projects: [projectDto({id: '11111111-1111-4111-8111-111111111112', name: 'Platform'})],
|
|
175
179
|
next_cursor: null,
|
|
176
180
|
}),
|
|
177
181
|
connections: jsonResponse(connectionsDto({lifecycleStatus})),
|
|
@@ -190,7 +194,7 @@ describe('ProjectsHubPage', () => {
|
|
|
190
194
|
configureApiClient({
|
|
191
195
|
fetchImpl: createHubFetch({
|
|
192
196
|
projects: jsonResponse({
|
|
193
|
-
projects: [projectDto({id: '
|
|
197
|
+
projects: [projectDto({id: '11111111-1111-4111-8111-111111111112', name: 'Platform'})],
|
|
194
198
|
next_cursor: null,
|
|
195
199
|
}),
|
|
196
200
|
connections: jsonResponse({code: 'server-error'}, {status: 500}),
|
|
@@ -218,7 +222,7 @@ describe('ProjectsHubPage', () => {
|
|
|
218
222
|
|
|
219
223
|
function createHubFetch({
|
|
220
224
|
projects = jsonResponse({
|
|
221
|
-
projects: [projectDto({id: '
|
|
225
|
+
projects: [projectDto({id: '11111111-1111-4111-8111-111111111112', name: 'Platform'})],
|
|
222
226
|
next_cursor: null,
|
|
223
227
|
}),
|
|
224
228
|
connections = jsonResponse(connectionsDto()),
|
|
@@ -276,14 +280,15 @@ function modelProviderConfigsDto() {
|
|
|
276
280
|
return {
|
|
277
281
|
configs: [
|
|
278
282
|
{
|
|
283
|
+
kind: 'builtin',
|
|
279
284
|
provider_id: 'anthropic',
|
|
280
285
|
default_model: null,
|
|
281
|
-
key_fingerprints: {'credential:api_key': '...abcd'},
|
|
282
286
|
created_at: new Date().toISOString(),
|
|
283
287
|
updated_at: new Date().toISOString(),
|
|
284
288
|
},
|
|
285
289
|
],
|
|
286
290
|
default_provider_id: 'anthropic',
|
|
291
|
+
default_harness_id: null,
|
|
287
292
|
};
|
|
288
293
|
}
|
|
289
294
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type {IntegrationConnectionDto} from '@shipfox/api-integration-core-dto';
|
|
2
|
-
import type {ProjectResponseDto} from '@shipfox/api-projects-dto';
|
|
3
1
|
import {useActiveWorkspace} from '@shipfox/client-auth';
|
|
4
2
|
import {
|
|
5
3
|
ConnectionStatusBadge,
|
|
4
|
+
type IntegrationConnection,
|
|
6
5
|
IntegrationIcon,
|
|
7
6
|
useIntegrationConnectionsQuery,
|
|
8
7
|
} from '@shipfox/client-integrations';
|
|
@@ -15,17 +14,15 @@ import {Icon} from '@shipfox/react-ui/icon';
|
|
|
15
14
|
import {Input} from '@shipfox/react-ui/input';
|
|
16
15
|
import {Skeleton} from '@shipfox/react-ui/skeleton';
|
|
17
16
|
import {Header, Text} from '@shipfox/react-ui/typography';
|
|
18
|
-
import {Link} from '@tanstack/react-router';
|
|
19
|
-
import {useEffect, useState} from 'react';
|
|
17
|
+
import {Link, useNavigate} from '@tanstack/react-router';
|
|
20
18
|
import {ModelProviderReminderBanner} from '#components/model-provider-reminder-banner.js';
|
|
19
|
+
import type {Project} from '#core/project.js';
|
|
21
20
|
import {useProjectsInfiniteQuery} from '#hooks/api/projects.js';
|
|
22
21
|
|
|
23
|
-
export function ProjectsHubPage() {
|
|
22
|
+
export function ProjectsHubPage({search = ''}: {search?: string}) {
|
|
24
23
|
const workspace = useActiveWorkspace();
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const debouncedSearch = useDebouncedValue(trimmedInput, 250);
|
|
28
|
-
const query = useProjectsInfiniteQuery(workspace.id, debouncedSearch || undefined);
|
|
24
|
+
const navigate = useNavigate();
|
|
25
|
+
const query = useProjectsInfiniteQuery(workspace.id, search || undefined);
|
|
29
26
|
const projects = query.data?.pages.flatMap((page) => page.projects) ?? [];
|
|
30
27
|
|
|
31
28
|
// The provider logo and connection health live on the integration connection,
|
|
@@ -35,13 +32,11 @@ export function ProjectsHubPage() {
|
|
|
35
32
|
projects.length > 0 ? workspace.id : undefined,
|
|
36
33
|
);
|
|
37
34
|
const connectionsById = new Map(
|
|
38
|
-
(connectionsQuery.data
|
|
35
|
+
(connectionsQuery.data ?? []).map((connection) => [connection.id, connection]),
|
|
39
36
|
);
|
|
40
37
|
|
|
41
38
|
const isInitialLoading = query.isPending;
|
|
42
|
-
const
|
|
43
|
-
const isSearching =
|
|
44
|
-
Boolean(trimmedInput) && (isDebouncePending || (query.isFetching && !query.isFetchingNextPage));
|
|
39
|
+
const isSearching = Boolean(search) && query.isFetching && !query.isFetchingNextPage;
|
|
45
40
|
const hasNoData = !query.data;
|
|
46
41
|
|
|
47
42
|
return (
|
|
@@ -53,8 +48,11 @@ export function ProjectsHubPage() {
|
|
|
53
48
|
<div className="flex items-center gap-12 max-[640px]:flex-col max-[640px]:items-stretch">
|
|
54
49
|
<Input
|
|
55
50
|
type="search"
|
|
56
|
-
value={
|
|
57
|
-
onChange={(event) =>
|
|
51
|
+
value={search}
|
|
52
|
+
onChange={(event) => {
|
|
53
|
+
const next = event.target.value.trim();
|
|
54
|
+
navigate({search: (next ? {search: next} : {}) as never, replace: true});
|
|
55
|
+
}}
|
|
58
56
|
placeholder="Search projects…"
|
|
59
57
|
aria-label="Search projects"
|
|
60
58
|
className="flex-1"
|
|
@@ -80,18 +78,19 @@ export function ProjectsHubPage() {
|
|
|
80
78
|
|
|
81
79
|
<ModelProviderReminderBanner workspaceId={workspace.id} />
|
|
82
80
|
|
|
83
|
-
{isInitialLoading || (
|
|
84
|
-
<ProjectsSkeleton />
|
|
85
|
-
) : null}
|
|
81
|
+
{isInitialLoading || (search && hasNoData && query.isFetching) ? <ProjectsSkeleton /> : null}
|
|
86
82
|
|
|
87
83
|
{query.isError && hasNoData ? <QueryLoadError query={query} subject="projects" /> : null}
|
|
88
84
|
|
|
89
|
-
{!isInitialLoading && !query.isError && projects.length === 0 && !
|
|
85
|
+
{!isInitialLoading && !query.isError && projects.length === 0 && !search ? (
|
|
90
86
|
<EmptyProjects workspaceId={workspace.id} />
|
|
91
87
|
) : null}
|
|
92
88
|
|
|
93
|
-
{!query.isFetching && !query.isError && projects.length === 0 &&
|
|
94
|
-
<NoSearchResults
|
|
89
|
+
{!query.isFetching && !query.isError && projects.length === 0 && search ? (
|
|
90
|
+
<NoSearchResults
|
|
91
|
+
search={search}
|
|
92
|
+
onClear={() => navigate({search: {} as never, replace: true})}
|
|
93
|
+
/>
|
|
95
94
|
) : null}
|
|
96
95
|
|
|
97
96
|
{projects.length > 0 ? (
|
|
@@ -100,7 +99,7 @@ export function ProjectsHubPage() {
|
|
|
100
99
|
{projects.map((project) => (
|
|
101
100
|
<ProjectCard
|
|
102
101
|
project={project}
|
|
103
|
-
connection={connectionsById.get(project.source.
|
|
102
|
+
connection={connectionsById.get(project.source.connectionId)}
|
|
104
103
|
connectionsResolved={connectionsQuery.isSuccess}
|
|
105
104
|
connectionsSettled={connectionsQuery.isSuccess || connectionsQuery.isError}
|
|
106
105
|
key={project.id}
|
|
@@ -132,15 +131,6 @@ export function ProjectsHubPage() {
|
|
|
132
131
|
);
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
function useDebouncedValue<T>(value: T, delayMs: number): T {
|
|
136
|
-
const [debounced, setDebounced] = useState(value);
|
|
137
|
-
useEffect(() => {
|
|
138
|
-
const handle = setTimeout(() => setDebounced(value), delayMs);
|
|
139
|
-
return () => clearTimeout(handle);
|
|
140
|
-
}, [value, delayMs]);
|
|
141
|
-
return debounced;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
134
|
function ProjectsSkeleton() {
|
|
145
135
|
return (
|
|
146
136
|
<ul
|
|
@@ -201,8 +191,8 @@ function ProjectCard({
|
|
|
201
191
|
connectionsSettled,
|
|
202
192
|
workspaceId,
|
|
203
193
|
}: {
|
|
204
|
-
project:
|
|
205
|
-
connection:
|
|
194
|
+
project: Project;
|
|
195
|
+
connection: IntegrationConnection | undefined;
|
|
206
196
|
connectionsResolved: boolean;
|
|
207
197
|
connectionsSettled: boolean;
|
|
208
198
|
workspaceId: string;
|
|
@@ -210,7 +200,7 @@ function ProjectCard({
|
|
|
210
200
|
// On a resolved fetch, `active` carries no badge while a missing connection
|
|
211
201
|
// reads as an error so a broken source is still flagged. An unresolved or
|
|
212
202
|
// failed fetch shows nothing, so a fetch failure never flags every card.
|
|
213
|
-
const status = connectionsResolved ? (connection?.
|
|
203
|
+
const status = connectionsResolved ? (connection?.lifecycleStatus ?? 'error') : undefined;
|
|
214
204
|
|
|
215
205
|
return (
|
|
216
206
|
<li>
|
package/src/routes/home.tsx
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {defineRoute} from '@shipfox/client-shell/runtime';
|
|
1
|
+
import {defineRoute, useRouteSearch} from '@shipfox/client-shell/runtime';
|
|
2
2
|
import {HomeRouter} from '#pages/home-router.js';
|
|
3
|
+
import {validateProjectsSearch} from './search.js';
|
|
3
4
|
|
|
4
|
-
export default defineRoute({
|
|
5
|
+
export default defineRoute({
|
|
6
|
+
validateSearch: validateProjectsSearch,
|
|
7
|
+
component: () => <HomeRouter search={useRouteSearch(validateProjectsSearch)} />,
|
|
8
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {projectRouteParams} from './inputs.js';
|
|
2
|
+
|
|
3
|
+
describe('project route inputs', () => {
|
|
4
|
+
it('requires both workspace and project ids', () => {
|
|
5
|
+
expect(projectRouteParams({wid: 'workspace-1', pid: 'project-1'})).toEqual({
|
|
6
|
+
wid: 'workspace-1',
|
|
7
|
+
pid: 'project-1',
|
|
8
|
+
});
|
|
9
|
+
expect(() => projectRouteParams({wid: 'workspace-1'})).toThrow(
|
|
10
|
+
'Project route is missing required path parameters.',
|
|
11
|
+
);
|
|
12
|
+
expect(() => projectRouteParams({wid: ['workspace-1'], pid: 'project-1'})).toThrow();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ProjectRouteParams {
|
|
2
|
+
wid: string;
|
|
3
|
+
pid: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function projectRouteParams(input: Record<string, unknown>): ProjectRouteParams {
|
|
7
|
+
const wid = stringParam(input.wid);
|
|
8
|
+
const pid = stringParam(input.pid);
|
|
9
|
+
if (!wid || !pid) throw new Error('Project route is missing required path parameters.');
|
|
10
|
+
return {wid, pid};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function stringParam(value: unknown): string | undefined {
|
|
14
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
15
|
+
}
|