@shipfox/client-projects 21.0.0 → 22.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +21 -0
- package/dist/components/source-strip.d.ts.map +1 -1
- package/dist/components/source-strip.js +49 -45
- package/dist/components/source-strip.js.map +1 -1
- package/dist/pages/create-project-page.d.ts.map +1 -1
- package/dist/pages/create-project-page.js +161 -175
- package/dist/pages/create-project-page.js.map +1 -1
- package/dist/pages/project-settings-page.d.ts.map +1 -1
- package/dist/pages/project-settings-page.js +82 -84
- package/dist/pages/project-settings-page.js.map +1 -1
- package/dist/pages/projects-hub-page.d.ts.map +1 -1
- package/dist/pages/projects-hub-page.js +160 -159
- package/dist/pages/projects-hub-page.js.map +1 -1
- package/dist/pages/projects-hub-page.stories.d.ts +22 -0
- package/dist/pages/projects-hub-page.stories.d.ts.map +1 -0
- package/dist/pages/projects-hub-page.stories.js +214 -0
- package/dist/pages/projects-hub-page.stories.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/components/source-strip.tsx +29 -26
- package/src/pages/create-project-page.test.tsx +85 -1
- package/src/pages/create-project-page.tsx +165 -153
- package/src/pages/home-router.test.tsx +2 -1
- package/src/pages/project-settings-page.test.tsx +10 -1
- package/src/pages/project-settings-page.tsx +88 -76
- package/src/pages/projects-hub-page.stories.tsx +203 -0
- package/src/pages/projects-hub-page.test.tsx +90 -10
- package/src/pages/projects-hub-page.tsx +136 -125
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/client-projects",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "22.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
"@shipfox/api-definitions-dto": "12.3.0",
|
|
32
32
|
"@shipfox/api-integration-core-dto": "12.2.0",
|
|
33
33
|
"@shipfox/api-projects-dto": "12.2.0",
|
|
34
|
-
"@shipfox/client-agent": "
|
|
34
|
+
"@shipfox/client-agent": "22.0.0",
|
|
35
35
|
"@shipfox/client-api": "6.0.1",
|
|
36
|
-
"@shipfox/client-auth": "
|
|
37
|
-
"@shipfox/client-integrations": "
|
|
38
|
-
"@shipfox/client-
|
|
39
|
-
"@shipfox/client-
|
|
40
|
-
"@shipfox/react-ui": "2.
|
|
36
|
+
"@shipfox/client-auth": "22.0.0",
|
|
37
|
+
"@shipfox/client-integrations": "22.0.0",
|
|
38
|
+
"@shipfox/client-ui": "22.0.0",
|
|
39
|
+
"@shipfox/client-shell": "22.0.0",
|
|
40
|
+
"@shipfox/react-ui": "2.1.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@tanstack/react-query": "^5.101.0",
|
|
@@ -2,6 +2,7 @@ import {useActiveWorkspace} from '@shipfox/client-auth';
|
|
|
2
2
|
import {useSourceConnectionsQuery} from '@shipfox/client-integrations';
|
|
3
3
|
import {StatusBadge} from '@shipfox/react-ui/badge';
|
|
4
4
|
import {Icon, type IconName} from '@shipfox/react-ui/icon';
|
|
5
|
+
import {Panel} from '@shipfox/react-ui/panel';
|
|
5
6
|
import {Skeleton} from '@shipfox/react-ui/skeleton';
|
|
6
7
|
import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
|
|
7
8
|
import {Code, Text} from '@shipfox/react-ui/typography';
|
|
@@ -29,34 +30,36 @@ export function SourceStrip({
|
|
|
29
30
|
const connection = connectionsQuery.data?.find((c) => c.id === connectionId);
|
|
30
31
|
|
|
31
32
|
return (
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
<Panel
|
|
34
|
+
asChild
|
|
35
|
+
className="gap-inline px-row py-row sm:flex-row sm:items-center sm:justify-between"
|
|
35
36
|
>
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
37
|
+
<section aria-label="Project source">
|
|
38
|
+
<div className="flex min-w-0 items-center gap-inline">
|
|
39
|
+
<Icon name={providerIconName(connection?.provider)} className="size-20 shrink-0" />
|
|
40
|
+
<div className="flex min-w-0 flex-col gap-tight sm:flex-row sm:items-center sm:gap-inline">
|
|
41
|
+
{connectionsQuery.isPending ? (
|
|
42
|
+
<Skeleton className="h-16 w-160" />
|
|
43
|
+
) : (
|
|
44
|
+
<Text size="sm" bold className="truncate">
|
|
45
|
+
{connection?.displayName ?? 'Connected source'}
|
|
46
|
+
</Text>
|
|
47
|
+
)}
|
|
48
|
+
<Tooltip>
|
|
49
|
+
<TooltipTrigger asChild>
|
|
50
|
+
<Code variant="label" className="truncate text-foreground-neutral-muted">
|
|
51
|
+
{externalRepositoryId}
|
|
52
|
+
</Code>
|
|
53
|
+
</TooltipTrigger>
|
|
54
|
+
<TooltipContent>{externalRepositoryId}</TooltipContent>
|
|
55
|
+
</Tooltip>
|
|
56
|
+
</div>
|
|
54
57
|
</div>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
</
|
|
59
|
-
</
|
|
58
|
+
<div className="shrink-0">
|
|
59
|
+
<SyncBadge sync={sync} isPending={isPending} />
|
|
60
|
+
</div>
|
|
61
|
+
</section>
|
|
62
|
+
</Panel>
|
|
60
63
|
);
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {configureApiClient} from '@shipfox/client-api';
|
|
2
2
|
import {QueryClient} from '@tanstack/react-query';
|
|
3
|
-
import {fireEvent, screen, waitFor} from '@testing-library/react';
|
|
3
|
+
import {fireEvent, screen, waitFor, within} from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
4
5
|
import {projectsQueryKeys} from '#hooks/api/projects.js';
|
|
5
6
|
import {
|
|
6
7
|
jsonResponse,
|
|
@@ -43,6 +44,24 @@ describe('CreateProjectPage', () => {
|
|
|
43
44
|
const nameInput = await screen.findByLabelText('Project name');
|
|
44
45
|
const slugInput = await screen.findByLabelText('Project slug');
|
|
45
46
|
await waitFor(() => expect(nameInput).toHaveValue('Platform'));
|
|
47
|
+
expect(
|
|
48
|
+
screen.queryByText(
|
|
49
|
+
'A Shipfox project starts from a Git repository. Choose the repository Shipfox should track, then give the project a name.',
|
|
50
|
+
),
|
|
51
|
+
).not.toBeInTheDocument();
|
|
52
|
+
expect(screen.getByRole('searchbox', {name: 'Search repositories'})).toBeInTheDocument();
|
|
53
|
+
expect(document.querySelectorAll('[data-slot="panel"]')).toHaveLength(3);
|
|
54
|
+
expect(within(panelForHeading('Project details')).getByLabelText('Project name')).toBe(
|
|
55
|
+
nameInput,
|
|
56
|
+
);
|
|
57
|
+
expect(within(panelForHeading('Project details')).getByLabelText('Project slug')).toBe(
|
|
58
|
+
slugInput,
|
|
59
|
+
);
|
|
60
|
+
expect(
|
|
61
|
+
within(panelForHeading('Repository')).getByRole('searchbox', {
|
|
62
|
+
name: 'Search repositories',
|
|
63
|
+
}),
|
|
64
|
+
).toBeInTheDocument();
|
|
46
65
|
expect(slugInput).toHaveValue('platform');
|
|
47
66
|
expect(slugInput).toHaveAttribute('aria-describedby', 'project-slug-description');
|
|
48
67
|
expect(screen.getByText('/w/acme/p/platform')).toBeInTheDocument();
|
|
@@ -69,6 +88,65 @@ describe('CreateProjectPage', () => {
|
|
|
69
88
|
});
|
|
70
89
|
}, 10_000);
|
|
71
90
|
|
|
91
|
+
test('does not render an empty source panel when connections fail to load', async () => {
|
|
92
|
+
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
93
|
+
const request = input as Request;
|
|
94
|
+
if (request.url.includes('/integration-connections?')) {
|
|
95
|
+
return Promise.resolve(jsonResponse({message: 'upstream unavailable'}, {status: 500}));
|
|
96
|
+
}
|
|
97
|
+
return Promise.resolve(jsonResponse({}));
|
|
98
|
+
});
|
|
99
|
+
configureApiClient({fetchImpl});
|
|
100
|
+
|
|
101
|
+
renderProjectPage(`/w/${PROJECT_TEST_WSLUG}/projects/new`, <CreateProjectPage />);
|
|
102
|
+
|
|
103
|
+
expect(
|
|
104
|
+
await screen.findByText(
|
|
105
|
+
'Could not load source integrations. Refresh the integrations list to continue.',
|
|
106
|
+
),
|
|
107
|
+
).toBeInTheDocument();
|
|
108
|
+
expect(screen.queryByRole('heading', {name: 'Source integration'})).not.toBeInTheDocument();
|
|
109
|
+
expect(document.querySelectorAll('[data-slot="panel"]')).toHaveLength(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('passes the repository search filter to the query and renders the filtered empty state', async () => {
|
|
113
|
+
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
114
|
+
const request = input as Request;
|
|
115
|
+
if (request.url.includes('/integration-connections?')) {
|
|
116
|
+
return Promise.resolve(jsonResponse({connections: [connectionDto()]}));
|
|
117
|
+
}
|
|
118
|
+
if (request.url.includes(`/integration-connections/${CONNECTION_ID}/repositories`)) {
|
|
119
|
+
const search = new URL(request.url).searchParams.get('search');
|
|
120
|
+
return Promise.resolve(
|
|
121
|
+
jsonResponse(
|
|
122
|
+
search === 'xyz'
|
|
123
|
+
? {repositories: [], next_cursor: null}
|
|
124
|
+
: {repositories: [repositoryDto()], next_cursor: null},
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return Promise.resolve(jsonResponse({}));
|
|
129
|
+
});
|
|
130
|
+
configureApiClient({fetchImpl});
|
|
131
|
+
|
|
132
|
+
renderProjectPage(`/w/${PROJECT_TEST_WSLUG}/projects/new`, <CreateProjectPage />);
|
|
133
|
+
expect((await screen.findAllByText('gitea-owner/platform')).length).toBeGreaterThan(0);
|
|
134
|
+
|
|
135
|
+
const user = userEvent.setup();
|
|
136
|
+
await user.type(screen.getByRole('searchbox', {name: 'Search repositories'}), 'xyz');
|
|
137
|
+
|
|
138
|
+
expect(await screen.findByText('No repositories matching "xyz".')).toBeInTheDocument();
|
|
139
|
+
expect(
|
|
140
|
+
fetchImpl.mock.calls.some(([input]) => {
|
|
141
|
+
const request = input as Request;
|
|
142
|
+
return (
|
|
143
|
+
request.url.includes(`/integration-connections/${CONNECTION_ID}/repositories`) &&
|
|
144
|
+
new URL(request.url).searchParams.get('search') === 'xyz'
|
|
145
|
+
);
|
|
146
|
+
}),
|
|
147
|
+
).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
72
150
|
test('auto-derives the slug from each keystroke in the name field until the slug is touched', async () => {
|
|
73
151
|
const fetchImpl = vi.fn((input: RequestInfo | URL) => {
|
|
74
152
|
const request = input as Request;
|
|
@@ -496,3 +574,9 @@ function projectPostCount(fetchImpl: ReturnType<typeof vi.fn>): number {
|
|
|
496
574
|
return request.url.endsWith('/projects') && request.method === 'POST';
|
|
497
575
|
}).length;
|
|
498
576
|
}
|
|
577
|
+
|
|
578
|
+
function panelForHeading(name: string): HTMLElement {
|
|
579
|
+
const panel = screen.getByRole('heading', {name}).closest('[data-slot="panel"]');
|
|
580
|
+
if (!(panel instanceof HTMLElement)) throw new Error(`Panel not found for heading: ${name}`);
|
|
581
|
+
return panel;
|
|
582
|
+
}
|
|
@@ -13,7 +13,9 @@ import {displayNameFieldError, SlugField} from '@shipfox/client-ui';
|
|
|
13
13
|
import {Button} from '@shipfox/react-ui/button';
|
|
14
14
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
15
15
|
import {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';
|
|
16
|
+
import {Input} from '@shipfox/react-ui/input';
|
|
16
17
|
import {FullPageLoader} from '@shipfox/react-ui/loader';
|
|
18
|
+
import {Panel, PanelActions, PanelBody, PanelHeader, PanelTitle} from '@shipfox/react-ui/panel';
|
|
17
19
|
import {toast} from '@shipfox/react-ui/toast';
|
|
18
20
|
import {Header, Text} from '@shipfox/react-ui/typography';
|
|
19
21
|
import {useForm} from '@tanstack/react-form';
|
|
@@ -199,15 +201,9 @@ export function CreateProjectPage() {
|
|
|
199
201
|
|
|
200
202
|
return (
|
|
201
203
|
<div className="flex w-full flex-col gap-section">
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
</Header>
|
|
206
|
-
<Text size="sm" className="text-foreground-neutral-muted">
|
|
207
|
-
A Shipfox project starts from a Git repository. Choose the repository Shipfox should
|
|
208
|
-
track, then give the project a name.
|
|
209
|
-
</Text>
|
|
210
|
-
</header>
|
|
204
|
+
<Header id="create-project-title" variant="h1">
|
|
205
|
+
Create project
|
|
206
|
+
</Header>
|
|
211
207
|
|
|
212
208
|
<ModelProviderReminderBanner workspaceId={workspace.id} />
|
|
213
209
|
|
|
@@ -240,161 +236,177 @@ export function CreateProjectPage() {
|
|
|
240
236
|
className="grid items-start gap-region lg:grid-cols-[minmax(0,1fr)_340px]"
|
|
241
237
|
>
|
|
242
238
|
<div className="flex min-w-0 flex-col gap-region">
|
|
243
|
-
|
|
244
|
-
<
|
|
245
|
-
<
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
</
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
239
|
+
{connections.length > 0 ? (
|
|
240
|
+
<section aria-label="Source integration">
|
|
241
|
+
<Panel>
|
|
242
|
+
<PanelHeader>
|
|
243
|
+
<PanelTitle>Source integration</PanelTitle>
|
|
244
|
+
{connections.length === 1 ? (
|
|
245
|
+
<PanelActions>
|
|
246
|
+
<Button asChild variant="transparent" size="sm" className="shrink-0">
|
|
247
|
+
<Link
|
|
248
|
+
to="/w/$workspaceSlug/integrations"
|
|
249
|
+
params={{workspaceSlug: workspace.slug}}
|
|
250
|
+
>
|
|
251
|
+
Add another integration
|
|
252
|
+
</Link>
|
|
253
|
+
</Button>
|
|
254
|
+
</PanelActions>
|
|
255
|
+
) : null}
|
|
256
|
+
</PanelHeader>
|
|
257
|
+
|
|
258
|
+
<PanelBody>
|
|
259
|
+
<ConnectionPicker
|
|
260
|
+
connections={connections}
|
|
261
|
+
selectedConnectionId={effectiveSelectedConnectionId}
|
|
262
|
+
onSelect={selectConnection}
|
|
263
|
+
/>
|
|
264
|
+
</PanelBody>
|
|
265
|
+
</Panel>
|
|
266
|
+
</section>
|
|
267
|
+
) : null}
|
|
272
268
|
|
|
273
269
|
{showRepoPicker ? (
|
|
274
|
-
<section
|
|
275
|
-
<
|
|
276
|
-
<
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
270
|
+
<section aria-label="Repository">
|
|
271
|
+
<Panel>
|
|
272
|
+
<PanelHeader className="flex-wrap">
|
|
273
|
+
<PanelTitle>Repository</PanelTitle>
|
|
274
|
+
<PanelActions className="min-w-0 max-[640px]:ml-0 max-[640px]:basis-full">
|
|
275
|
+
<Input
|
|
276
|
+
type="search"
|
|
277
|
+
placeholder="Search repositories…"
|
|
278
|
+
aria-label="Search repositories"
|
|
279
|
+
value={repoFilter}
|
|
280
|
+
onChange={(event) => setRepoFilter(event.target.value)}
|
|
281
|
+
className="w-full max-w-[320px]"
|
|
282
|
+
/>
|
|
283
|
+
</PanelActions>
|
|
284
|
+
</PanelHeader>
|
|
285
|
+
<PanelBody>
|
|
286
|
+
<RepositoryPicker
|
|
287
|
+
repositories={repositories}
|
|
288
|
+
selectedRepositoryId={selectedRepositoryId}
|
|
289
|
+
onSelect={setSelectedRepositoryId}
|
|
290
|
+
isLoading={repositoriesQuery.isPending}
|
|
291
|
+
isFetchingNextPage={repositoriesQuery.isFetchingNextPage}
|
|
292
|
+
hasNextPage={repositoriesQuery.hasNextPage}
|
|
293
|
+
onLoadMore={() => repositoriesQuery.fetchNextPage()}
|
|
294
|
+
emptyMessage={filteredEmptyMessage}
|
|
295
|
+
/>
|
|
296
|
+
</PanelBody>
|
|
297
|
+
</Panel>
|
|
294
298
|
</section>
|
|
295
299
|
) : null}
|
|
296
300
|
</div>
|
|
297
301
|
|
|
298
302
|
<aside className="lg:sticky lg:top-32">
|
|
299
|
-
<
|
|
300
|
-
<
|
|
301
|
-
<
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
303
|
+
<Panel>
|
|
304
|
+
<PanelHeader>
|
|
305
|
+
<PanelTitle>Project details</PanelTitle>
|
|
306
|
+
</PanelHeader>
|
|
307
|
+
<PanelBody className="gap-group p-panel">
|
|
308
|
+
{formError ? (
|
|
309
|
+
<Callout role="alert" type="error">
|
|
310
|
+
<div ref={errorRef} tabIndex={-1}>
|
|
311
|
+
{formError}
|
|
312
|
+
</div>
|
|
313
|
+
</Callout>
|
|
314
|
+
) : null}
|
|
315
|
+
|
|
316
|
+
<ProjectSummary
|
|
317
|
+
connection={selectedConnection}
|
|
318
|
+
repositoryName={selectedRepository?.fullName}
|
|
319
|
+
/>
|
|
320
|
+
|
|
321
|
+
<form.Field
|
|
322
|
+
name="name"
|
|
323
|
+
validators={{
|
|
324
|
+
onBlur: ({value}) =>
|
|
325
|
+
displayNameFieldError(
|
|
326
|
+
value,
|
|
327
|
+
'Project name',
|
|
328
|
+
createProjectBodySchema.shape.name,
|
|
329
|
+
),
|
|
330
|
+
onSubmit: ({value}) =>
|
|
331
|
+
displayNameFieldError(
|
|
332
|
+
value,
|
|
333
|
+
'Project name',
|
|
334
|
+
createProjectBodySchema.shape.name,
|
|
335
|
+
),
|
|
336
|
+
}}
|
|
337
|
+
>
|
|
338
|
+
{(field) => (
|
|
339
|
+
<FormField label="Project name" id="project-name" error={fieldError(field)}>
|
|
340
|
+
<FormFieldInput
|
|
341
|
+
name="name"
|
|
342
|
+
type="text"
|
|
343
|
+
value={field.state.value}
|
|
344
|
+
onChange={(event) => {
|
|
345
|
+
const nextName = event.target.value;
|
|
346
|
+
setNameTouched(true);
|
|
347
|
+
field.handleChange(nextName);
|
|
348
|
+
if (!slugManuallyEdited) {
|
|
349
|
+
setSlugCheckEnabled(true);
|
|
350
|
+
setSlugConflict(false);
|
|
351
|
+
form.setFieldValue('slug', slugifyName(nextName, {fallback: 'project'}));
|
|
352
|
+
}
|
|
353
|
+
}}
|
|
354
|
+
onBlur={field.handleBlur}
|
|
355
|
+
placeholder="Platform"
|
|
356
|
+
/>
|
|
357
|
+
</FormField>
|
|
358
|
+
)}
|
|
359
|
+
</form.Field>
|
|
360
|
+
|
|
361
|
+
<form.Field
|
|
362
|
+
name="slug"
|
|
363
|
+
validators={{
|
|
364
|
+
onBlur: createProjectBodySchema.shape.slug,
|
|
365
|
+
onSubmit: createProjectBodySchema.shape.slug,
|
|
366
|
+
}}
|
|
367
|
+
>
|
|
368
|
+
{(field) => (
|
|
369
|
+
<SlugField
|
|
370
|
+
id="project-slug"
|
|
371
|
+
label="Project slug"
|
|
372
|
+
name="slug"
|
|
334
373
|
value={field.state.value}
|
|
335
|
-
onChange={(
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
setSlugCheckEnabled(true);
|
|
341
|
-
setSlugConflict(false);
|
|
342
|
-
form.setFieldValue('slug', slugifyName(nextName, {fallback: 'project'}));
|
|
343
|
-
}
|
|
374
|
+
onChange={(value) => {
|
|
375
|
+
setSlugManuallyEdited(true);
|
|
376
|
+
setSlugCheckEnabled(true);
|
|
377
|
+
setSlugConflict(false);
|
|
378
|
+
field.handleChange(value);
|
|
344
379
|
}}
|
|
345
380
|
onBlur={field.handleBlur}
|
|
346
|
-
|
|
381
|
+
error={
|
|
382
|
+
slugConflict ? 'This project slug is already in use.' : fieldError(field)
|
|
383
|
+
}
|
|
384
|
+
description={
|
|
385
|
+
<span className="font-code">
|
|
386
|
+
{`/w/${workspace.slug}/p/${field.state.value}`}
|
|
387
|
+
</span>
|
|
388
|
+
}
|
|
389
|
+
placeholder="platform"
|
|
390
|
+
className="font-code"
|
|
391
|
+
checkEnabled={slugCheckEnabled}
|
|
392
|
+
debounceMs={0}
|
|
393
|
+
isValid={isSlugValid}
|
|
394
|
+
checkAvailability={checkProjectSlugAvailability}
|
|
347
395
|
/>
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
label="Project slug"
|
|
363
|
-
name="slug"
|
|
364
|
-
value={field.state.value}
|
|
365
|
-
onChange={(value) => {
|
|
366
|
-
setSlugManuallyEdited(true);
|
|
367
|
-
setSlugCheckEnabled(true);
|
|
368
|
-
setSlugConflict(false);
|
|
369
|
-
field.handleChange(value);
|
|
370
|
-
}}
|
|
371
|
-
onBlur={field.handleBlur}
|
|
372
|
-
error={slugConflict ? 'This project slug is already in use.' : fieldError(field)}
|
|
373
|
-
description={
|
|
374
|
-
<span className="font-code">
|
|
375
|
-
{`/w/${workspace.slug}/p/${field.state.value}`}
|
|
376
|
-
</span>
|
|
377
|
-
}
|
|
378
|
-
placeholder="platform"
|
|
379
|
-
className="font-code"
|
|
380
|
-
checkEnabled={slugCheckEnabled}
|
|
381
|
-
debounceMs={0}
|
|
382
|
-
isValid={isSlugValid}
|
|
383
|
-
checkAvailability={checkProjectSlugAvailability}
|
|
384
|
-
/>
|
|
385
|
-
)}
|
|
386
|
-
</form.Field>
|
|
387
|
-
|
|
388
|
-
<Button
|
|
389
|
-
type="submit"
|
|
390
|
-
iconRight="chevronRight"
|
|
391
|
-
isLoading={createProject.isPending}
|
|
392
|
-
disabled={!selectedConnection || !selectedRepository}
|
|
393
|
-
className="w-full"
|
|
394
|
-
>
|
|
395
|
-
Create project
|
|
396
|
-
</Button>
|
|
397
|
-
</div>
|
|
396
|
+
)}
|
|
397
|
+
</form.Field>
|
|
398
|
+
|
|
399
|
+
<Button
|
|
400
|
+
type="submit"
|
|
401
|
+
iconRight="chevronRight"
|
|
402
|
+
isLoading={createProject.isPending}
|
|
403
|
+
disabled={!selectedConnection || !selectedRepository}
|
|
404
|
+
className="w-full"
|
|
405
|
+
>
|
|
406
|
+
Create project
|
|
407
|
+
</Button>
|
|
408
|
+
</PanelBody>
|
|
409
|
+
</Panel>
|
|
398
410
|
</aside>
|
|
399
411
|
</form>
|
|
400
412
|
</div>
|
|
@@ -18,7 +18,8 @@ describe('HomeRouter', () => {
|
|
|
18
18
|
|
|
19
19
|
renderProjectPage(`/w/${PROJECT_TEST_WSLUG}`, <HomeRouter />);
|
|
20
20
|
|
|
21
|
-
expect(await screen.findByRole('
|
|
21
|
+
expect(await screen.findByRole('region', {name: 'Projects'})).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByRole('searchbox', {name: 'Search projects'})).toBeInTheDocument();
|
|
22
23
|
const calledUrls = fetchImpl.mock.calls.map(([input]) =>
|
|
23
24
|
input instanceof Request ? input.url : String(input),
|
|
24
25
|
);
|
|
@@ -17,7 +17,16 @@ describe('ProjectSettingsPage', () => {
|
|
|
17
17
|
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
18
18
|
|
|
19
19
|
renderProjectPage('/w/acme/p/platform/settings/general', <ProjectSettingsPage />);
|
|
20
|
-
|
|
20
|
+
const nameInput = await screen.findByLabelText('Project name');
|
|
21
|
+
expect(screen.getByRole('heading', {name: 'General'})).toBeInTheDocument();
|
|
22
|
+
const settingsForm = nameInput.closest('form');
|
|
23
|
+
expect(settingsForm).not.toBeNull();
|
|
24
|
+
expect(settingsForm).toHaveClass('max-w-[560px]');
|
|
25
|
+
expect(
|
|
26
|
+
screen.queryByText('Update the project name and the slug used in its URLs.'),
|
|
27
|
+
).not.toBeInTheDocument();
|
|
28
|
+
expect(document.querySelectorAll('[data-slot="panel"]')).toHaveLength(1);
|
|
29
|
+
fireEvent.change(nameInput, {
|
|
21
30
|
target: {value: 'Platform API'},
|
|
22
31
|
});
|
|
23
32
|
fireEvent.click(screen.getByRole('button', {name: 'Save changes'}));
|