@shipfox/client-projects 4.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 +78 -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 -10
- package/dist/feature.d.ts.map +1 -1
- package/dist/feature.js +11 -22
- 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 +19 -81
- package/dist/hooks/api/projects.d.ts.map +1 -1
- package/dist/hooks/api/projects.js +72 -18
- package/dist/hooks/api/projects.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- 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.d.ts.map +1 -1
- package/dist/pages/create-project-page.stories.js +4 -5
- 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 -17
- 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 -23
- 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 +125 -28
- package/src/index.ts +10 -6
- package/src/pages/create-project-page.stories.tsx +7 -5
- 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/test/pages.tsx +2 -17
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/components/workspace-setup-guard.d.ts +0 -9
- package/dist/components/workspace-setup-guard.d.ts.map +0 -1
- package/dist/components/workspace-setup-guard.js +0 -160
- package/dist/components/workspace-setup-guard.js.map +0 -1
- package/dist/pages/project-workflows-page.d.ts +0 -4
- package/dist/pages/project-workflows-page.d.ts.map +0 -1
- package/dist/pages/project-workflows-page.js +0 -481
- package/dist/pages/project-workflows-page.js.map +0 -1
- package/dist/routes/workflows.d.ts +0 -5
- package/dist/routes/workflows.d.ts.map +0 -1
- package/dist/routes/workflows.js +0 -16
- package/dist/routes/workflows.js.map +0 -1
- package/src/components/workspace-setup-guard.test.tsx +0 -534
- package/src/components/workspace-setup-guard.tsx +0 -185
- package/src/pages/project-workflows-page.test.tsx +0 -258
- package/src/pages/project-workflows-page.tsx +0 -445
- package/src/routes/workflows.tsx +0 -10
|
@@ -1,185 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import {configureApiClient} from '@shipfox/client-api';
|
|
2
|
-
import {fireEvent, screen, waitFor} from '@testing-library/react';
|
|
3
|
-
import {jsonResponse, PROJECT_TEST_WID, renderProjectPage} from '#test/pages.js';
|
|
4
|
-
import {ProjectWorkflowsPage} from './project-workflows-page.js';
|
|
5
|
-
|
|
6
|
-
const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
|
|
7
|
-
const CONNECTION_ID = '33333333-3333-4333-8333-333333333333';
|
|
8
|
-
|
|
9
|
-
describe('ProjectWorkflowsPage', () => {
|
|
10
|
-
test('renders workflow definitions and the source strip', async () => {
|
|
11
|
-
configureApiClient({fetchImpl: createProjectDetailFetch()});
|
|
12
|
-
|
|
13
|
-
renderProjectPage(
|
|
14
|
-
`/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}`,
|
|
15
|
-
<ProjectWorkflowsPage projectId={PROJECT_ID} />,
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
expect(await screen.findByRole('heading', {name: 'Workflows'})).toBeInTheDocument();
|
|
19
|
-
expect(screen.getAllByText('Deploy production')[0]).toBeInTheDocument();
|
|
20
|
-
expect(screen.getAllByText('.shipfox/workflows/deploy.yml')[0]).toBeInTheDocument();
|
|
21
|
-
expect(screen.getByRole('region', {name: 'Project source'})).toBeInTheDocument();
|
|
22
|
-
// Source strip resolves connection display_name from the integrations
|
|
23
|
-
// workspace cache; external_repository_id renders as a Code chip.
|
|
24
|
-
expect(await screen.findByText('Acme GitHub')).toBeInTheDocument();
|
|
25
|
-
expect(screen.getAllByText('platform')[0]).toBeInTheDocument();
|
|
26
|
-
expect(screen.getAllByText('succeeded')[0]).toBeInTheDocument();
|
|
27
|
-
expect(screen.queryByRole('heading', {name: 'Source identity'})).not.toBeInTheDocument();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('shows definitions error while keeping the source strip visible', async () => {
|
|
31
|
-
configureApiClient({
|
|
32
|
-
fetchImpl: createProjectDetailFetch({
|
|
33
|
-
definitions: jsonResponse({code: 'server-error'}, {status: 500}),
|
|
34
|
-
}),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
renderProjectPage(
|
|
38
|
-
`/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}`,
|
|
39
|
-
<ProjectWorkflowsPage projectId={PROJECT_ID} />,
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
expect(await screen.findByText("Couldn't load workflows")).toBeInTheDocument();
|
|
43
|
-
// SyncBadge in the strip falls back to Unavailable when sync is undefined
|
|
44
|
-
// (definitions errored before providing one).
|
|
45
|
-
expect(screen.getByText('Unavailable')).toBeInTheDocument();
|
|
46
|
-
expect(screen.getByRole('region', {name: 'Project source'})).toBeInTheDocument();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('shows failed sync empty state', async () => {
|
|
50
|
-
configureApiClient({
|
|
51
|
-
fetchImpl: createProjectDetailFetch({
|
|
52
|
-
definitions: jsonResponse(
|
|
53
|
-
definitionsDto({
|
|
54
|
-
definitions: [],
|
|
55
|
-
sync: {
|
|
56
|
-
ref: 'main',
|
|
57
|
-
status: 'failed',
|
|
58
|
-
last_sync_at: '2026-05-07T01:00:00.000Z',
|
|
59
|
-
started_at: '2026-05-07T01:00:00.000Z',
|
|
60
|
-
finished_at: null,
|
|
61
|
-
last_error_code: 'no-workflow-files',
|
|
62
|
-
last_error_message: 'No workflow files found',
|
|
63
|
-
},
|
|
64
|
-
}),
|
|
65
|
-
),
|
|
66
|
-
}),
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
renderProjectPage(
|
|
70
|
-
`/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}`,
|
|
71
|
-
<ProjectWorkflowsPage projectId={PROJECT_ID} />,
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
expect(
|
|
75
|
-
await screen.findByText('No workflow files found under .shipfox/workflows/.'),
|
|
76
|
-
).toBeInTheDocument();
|
|
77
|
-
expect(screen.getByText('Workflow sync failed')).toBeInTheDocument();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('opens and closes the definition drawer by clicking the row', async () => {
|
|
81
|
-
configureApiClient({fetchImpl: createProjectDetailFetch()});
|
|
82
|
-
|
|
83
|
-
renderProjectPage(
|
|
84
|
-
`/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}`,
|
|
85
|
-
<ProjectWorkflowsPage projectId={PROJECT_ID} />,
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const workflowName = (await screen.findAllByText('Deploy production'))[0];
|
|
89
|
-
if (!workflowName) throw new Error('Workflow row was not rendered');
|
|
90
|
-
fireEvent.click(workflowName);
|
|
91
|
-
|
|
92
|
-
expect(await screen.findByText('Normalized definition')).toBeInTheDocument();
|
|
93
|
-
expect(screen.getByText((content) => content.includes('"deploy"'))).toBeInTheDocument();
|
|
94
|
-
|
|
95
|
-
fireEvent.keyDown(document, {key: 'Escape'});
|
|
96
|
-
|
|
97
|
-
await waitFor(() => {
|
|
98
|
-
expect(screen.queryByText('Normalized definition')).not.toBeInTheDocument();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('queues a run from a workflow definition', async () => {
|
|
103
|
-
configureApiClient({fetchImpl: createProjectDetailFetch()});
|
|
104
|
-
|
|
105
|
-
renderProjectPage(
|
|
106
|
-
`/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}`,
|
|
107
|
-
<ProjectWorkflowsPage projectId={PROJECT_ID} />,
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
// Run button lives in the row's hover-reveal slot; getAllByRole still
|
|
111
|
-
// sees it (opacity-0, not display:none).
|
|
112
|
-
const [runButton] = await screen.findAllByRole('button', {name: 'Run'});
|
|
113
|
-
if (!runButton) throw new Error('Run button was not rendered');
|
|
114
|
-
|
|
115
|
-
fireEvent.click(runButton);
|
|
116
|
-
|
|
117
|
-
expect(await screen.findByText('Run queued')).toBeInTheDocument();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('renders not found state', async () => {
|
|
121
|
-
configureApiClient({
|
|
122
|
-
fetchImpl: vi.fn((input) => {
|
|
123
|
-
const url = new URL(requestInputUrl(input));
|
|
124
|
-
if (url.pathname === `/projects/${PROJECT_ID}`) {
|
|
125
|
-
return Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}));
|
|
126
|
-
}
|
|
127
|
-
if (url.pathname === '/integration-connections') {
|
|
128
|
-
return Promise.resolve(jsonResponse(connectionsDto()));
|
|
129
|
-
}
|
|
130
|
-
return Promise.resolve(jsonResponse(definitionsDto()));
|
|
131
|
-
}),
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
renderProjectPage(
|
|
135
|
-
`/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}`,
|
|
136
|
-
<ProjectWorkflowsPage projectId={PROJECT_ID} />,
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
expect(await screen.findByText('Project not found')).toBeInTheDocument();
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
function createProjectDetailFetch({
|
|
144
|
-
project = jsonResponse(projectDto()),
|
|
145
|
-
definitions = jsonResponse(definitionsDto()),
|
|
146
|
-
run = jsonResponse(runDto(), {status: 201}),
|
|
147
|
-
connections = jsonResponse(connectionsDto()),
|
|
148
|
-
}: {
|
|
149
|
-
project?: Response;
|
|
150
|
-
definitions?: Response;
|
|
151
|
-
run?: Response;
|
|
152
|
-
connections?: Response;
|
|
153
|
-
} = {}) {
|
|
154
|
-
return vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
|
155
|
-
const url = new URL(requestInputUrl(input));
|
|
156
|
-
const method = init?.method ?? (input instanceof Request ? input.method : 'GET');
|
|
157
|
-
|
|
158
|
-
if (url.pathname === `/projects/${PROJECT_ID}`) {
|
|
159
|
-
return Promise.resolve(project.clone());
|
|
160
|
-
}
|
|
161
|
-
if (url.pathname === '/definitions') {
|
|
162
|
-
return Promise.resolve(definitions.clone());
|
|
163
|
-
}
|
|
164
|
-
if (url.pathname === '/integration-connections') {
|
|
165
|
-
return Promise.resolve(connections.clone());
|
|
166
|
-
}
|
|
167
|
-
if (
|
|
168
|
-
url.pathname.startsWith('/workflow-definitions/') &&
|
|
169
|
-
url.pathname.endsWith('/fire-manual') &&
|
|
170
|
-
method === 'POST'
|
|
171
|
-
) {
|
|
172
|
-
return Promise.resolve(run.clone());
|
|
173
|
-
}
|
|
174
|
-
return Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}));
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function requestInputUrl(input: RequestInfo | URL) {
|
|
179
|
-
if (input instanceof Request) return input.url;
|
|
180
|
-
return String(input);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function projectDto() {
|
|
184
|
-
return {
|
|
185
|
-
id: PROJECT_ID,
|
|
186
|
-
workspace_id: PROJECT_TEST_WID,
|
|
187
|
-
name: 'Platform',
|
|
188
|
-
source: {
|
|
189
|
-
connection_id: CONNECTION_ID,
|
|
190
|
-
external_repository_id: 'platform',
|
|
191
|
-
},
|
|
192
|
-
created_at: new Date().toISOString(),
|
|
193
|
-
updated_at: new Date().toISOString(),
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function connectionsDto() {
|
|
198
|
-
return {
|
|
199
|
-
connections: [
|
|
200
|
-
{
|
|
201
|
-
id: CONNECTION_ID,
|
|
202
|
-
workspace_id: PROJECT_TEST_WID,
|
|
203
|
-
provider: 'github',
|
|
204
|
-
external_account_id: 'acme',
|
|
205
|
-
slug: 'github_acme',
|
|
206
|
-
display_name: 'Acme GitHub',
|
|
207
|
-
lifecycle_status: 'active',
|
|
208
|
-
capabilities: ['source_control'],
|
|
209
|
-
created_at: '2026-05-07T00:00:00.000Z',
|
|
210
|
-
updated_at: '2026-05-07T00:00:00.000Z',
|
|
211
|
-
},
|
|
212
|
-
],
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function definitionsDto(overrides: Partial<{definitions: unknown[]; sync: unknown}> = {}) {
|
|
217
|
-
return {...baseDefinitionsDto(), ...overrides};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function baseDefinitionsDto() {
|
|
221
|
-
return {
|
|
222
|
-
definitions: [
|
|
223
|
-
{
|
|
224
|
-
id: '55555555-5555-4555-8555-555555555555',
|
|
225
|
-
project_id: PROJECT_ID,
|
|
226
|
-
config_path: '.shipfox/workflows/deploy.yml',
|
|
227
|
-
source: 'vcs',
|
|
228
|
-
sha: 'abc123',
|
|
229
|
-
ref: 'main',
|
|
230
|
-
name: 'Deploy production',
|
|
231
|
-
workflow_document: {
|
|
232
|
-
name: 'Deploy production',
|
|
233
|
-
triggers: {on_demand: {source: 'manual', event: 'fire'}},
|
|
234
|
-
jobs: {deploy: {steps: [{run: './deploy.sh'}]}},
|
|
235
|
-
},
|
|
236
|
-
workflow_model: {kind: 'workflow', name: 'Deploy production'},
|
|
237
|
-
manual_trigger: {name: 'on_demand'},
|
|
238
|
-
fetched_at: '2026-05-07T01:00:00.000Z',
|
|
239
|
-
created_at: '2026-05-07T01:00:00.000Z',
|
|
240
|
-
updated_at: '2026-05-07T01:00:00.000Z',
|
|
241
|
-
},
|
|
242
|
-
],
|
|
243
|
-
next_cursor: null,
|
|
244
|
-
sync: {
|
|
245
|
-
ref: 'main',
|
|
246
|
-
status: 'succeeded',
|
|
247
|
-
last_sync_at: '2026-05-07T01:00:00.000Z',
|
|
248
|
-
started_at: '2026-05-07T00:59:55.000Z',
|
|
249
|
-
finished_at: '2026-05-07T01:00:00.000Z',
|
|
250
|
-
last_error_code: null,
|
|
251
|
-
last_error_message: null,
|
|
252
|
-
},
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function runDto() {
|
|
257
|
-
return {run_id: '66666666-6666-4666-8666-666666666666'};
|
|
258
|
-
}
|