@shipfox/client-projects 12.0.2 → 14.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 +64 -0
- package/dist/chrome.d.ts +5 -1
- package/dist/chrome.d.ts.map +1 -1
- package/dist/chrome.js +18 -26
- 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 +4 -2
- package/dist/components/model-provider-reminder-banner.js.map +1 -1
- package/dist/components/project-crumb.d.ts +3 -1
- package/dist/components/project-crumb.d.ts.map +1 -1
- package/dist/components/project-crumb.js +7 -5
- package/dist/components/project-crumb.js.map +1 -1
- package/dist/components/project-switcher.d.ts +2 -1
- package/dist/components/project-switcher.d.ts.map +1 -1
- package/dist/components/project-switcher.js +10 -8
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/core/definition.d.ts +6 -0
- package/dist/core/definition.d.ts.map +1 -1
- package/dist/core/definition.js.map +1 -1
- package/dist/core/project.d.ts +7 -0
- package/dist/core/project.d.ts.map +1 -1
- package/dist/core/project.js.map +1 -1
- package/dist/feature.d.ts +29 -5
- package/dist/feature.d.ts.map +1 -1
- package/dist/feature.js +26 -5
- package/dist/feature.js.map +1 -1
- package/dist/hooks/api/mappers.d.ts.map +1 -1
- package/dist/hooks/api/mappers.js +9 -1
- package/dist/hooks/api/mappers.js.map +1 -1
- package/dist/hooks/api/projects.d.ts +21 -2
- package/dist/hooks/api/projects.d.ts.map +1 -1
- package/dist/hooks/api/projects.js +175 -2
- package/dist/hooks/api/projects.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- 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 +97 -21
- package/dist/pages/create-project-page.js.map +1 -1
- package/dist/pages/create-project-page.stories.js +5 -3
- package/dist/pages/create-project-page.stories.js.map +1 -1
- package/dist/pages/project-settings-form-errors.d.ts +11 -0
- package/dist/pages/project-settings-form-errors.d.ts.map +1 -0
- package/dist/pages/project-settings-form-errors.js +16 -0
- package/dist/pages/project-settings-form-errors.js.map +1 -0
- package/dist/pages/project-settings-page.d.ts +2 -0
- package/dist/pages/project-settings-page.d.ts.map +1 -0
- package/dist/pages/project-settings-page.js +225 -0
- package/dist/pages/project-settings-page.js.map +1 -0
- package/dist/pages/project-settings-page.stories.d.ts +23 -0
- package/dist/pages/project-settings-page.stories.d.ts.map +1 -0
- package/dist/pages/project-settings-page.stories.js +206 -0
- package/dist/pages/project-settings-page.stories.js.map +1 -0
- package/dist/pages/projects-hub-page.js +11 -11
- package/dist/pages/projects-hub-page.js.map +1 -1
- package/dist/project-error.d.ts +1 -0
- package/dist/project-error.d.ts.map +1 -1
- package/dist/project-error.js +7 -0
- package/dist/project-error.js.map +1 -1
- package/dist/routes/inputs.d.ts +2 -2
- package/dist/routes/inputs.d.ts.map +1 -1
- package/dist/routes/inputs.js +5 -5
- package/dist/routes/inputs.js.map +1 -1
- package/dist/routes/project-index.d.ts +2 -2
- package/dist/routes/project-index.d.ts.map +1 -1
- package/dist/routes/project-index.js +3 -3
- package/dist/routes/project-index.js.map +1 -1
- package/dist/routes/project-settings-index.d.ts +10 -0
- package/dist/routes/project-settings-index.d.ts.map +1 -0
- package/dist/routes/project-settings-index.js +12 -0
- package/dist/routes/project-settings-index.js.map +1 -0
- package/dist/routes/project-settings.d.ts +6 -0
- package/dist/routes/project-settings.d.ts.map +1 -0
- package/dist/routes/project-settings.js +7 -0
- package/dist/routes/project-settings.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +11 -10
- package/src/chrome.tsx +27 -19
- package/src/components/model-provider-reminder-banner.tsx +3 -1
- package/src/components/project-crumb.tsx +18 -5
- package/src/components/project-switcher.test.tsx +7 -4
- package/src/components/project-switcher.tsx +16 -5
- package/src/core/definition.ts +7 -0
- package/src/core/project.ts +8 -0
- package/src/feature.ts +26 -4
- package/src/hooks/api/mappers.test.ts +19 -0
- package/src/hooks/api/mappers.ts +6 -0
- package/src/hooks/api/projects.test.ts +247 -1
- package/src/hooks/api/projects.ts +227 -2
- package/src/index.ts +9 -1
- package/src/pages/create-project-page.stories.tsx +5 -4
- package/src/pages/create-project-page.test.tsx +191 -12
- package/src/pages/create-project-page.tsx +102 -15
- package/src/pages/home-router.test.tsx +2 -2
- package/src/pages/project-settings-form-errors.ts +17 -0
- package/src/pages/project-settings-page.stories.tsx +216 -0
- package/src/pages/project-settings-page.test.tsx +74 -0
- package/src/pages/project-settings-page.tsx +220 -0
- package/src/pages/projects-hub-page.test.tsx +17 -11
- package/src/pages/projects-hub-page.tsx +9 -9
- package/src/project-error.test.ts +1 -0
- package/src/project-error.ts +8 -0
- package/src/routes/inputs.test.ts +8 -6
- package/src/routes/inputs.ts +7 -6
- package/src/routes/project-index.tsx +3 -3
- package/src/routes/project-settings-index.tsx +11 -0
- package/src/routes/project-settings.tsx +4 -0
- package/test/pages.tsx +26 -22
- 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": "14.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -27,16 +27,17 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@swc/helpers": "^0.5.17",
|
|
29
29
|
"@tanstack/react-form": "^1.32.0",
|
|
30
|
-
"@shipfox/api-
|
|
31
|
-
"@shipfox/api-
|
|
32
|
-
"@shipfox/api-
|
|
33
|
-
"@shipfox/
|
|
30
|
+
"@shipfox/api-common-dto": "12.0.0",
|
|
31
|
+
"@shipfox/api-definitions-dto": "12.0.0",
|
|
32
|
+
"@shipfox/api-integration-core-dto": "12.0.0",
|
|
33
|
+
"@shipfox/api-projects-dto": "12.0.0",
|
|
34
|
+
"@shipfox/client-agent": "14.0.0",
|
|
34
35
|
"@shipfox/client-api": "6.0.1",
|
|
35
|
-
"@shipfox/client-auth": "
|
|
36
|
-
"@shipfox/client-
|
|
37
|
-
"@shipfox/client-
|
|
38
|
-
"@shipfox/
|
|
39
|
-
"@shipfox/
|
|
36
|
+
"@shipfox/client-auth": "14.0.0",
|
|
37
|
+
"@shipfox/client-integrations": "14.0.0",
|
|
38
|
+
"@shipfox/client-shell": "14.0.0",
|
|
39
|
+
"@shipfox/client-ui": "14.0.0",
|
|
40
|
+
"@shipfox/react-ui": "0.5.0"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
43
|
"@tanstack/react-query": "^5.101.0",
|
package/src/chrome.tsx
CHANGED
|
@@ -3,29 +3,37 @@ import {
|
|
|
3
3
|
useActiveWorkspace,
|
|
4
4
|
useRouteParams,
|
|
5
5
|
} from '@shipfox/client-shell/runtime';
|
|
6
|
-
import {Outlet, useNavigate} from '@tanstack/react-router';
|
|
7
|
-
import {useEffect} from 'react';
|
|
8
6
|
import {ProjectCrumb} from '#components/project-crumb.js';
|
|
9
|
-
import {
|
|
10
|
-
|
|
7
|
+
import {resolveProjectSlug, useProjectSlugQuery} from '#hooks/api/projects.js';
|
|
8
|
+
|
|
9
|
+
export {resolveProjectSlug};
|
|
10
|
+
|
|
11
|
+
export function useMaybeActiveProjectQuery() {
|
|
12
|
+
const workspace = useActiveWorkspace();
|
|
13
|
+
const {projectSlug} = useRouteParams(parseWorkspaceProjectParams);
|
|
14
|
+
return useProjectSlugQuery(workspace.id, projectSlug);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useMaybeActiveProject() {
|
|
18
|
+
return useMaybeActiveProjectQuery().data;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useActiveProject() {
|
|
22
|
+
const project = useMaybeActiveProject();
|
|
23
|
+
if (!project) throw new Error('No active project is available for this route.');
|
|
24
|
+
return project;
|
|
25
|
+
}
|
|
11
26
|
|
|
12
27
|
export function ProjectBreadcrumb() {
|
|
13
28
|
const workspace = useActiveWorkspace();
|
|
14
|
-
const
|
|
15
|
-
const project = useProjectQuery(pid).data;
|
|
29
|
+
const project = useMaybeActiveProject();
|
|
16
30
|
return (
|
|
17
|
-
<ProjectCrumb
|
|
31
|
+
<ProjectCrumb
|
|
32
|
+
workspaceId={workspace.id}
|
|
33
|
+
workspaceSlug={workspace.slug}
|
|
34
|
+
projectId={project?.id}
|
|
35
|
+
projectSlug={project?.slug}
|
|
36
|
+
projectName={project?.name}
|
|
37
|
+
/>
|
|
18
38
|
);
|
|
19
39
|
}
|
|
20
|
-
|
|
21
|
-
export function ProjectLayoutGuard() {
|
|
22
|
-
const {wid, pid} = useRouteParams(projectRouteParams);
|
|
23
|
-
const navigate = useNavigate();
|
|
24
|
-
const project = useProjectQuery(pid).data;
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (project && project.workspaceId !== wid)
|
|
27
|
-
void navigate({to: '/workspaces/$wid', params: {wid}, replace: true});
|
|
28
|
-
}, [navigate, project, wid]);
|
|
29
|
-
if (project && project.workspaceId !== wid) return null;
|
|
30
|
-
return <Outlet />;
|
|
31
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {useModelProviderConfigsQuery} from '@shipfox/client-agent';
|
|
2
|
+
import {useActiveWorkspace} from '@shipfox/client-auth';
|
|
2
3
|
import {createTypedBrowserStorage, sessionStorageOrUndefined} from '@shipfox/client-ui';
|
|
3
4
|
import {
|
|
4
5
|
Alert,
|
|
@@ -13,6 +14,7 @@ import {Link} from '@tanstack/react-router';
|
|
|
13
14
|
import {useState} from 'react';
|
|
14
15
|
|
|
15
16
|
export function ModelProviderReminderBanner({workspaceId}: {workspaceId: string}) {
|
|
17
|
+
const workspace = useActiveWorkspace();
|
|
16
18
|
const configsQuery = useModelProviderConfigsQuery(workspaceId);
|
|
17
19
|
const [dismissed, setDismissed] = useState(() => isReminderDismissed(workspaceId));
|
|
18
20
|
const configs = configsQuery.data?.configs;
|
|
@@ -39,7 +41,7 @@ export function ModelProviderReminderBanner({workspaceId}: {workspaceId: string}
|
|
|
39
41
|
</AlertDescription>
|
|
40
42
|
<AlertActions>
|
|
41
43
|
<Button asChild size="sm" variant="secondary">
|
|
42
|
-
<Link to="/
|
|
44
|
+
<Link to="/w/$workspaceSlug/settings/agents" params={{workspaceSlug: workspace.slug}}>
|
|
43
45
|
Agents
|
|
44
46
|
</Link>
|
|
45
47
|
</Button>
|
|
@@ -6,19 +6,27 @@ import {ProjectSwitcher} from './project-switcher.js';
|
|
|
6
6
|
|
|
7
7
|
export interface ProjectCrumbProps {
|
|
8
8
|
workspaceId: string;
|
|
9
|
+
workspaceSlug: string;
|
|
9
10
|
projectId?: string | undefined;
|
|
11
|
+
projectSlug?: string | undefined;
|
|
10
12
|
projectName?: string | undefined;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export function ProjectCrumb({
|
|
15
|
+
export function ProjectCrumb({
|
|
16
|
+
workspaceId,
|
|
17
|
+
workspaceSlug,
|
|
18
|
+
projectId,
|
|
19
|
+
projectSlug,
|
|
20
|
+
projectName,
|
|
21
|
+
}: ProjectCrumbProps) {
|
|
14
22
|
const [open, setOpen] = useState(false);
|
|
15
23
|
|
|
16
|
-
if (
|
|
24
|
+
if (projectSlug && projectName) {
|
|
17
25
|
return (
|
|
18
26
|
<div className="flex items-center">
|
|
19
27
|
<Link
|
|
20
|
-
to="/
|
|
21
|
-
params={{
|
|
28
|
+
to="/w/$workspaceSlug/p/$projectSlug"
|
|
29
|
+
params={{workspaceSlug, projectSlug}}
|
|
22
30
|
aria-current="page"
|
|
23
31
|
className="text-md font-medium text-foreground-neutral-base px-6 py-4 rounded-6 hover:bg-background-components-hover transition-colors max-w-[240px] truncate"
|
|
24
32
|
>
|
|
@@ -39,6 +47,7 @@ export function ProjectCrumb({workspaceId, projectId, projectName}: ProjectCrumb
|
|
|
39
47
|
<PopoverContent className="w-[320px] p-0" align="start" sideOffset={8}>
|
|
40
48
|
<ProjectSwitcher
|
|
41
49
|
workspaceId={workspaceId}
|
|
50
|
+
workspaceSlug={workspaceSlug}
|
|
42
51
|
activeProjectId={projectId}
|
|
43
52
|
onSelect={() => setOpen(false)}
|
|
44
53
|
/>
|
|
@@ -63,7 +72,11 @@ export function ProjectCrumb({workspaceId, projectId, projectName}: ProjectCrumb
|
|
|
63
72
|
</button>
|
|
64
73
|
</PopoverTrigger>
|
|
65
74
|
<PopoverContent className="w-[320px] p-0" align="start" sideOffset={8}>
|
|
66
|
-
<ProjectSwitcher
|
|
75
|
+
<ProjectSwitcher
|
|
76
|
+
workspaceId={workspaceId}
|
|
77
|
+
workspaceSlug={workspaceSlug}
|
|
78
|
+
onSelect={() => setOpen(false)}
|
|
79
|
+
/>
|
|
67
80
|
</PopoverContent>
|
|
68
81
|
</Popover>
|
|
69
82
|
);
|
|
@@ -57,16 +57,18 @@ function renderProjectSwitcher({
|
|
|
57
57
|
const rootRoute = createRootRoute({component: Outlet});
|
|
58
58
|
const workspaceRoute = createRoute({
|
|
59
59
|
getParentRoute: () => rootRoute,
|
|
60
|
-
path: '/
|
|
61
|
-
component: () =>
|
|
60
|
+
path: '/w/$workspaceSlug',
|
|
61
|
+
component: () => (
|
|
62
|
+
<ProjectSwitcher workspaceId={PROJECT_TEST_WID} workspaceSlug="acme" onSelect={onSelect} />
|
|
63
|
+
),
|
|
62
64
|
});
|
|
63
65
|
const createProjectRoute = createRoute({
|
|
64
66
|
getParentRoute: () => rootRoute,
|
|
65
|
-
path: '/
|
|
67
|
+
path: '/w/$workspaceSlug/projects/new',
|
|
66
68
|
component: () => <div data-testid="create-project-route">Create project route</div>,
|
|
67
69
|
});
|
|
68
70
|
const router = createRouter({
|
|
69
|
-
history: createMemoryHistory({initialEntries: [
|
|
71
|
+
history: createMemoryHistory({initialEntries: ['/w/acme']}),
|
|
70
72
|
routeTree: rootRoute.addChildren([workspaceRoute, createProjectRoute]),
|
|
71
73
|
});
|
|
72
74
|
|
|
@@ -82,6 +84,7 @@ function projectDto({id, name}: {id: string; name: string}) {
|
|
|
82
84
|
id,
|
|
83
85
|
workspace_id: PROJECT_TEST_WID,
|
|
84
86
|
name,
|
|
87
|
+
slug: name.toLowerCase(),
|
|
85
88
|
source: {
|
|
86
89
|
connection_id: '33333333-3333-4333-8333-333333333333',
|
|
87
90
|
external_repository_id: name.toLowerCase(),
|
|
@@ -14,11 +14,17 @@ import {useProjectsInfiniteQuery} from '#hooks/api/projects.js';
|
|
|
14
14
|
|
|
15
15
|
export interface ProjectSwitcherProps {
|
|
16
16
|
workspaceId: string;
|
|
17
|
+
workspaceSlug: string;
|
|
17
18
|
activeProjectId?: string | undefined;
|
|
18
19
|
onSelect?: () => void;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
export function ProjectSwitcher({
|
|
22
|
+
export function ProjectSwitcher({
|
|
23
|
+
workspaceId,
|
|
24
|
+
workspaceSlug,
|
|
25
|
+
activeProjectId,
|
|
26
|
+
onSelect,
|
|
27
|
+
}: ProjectSwitcherProps) {
|
|
22
28
|
const navigate = useNavigate();
|
|
23
29
|
const [searchValue, setSearchValue] = useState('');
|
|
24
30
|
const query = useProjectsInfiniteQuery(workspaceId);
|
|
@@ -33,20 +39,25 @@ export function ProjectSwitcher({workspaceId, activeProjectId, onSelect}: Projec
|
|
|
33
39
|
}, [query.hasNextPage, query.isFetchingNextPage, query.fetchNextPage]);
|
|
34
40
|
|
|
35
41
|
const handleSelect = (projectId: string) => {
|
|
42
|
+
const project = projects.find((candidate) => candidate.id === projectId);
|
|
43
|
+
if (!project) return;
|
|
36
44
|
navigate({
|
|
37
|
-
to: '/
|
|
38
|
-
params: {
|
|
45
|
+
to: '/w/$workspaceSlug/p/$projectSlug',
|
|
46
|
+
params: {
|
|
47
|
+
workspaceSlug,
|
|
48
|
+
projectSlug: project.slug,
|
|
49
|
+
},
|
|
39
50
|
});
|
|
40
51
|
onSelect?.();
|
|
41
52
|
};
|
|
42
53
|
|
|
43
54
|
const handleSelectAll = () => {
|
|
44
|
-
navigate({to: '/
|
|
55
|
+
navigate({to: '/w/$workspaceSlug', params: {workspaceSlug}});
|
|
45
56
|
onSelect?.();
|
|
46
57
|
};
|
|
47
58
|
|
|
48
59
|
const handleCreate = () => {
|
|
49
|
-
navigate({to: '/
|
|
60
|
+
navigate({to: '/w/$workspaceSlug/projects/new', params: {workspaceSlug}});
|
|
50
61
|
onSelect?.();
|
|
51
62
|
};
|
|
52
63
|
|
package/src/core/definition.ts
CHANGED
|
@@ -22,6 +22,13 @@ export interface DefinitionSyncSummary {
|
|
|
22
22
|
finishedAt: string | null;
|
|
23
23
|
lastErrorCode: string | null;
|
|
24
24
|
lastErrorMessage: string | null;
|
|
25
|
+
warnings: DefinitionSyncWarning[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DefinitionSyncWarning {
|
|
29
|
+
code: string;
|
|
30
|
+
message: string;
|
|
31
|
+
path?: string | undefined;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export interface DefinitionList {
|
package/src/core/project.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface Project {
|
|
|
7
7
|
id: string;
|
|
8
8
|
workspaceId: string;
|
|
9
9
|
name: string;
|
|
10
|
+
slug: string;
|
|
10
11
|
source: ProjectSource;
|
|
11
12
|
createdAt: string;
|
|
12
13
|
updatedAt: string;
|
|
@@ -20,9 +21,16 @@ export interface ProjectList {
|
|
|
20
21
|
export interface CreateProjectCommand {
|
|
21
22
|
workspaceId: string;
|
|
22
23
|
name: string;
|
|
24
|
+
slug: string;
|
|
23
25
|
source: ProjectSource;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
export interface UpdateProjectCommand {
|
|
29
|
+
projectId: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
slug?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
const REPOSITORY_NAME_SPLIT_RE = /[/-]/;
|
|
27
35
|
|
|
28
36
|
export function projectNameFromRepository(repositoryId: string): string {
|
package/src/feature.ts
CHANGED
|
@@ -5,30 +5,52 @@ export const projectsNavigation = [
|
|
|
5
5
|
id: 'nav.projects',
|
|
6
6
|
scope: 'workspace',
|
|
7
7
|
label: 'Projects',
|
|
8
|
-
to: '/
|
|
8
|
+
to: '/w/$workspaceSlug',
|
|
9
9
|
exact: true,
|
|
10
10
|
order: 100,
|
|
11
11
|
},
|
|
12
12
|
] as const satisfies readonly NavTabEntry[];
|
|
13
13
|
|
|
14
|
+
export const projectSettingsSections = [
|
|
15
|
+
{
|
|
16
|
+
id: 'settings.project-general',
|
|
17
|
+
scope: 'project',
|
|
18
|
+
pathSegment: 'general',
|
|
19
|
+
label: 'General',
|
|
20
|
+
icon: 'settings3Line',
|
|
21
|
+
order: 50,
|
|
22
|
+
},
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
14
25
|
export const projectsFeature = defineClientFeature({
|
|
15
26
|
id: 'shipfox.projects',
|
|
16
27
|
routes: [
|
|
17
28
|
{
|
|
18
|
-
path: '/
|
|
29
|
+
path: '/w/$workspaceSlug',
|
|
19
30
|
parent: 'workspaceLayout',
|
|
20
31
|
impl: '@shipfox/client-projects/routes/home',
|
|
21
32
|
},
|
|
22
33
|
{
|
|
23
|
-
path: '/
|
|
34
|
+
path: '/w/$workspaceSlug/projects/new',
|
|
24
35
|
parent: 'workspaceLayout',
|
|
25
36
|
impl: '@shipfox/client-projects/routes/create-project',
|
|
26
37
|
},
|
|
27
38
|
{
|
|
28
|
-
path: '/
|
|
39
|
+
path: '/w/$workspaceSlug/p/$projectSlug',
|
|
29
40
|
parent: 'projectLayout',
|
|
30
41
|
impl: '@shipfox/client-projects/routes/project-index',
|
|
31
42
|
},
|
|
43
|
+
{
|
|
44
|
+
path: '/w/$workspaceSlug/p/$projectSlug/settings',
|
|
45
|
+
parent: 'projectSettings',
|
|
46
|
+
impl: '@shipfox/client-projects/routes/project-settings-index',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
path: '/w/$workspaceSlug/p/$projectSlug/settings/general',
|
|
50
|
+
parent: 'projectSettings',
|
|
51
|
+
impl: '@shipfox/client-projects/routes/project-settings',
|
|
52
|
+
},
|
|
32
53
|
],
|
|
33
54
|
navigation: projectsNavigation,
|
|
55
|
+
settingsSections: projectSettingsSections,
|
|
34
56
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {toDefinitionSyncSummary} from './mappers.js';
|
|
2
|
+
|
|
3
|
+
describe('toDefinitionSyncSummary', () => {
|
|
4
|
+
it('preserves warnings without manufacturing a missing path', () => {
|
|
5
|
+
const summary = toDefinitionSyncSummary({
|
|
6
|
+
ref: 'main',
|
|
7
|
+
status: 'succeeded',
|
|
8
|
+
last_sync_at: '2026-05-07T01:00:00.000Z',
|
|
9
|
+
started_at: '2026-05-07T00:59:55.000Z',
|
|
10
|
+
finished_at: '2026-05-07T01:00:00.000Z',
|
|
11
|
+
last_error_code: null,
|
|
12
|
+
last_error_message: null,
|
|
13
|
+
warnings: [{code: 'warning-code', message: 'Warning without a path'}],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(summary.warnings).toEqual([{code: 'warning-code', message: 'Warning without a path'}]);
|
|
17
|
+
expect(summary.warnings[0]).not.toHaveProperty('path');
|
|
18
|
+
});
|
|
19
|
+
});
|
package/src/hooks/api/mappers.ts
CHANGED
|
@@ -8,6 +8,7 @@ export function toProject(dto: ProjectResponseDto): Project {
|
|
|
8
8
|
id: dto.id,
|
|
9
9
|
workspaceId: dto.workspace_id,
|
|
10
10
|
name: dto.name,
|
|
11
|
+
slug: dto.slug,
|
|
11
12
|
source: {
|
|
12
13
|
connectionId: dto.source.connection_id,
|
|
13
14
|
externalRepositoryId: dto.source.external_repository_id,
|
|
@@ -51,6 +52,11 @@ export function toDefinitionSyncSummary(dto: DefinitionSyncSummaryDto): Definiti
|
|
|
51
52
|
finishedAt: dto.finished_at,
|
|
52
53
|
lastErrorCode: dto.last_error_code,
|
|
53
54
|
lastErrorMessage: dto.last_error_message,
|
|
55
|
+
warnings: dto.warnings.map((warning) => ({
|
|
56
|
+
code: warning.code,
|
|
57
|
+
message: warning.message,
|
|
58
|
+
...(warning.path === undefined ? {} : {path: warning.path}),
|
|
59
|
+
})),
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import {configureApiClient} from '@shipfox/client-api';
|
|
2
|
-
import {
|
|
2
|
+
import {QueryClient} from '@tanstack/react-query';
|
|
3
|
+
import {
|
|
4
|
+
createProject,
|
|
5
|
+
isProjectSlugAvailable,
|
|
6
|
+
listProjects,
|
|
7
|
+
projectSlugQueryOptions,
|
|
8
|
+
projectsInfiniteQueryOptions,
|
|
9
|
+
projectsQueryKeys,
|
|
10
|
+
resolveProjectSlug,
|
|
11
|
+
updateProject,
|
|
12
|
+
} from './projects.js';
|
|
3
13
|
|
|
4
14
|
function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
|
|
5
15
|
return new Response(JSON.stringify(body), {
|
|
@@ -49,6 +59,7 @@ describe('createProject', () => {
|
|
|
49
59
|
id: '44444444-4444-4444-8444-444444444444',
|
|
50
60
|
workspace_id: '11111111-1111-4111-8111-111111111111',
|
|
51
61
|
name: 'Platform',
|
|
62
|
+
slug: 'platform',
|
|
52
63
|
source: {
|
|
53
64
|
connection_id: '33333333-3333-4333-8333-333333333333',
|
|
54
65
|
external_repository_id: 'platform',
|
|
@@ -61,6 +72,7 @@ describe('createProject', () => {
|
|
|
61
72
|
const body = {
|
|
62
73
|
workspaceId: '11111111-1111-4111-8111-111111111111',
|
|
63
74
|
name: 'Platform',
|
|
75
|
+
slug: 'platform',
|
|
64
76
|
source: {
|
|
65
77
|
connectionId: '33333333-3333-4333-8333-333333333333',
|
|
66
78
|
externalRepositoryId: 'platform',
|
|
@@ -71,11 +83,13 @@ describe('createProject', () => {
|
|
|
71
83
|
|
|
72
84
|
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
73
85
|
expect(result.name).toBe('Platform');
|
|
86
|
+
expect(result.slug).toBe('platform');
|
|
74
87
|
expect(request.url).toBe('https://api.example.test/projects');
|
|
75
88
|
expect(request.method).toBe('POST');
|
|
76
89
|
expect(requestBody).toEqual({
|
|
77
90
|
workspace_id: body.workspaceId,
|
|
78
91
|
name: body.name,
|
|
92
|
+
slug: body.slug,
|
|
79
93
|
source: {
|
|
80
94
|
connection_id: body.source.connectionId,
|
|
81
95
|
external_repository_id: body.source.externalRepositoryId,
|
|
@@ -83,3 +97,235 @@ describe('createProject', () => {
|
|
|
83
97
|
});
|
|
84
98
|
});
|
|
85
99
|
});
|
|
100
|
+
|
|
101
|
+
describe('updateProject', () => {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl: undefined});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('patches the project body', async () => {
|
|
107
|
+
let requestBody: unknown;
|
|
108
|
+
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
|
109
|
+
requestBody = await (input as Request).clone().json();
|
|
110
|
+
return jsonResponse(projectResponse('renamed-project'));
|
|
111
|
+
});
|
|
112
|
+
configureApiClient({fetchImpl});
|
|
113
|
+
|
|
114
|
+
const result = await updateProject({
|
|
115
|
+
projectId: '33333333-3333-4333-8333-333333333333',
|
|
116
|
+
name: 'Renamed project',
|
|
117
|
+
slug: 'renamed-project',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const request = fetchImpl.mock.calls[0]?.[0] as Request;
|
|
121
|
+
expect(result.slug).toBe('renamed-project');
|
|
122
|
+
expect(request.url).toBe(
|
|
123
|
+
'https://api.example.test/projects/33333333-3333-4333-8333-333333333333',
|
|
124
|
+
);
|
|
125
|
+
expect(request.method).toBe('PATCH');
|
|
126
|
+
expect(requestBody).toEqual({name: 'Renamed project', slug: 'renamed-project'});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('isProjectSlugAvailable', () => {
|
|
131
|
+
test('uses cached project lists without making a request', () => {
|
|
132
|
+
const workspaceId = '11111111-1111-4111-8111-111111111111';
|
|
133
|
+
const queryClient = new QueryClient();
|
|
134
|
+
queryClient.setQueryData(projectsQueryKeys.list(workspaceId, 'taken'), {
|
|
135
|
+
pages: [
|
|
136
|
+
{
|
|
137
|
+
projects: [projectResponse('taken-project')],
|
|
138
|
+
nextCursor: null,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
pageParams: [undefined],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(isProjectSlugAvailable({queryClient, workspaceId, projectSlug: 'taken-project'})).toBe(
|
|
145
|
+
false,
|
|
146
|
+
);
|
|
147
|
+
expect(
|
|
148
|
+
isProjectSlugAvailable({
|
|
149
|
+
queryClient,
|
|
150
|
+
workspaceId,
|
|
151
|
+
projectSlug: 'taken-project',
|
|
152
|
+
currentProjectId: '33333333-3333-4333-8333-333333333333',
|
|
153
|
+
}),
|
|
154
|
+
).toBe(true);
|
|
155
|
+
expect(isProjectSlugAvailable({queryClient, workspaceId, projectSlug: 'new-project'})).toBe(
|
|
156
|
+
true,
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('resolveProjectSlug', () => {
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl: undefined});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('refreshes the cached list once after an exhausted slug search', async () => {
|
|
167
|
+
const workspaceId = '11111111-1111-4111-8111-111111111111';
|
|
168
|
+
const projectId = '44444444-4444-4444-8444-444444444444';
|
|
169
|
+
const fetchImpl = vi
|
|
170
|
+
.fn()
|
|
171
|
+
.mockResolvedValueOnce(
|
|
172
|
+
jsonResponse({projects: [projectResponse('old-project')], next_cursor: null}),
|
|
173
|
+
)
|
|
174
|
+
.mockResolvedValueOnce(
|
|
175
|
+
jsonResponse({projects: [projectResponse('new-project', projectId)], next_cursor: null}),
|
|
176
|
+
);
|
|
177
|
+
configureApiClient({fetchImpl});
|
|
178
|
+
const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}});
|
|
179
|
+
|
|
180
|
+
await queryClient.fetchInfiniteQuery(projectsInfiniteQueryOptions(workspaceId));
|
|
181
|
+
|
|
182
|
+
await expect(
|
|
183
|
+
resolveProjectSlug({queryClient, workspaceId, projectSlug: 'new-project'}),
|
|
184
|
+
).resolves.toBe(projectId);
|
|
185
|
+
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('revalidates a cached slug match before returning it', async () => {
|
|
189
|
+
const workspaceId = '11111111-1111-4111-8111-111111111111';
|
|
190
|
+
const projectId = '44444444-4444-4444-8444-444444444444';
|
|
191
|
+
const fetchImpl = vi
|
|
192
|
+
.fn()
|
|
193
|
+
.mockResolvedValueOnce(
|
|
194
|
+
jsonResponse({projects: [projectResponse('checkout-api', projectId)], next_cursor: null}),
|
|
195
|
+
)
|
|
196
|
+
.mockResolvedValueOnce(
|
|
197
|
+
jsonResponse({projects: [projectResponse('renamed-api', projectId)], next_cursor: null}),
|
|
198
|
+
);
|
|
199
|
+
configureApiClient({fetchImpl});
|
|
200
|
+
const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}});
|
|
201
|
+
|
|
202
|
+
await queryClient.fetchInfiniteQuery(projectsInfiniteQueryOptions(workspaceId));
|
|
203
|
+
queryClient.setQueryData(
|
|
204
|
+
projectsQueryKeys.list(workspaceId),
|
|
205
|
+
queryClient.getQueryData(projectsQueryKeys.list(workspaceId)),
|
|
206
|
+
{updatedAt: 0},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
await expect(
|
|
210
|
+
resolveProjectSlug({queryClient, workspaceId, projectSlug: 'checkout-api'}),
|
|
211
|
+
).resolves.toBeUndefined();
|
|
212
|
+
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('uses a fresh cached slug and hydrates the detail query', async () => {
|
|
216
|
+
const workspaceId = '11111111-1111-4111-8111-111111111111';
|
|
217
|
+
const projectId = '44444444-4444-4444-8444-444444444444';
|
|
218
|
+
const fetchImpl = vi
|
|
219
|
+
.fn()
|
|
220
|
+
.mockResolvedValueOnce(
|
|
221
|
+
jsonResponse({projects: [projectResponse('checkout-api', projectId)], next_cursor: null}),
|
|
222
|
+
);
|
|
223
|
+
configureApiClient({fetchImpl});
|
|
224
|
+
const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}});
|
|
225
|
+
|
|
226
|
+
await queryClient.fetchInfiniteQuery(projectsInfiniteQueryOptions(workspaceId));
|
|
227
|
+
|
|
228
|
+
await expect(
|
|
229
|
+
resolveProjectSlug({queryClient, workspaceId, projectSlug: 'checkout-api'}),
|
|
230
|
+
).resolves.toBe(projectId);
|
|
231
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
232
|
+
expect(queryClient.getQueryData(projectsQueryKeys.detail(projectId))).toMatchObject({
|
|
233
|
+
id: projectId,
|
|
234
|
+
slug: 'checkout-api',
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('paginates from the refreshed first page after a cached miss', async () => {
|
|
239
|
+
const workspaceId = '11111111-1111-4111-8111-111111111111';
|
|
240
|
+
const projectId = '44444444-4444-4444-8444-444444444444';
|
|
241
|
+
const fetchImpl = vi
|
|
242
|
+
.fn()
|
|
243
|
+
.mockResolvedValueOnce(
|
|
244
|
+
jsonResponse({
|
|
245
|
+
projects: [projectResponse('new-project', '55555555-5555-4555-8555-555555555555')],
|
|
246
|
+
next_cursor: 'cursor-1',
|
|
247
|
+
}),
|
|
248
|
+
)
|
|
249
|
+
.mockResolvedValueOnce(
|
|
250
|
+
jsonResponse({projects: [projectResponse('first-project')], next_cursor: 'cursor-1'}),
|
|
251
|
+
)
|
|
252
|
+
.mockResolvedValueOnce(
|
|
253
|
+
jsonResponse({projects: [projectResponse('new-project', projectId)], next_cursor: null}),
|
|
254
|
+
);
|
|
255
|
+
configureApiClient({fetchImpl});
|
|
256
|
+
const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}});
|
|
257
|
+
|
|
258
|
+
await queryClient.fetchInfiniteQuery(projectsInfiniteQueryOptions(workspaceId));
|
|
259
|
+
queryClient.setQueryData(
|
|
260
|
+
projectsQueryKeys.list(workspaceId),
|
|
261
|
+
queryClient.getQueryData(projectsQueryKeys.list(workspaceId)),
|
|
262
|
+
{updatedAt: 0},
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
await expect(
|
|
266
|
+
resolveProjectSlug({queryClient, workspaceId, projectSlug: 'new-project'}),
|
|
267
|
+
).resolves.toBe(projectId);
|
|
268
|
+
expect(fetchImpl).toHaveBeenCalledTimes(3);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('paginates slug search results until it finds the exact slug', async () => {
|
|
272
|
+
const workspaceId = '11111111-1111-4111-8111-111111111111';
|
|
273
|
+
const projectId = '44444444-4444-4444-8444-444444444444';
|
|
274
|
+
const fetchImpl = vi
|
|
275
|
+
.fn()
|
|
276
|
+
.mockResolvedValueOnce(
|
|
277
|
+
jsonResponse({projects: [projectResponse('old-project')], next_cursor: null}),
|
|
278
|
+
)
|
|
279
|
+
.mockResolvedValueOnce(
|
|
280
|
+
jsonResponse({projects: [projectResponse('still-old')], next_cursor: 'cursor-1'}),
|
|
281
|
+
)
|
|
282
|
+
.mockResolvedValueOnce(
|
|
283
|
+
jsonResponse({projects: [projectResponse('matching-name')], next_cursor: 'cursor-1'}),
|
|
284
|
+
)
|
|
285
|
+
.mockResolvedValueOnce(
|
|
286
|
+
jsonResponse({projects: [projectResponse('target-project', projectId)], next_cursor: null}),
|
|
287
|
+
);
|
|
288
|
+
configureApiClient({fetchImpl});
|
|
289
|
+
const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}});
|
|
290
|
+
|
|
291
|
+
await queryClient.fetchInfiniteQuery(projectsInfiniteQueryOptions(workspaceId));
|
|
292
|
+
|
|
293
|
+
await expect(
|
|
294
|
+
resolveProjectSlug({queryClient, workspaceId, projectSlug: 'target-project'}),
|
|
295
|
+
).resolves.toBe(projectId);
|
|
296
|
+
expect(fetchImpl).toHaveBeenCalledTimes(4);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe('projectSlugQueryOptions', () => {
|
|
301
|
+
beforeEach(() => {
|
|
302
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl: undefined});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('represents an unknown slug with null query data', async () => {
|
|
306
|
+
const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({projects: [], next_cursor: null}));
|
|
307
|
+
configureApiClient({fetchImpl});
|
|
308
|
+
const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}});
|
|
309
|
+
|
|
310
|
+
await expect(
|
|
311
|
+
queryClient.fetchQuery(
|
|
312
|
+
projectSlugQueryOptions('11111111-1111-4111-8111-111111111111', 'missing-project'),
|
|
313
|
+
),
|
|
314
|
+
).resolves.toBeNull();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
function projectResponse(slug: string, id = '33333333-3333-4333-8333-333333333333') {
|
|
319
|
+
return {
|
|
320
|
+
id,
|
|
321
|
+
workspace_id: '11111111-1111-4111-8111-111111111111',
|
|
322
|
+
name: slug,
|
|
323
|
+
slug,
|
|
324
|
+
source: {
|
|
325
|
+
connection_id: '22222222-2222-4222-8222-222222222222',
|
|
326
|
+
external_repository_id: slug,
|
|
327
|
+
},
|
|
328
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
329
|
+
updated_at: '2026-01-01T00:00:00.000Z',
|
|
330
|
+
};
|
|
331
|
+
}
|